fix(app): all panels transition

This commit is contained in:
Adam
2026-03-07 06:48:32 -06:00
parent 99d8aab0ac
commit 73c9b685a7
5 changed files with 449 additions and 254 deletions

View File

@@ -2252,7 +2252,7 @@ export default function Layout(props: ParentProps) {
> >
<main <main
classList={{ classList={{
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base xl:border-l xl:rounded-tl-[12px]": true, "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
}} }}
> >
<Show when={!autoselecting()} fallback={<div class="size-full" />}> <Show when={!autoselecting()} fallback={<div class="size-full" />}>

View File

@@ -33,7 +33,7 @@ import { usePrompt } from "@/context/prompt"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer" import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
import { createOpenReviewFile } from "@/pages/session/helpers" import { createOpenReviewFile, createSizing } from "@/pages/session/helpers"
import { MessageTimeline } from "@/pages/session/message-timeline" import { MessageTimeline } from "@/pages/session/message-timeline"
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab" import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
import { createScrollSpy } from "@/pages/session/scroll-spy" import { createScrollSpy } from "@/pages/session/scroll-spy"
@@ -332,6 +332,7 @@ export default function Page() {
) )
const isDesktop = createMediaQuery("(min-width: 768px)") const isDesktop = createMediaQuery("(min-width: 768px)")
const size = createSizing()
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen()) const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
@@ -1252,9 +1253,9 @@ export default function Page() {
{/* Session panel */} {/* Session panel */}
<div <div
classList={{ classList={{
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger": true, "@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger flex-1 md:flex-none": true,
"flex-1": true, "transition-[width] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
"md:flex-none": desktopSidePanelOpen(), !size.active(),
}} }}
style={{ style={{
width: sessionPanelWidth(), width: sessionPanelWidth(),
@@ -1351,17 +1352,27 @@ export default function Page() {
/> />
<Show when={desktopReviewOpen()}> <Show when={desktopReviewOpen()}>
<ResizeHandle <div onPointerDown={() => size.start()}>
direction="horizontal" <ResizeHandle
size={layout.session.width()} direction="horizontal"
min={450} size={layout.session.width()}
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.45} min={450}
onResize={layout.session.resize} max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.45}
/> onResize={(width) => {
size.touch()
layout.session.resize(width)
}}
/>
</div>
</Show> </Show>
</div> </div>
<SessionSidePanel reviewPanel={reviewPanel} activeDiff={tree.activeDiff} focusReviewDiff={focusReviewDiff} /> <SessionSidePanel
reviewPanel={reviewPanel}
activeDiff={tree.activeDiff}
focusReviewDiff={focusReviewDiff}
size={size}
/>
</div> </div>
<TerminalPanel /> <TerminalPanel />

View File

@@ -1,4 +1,5 @@
import { batch } from "solid-js" import { batch, createEffect, on, onCleanup, onMount, type Accessor } from "solid-js"
import { createStore } from "solid-js/store"
export const focusTerminalById = (id: string) => { export const focusTerminalById = (id: string) => {
const wrapper = document.getElementById(`terminal-wrapper-${id}`) const wrapper = document.getElementById(`terminal-wrapper-${id}`)
@@ -69,3 +70,104 @@ export const getTabReorderIndex = (tabs: readonly string[], from: string, to: st
if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return undefined if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return undefined
return toIndex return toIndex
} }
export const createSizing = () => {
const [state, setState] = createStore({ active: false })
let t: number | undefined
const stop = () => {
if (t !== undefined) {
clearTimeout(t)
t = undefined
}
setState("active", false)
}
const start = () => {
if (t !== undefined) {
clearTimeout(t)
t = undefined
}
setState("active", true)
}
onMount(() => {
window.addEventListener("pointerup", stop)
window.addEventListener("pointercancel", stop)
window.addEventListener("blur", stop)
onCleanup(() => {
window.removeEventListener("pointerup", stop)
window.removeEventListener("pointercancel", stop)
window.removeEventListener("blur", stop)
})
})
onCleanup(() => {
if (t !== undefined) clearTimeout(t)
})
return {
active: () => state.active,
start,
touch() {
start()
t = window.setTimeout(stop, 120)
},
}
}
export type Sizing = ReturnType<typeof createSizing>
export const createPresence = (open: Accessor<boolean>, wait = 200) => {
const [state, setState] = createStore({
show: open(),
open: open(),
})
let frame: number | undefined
let t: number | undefined
const clear = () => {
if (frame !== undefined) {
cancelAnimationFrame(frame)
frame = undefined
}
if (t !== undefined) {
clearTimeout(t)
t = undefined
}
}
createEffect(
on(open, (next) => {
clear()
if (next) {
if (state.show) {
setState("open", true)
return
}
setState({ show: true, open: false })
frame = requestAnimationFrame(() => {
frame = undefined
setState("open", true)
})
return
}
if (!state.show) return
setState("open", false)
t = window.setTimeout(() => {
t = undefined
setState("show", false)
}, wait)
}),
)
onCleanup(clear)
return {
show: () => state.show,
open: () => state.open,
}
}

View File

@@ -23,7 +23,7 @@ import { useLayout } from "@/context/layout"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { createFileTabListSync } from "@/pages/session/file-tab-scroll" import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
import { FileTabContent } from "@/pages/session/file-tabs" import { FileTabContent } from "@/pages/session/file-tabs"
import { createOpenSessionFileTab, getTabReorderIndex } from "@/pages/session/helpers" import { createOpenSessionFileTab, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
import { StickyAddButton } from "@/pages/session/review-tab" import { StickyAddButton } from "@/pages/session/review-tab"
import { setSessionHandoff } from "@/pages/session/handoff" import { setSessionHandoff } from "@/pages/session/handoff"
@@ -31,6 +31,7 @@ export function SessionSidePanel(props: {
reviewPanel: () => JSX.Element reviewPanel: () => JSX.Element
activeDiff?: string activeDiff?: string
focusReviewDiff: (path: string) => void focusReviewDiff: (path: string) => void
size: Sizing
}) { }) {
const params = useParams() const params = useParams()
const layout = useLayout() const layout = useLayout()
@@ -46,8 +47,20 @@ export function SessionSidePanel(props: {
const view = createMemo(() => layout.view(sessionKey)) const view = createMemo(() => layout.view(sessionKey))
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
const open = createMemo(() => isDesktop() && (view().reviewPanel.opened() || layout.fileTree.opened())) const fileOpen = createMemo(() => isDesktop() && layout.fileTree.opened())
const open = createMemo(() => reviewOpen() || fileOpen())
const reviewTab = createMemo(() => isDesktop()) const reviewTab = createMemo(() => isDesktop())
const panelWidth = createMemo(() => {
if (!open()) return "0px"
if (reviewOpen()) return `calc(100% - ${layout.session.width()}px)`
return `${layout.fileTree.width()}px`
})
const reviewWidth = createMemo(() => {
if (!reviewOpen()) return "0px"
if (!fileOpen()) return "100%"
return `calc(100% - ${layout.fileTree.width()}px)`
})
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
@@ -210,146 +223,175 @@ export function SessionSidePanel(props: {
}) })
return ( return (
<Show when={open()}> <Show when={isDesktop()}>
<aside <aside
id="review-panel" id="review-panel"
aria-label={language.t("session.panel.reviewAndFiles")} aria-label={language.t("session.panel.reviewAndFiles")}
class="relative min-w-0 h-full border-l border-border-weaker-base flex" aria-hidden={!open()}
inert={!open()}
class="relative min-w-0 h-full flex shrink-0 overflow-hidden bg-background-base"
classList={{ classList={{
"flex-1": reviewOpen(), "opacity-100": open(),
"shrink-0": !reviewOpen(), "opacity-0 pointer-events-none": !open(),
"transition-[width,opacity] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!props.size.active(),
}} }}
style={{ width: reviewOpen() ? undefined : `${layout.fileTree.width()}px` }} style={{ width: panelWidth() }}
> >
<Show when={reviewOpen()}> <div class="size-full flex border-l border-border-weaker-base">
<div class="flex-1 min-w-0 h-full"> <div
<DragDropProvider aria-hidden={!reviewOpen()}
onDragStart={handleDragStart} inert={!reviewOpen()}
onDragEnd={handleDragEnd} class="relative min-w-0 h-full shrink-0 overflow-hidden bg-background-base"
onDragOver={handleDragOver} classList={{
collisionDetector={closestCenter} "opacity-100": reviewOpen(),
> "opacity-0 pointer-events-none": !reviewOpen(),
<DragDropSensors /> "transition-[width,opacity] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
<ConstrainDragYAxis /> !props.size.active(),
<Tabs value={activeTab()} onChange={openTab}> }}
<div class="sticky top-0 shrink-0 flex"> style={{ width: reviewWidth() }}
<Tabs.List >
ref={(el: HTMLDivElement) => { <div class="size-full min-w-0 h-full bg-background-base">
const stop = createFileTabListSync({ el, contextOpen }) <DragDropProvider
onCleanup(stop) onDragStart={handleDragStart}
}} onDragEnd={handleDragEnd}
> onDragOver={handleDragOver}
<Show when={reviewTab()}> collisionDetector={closestCenter}
<Tabs.Trigger value="review"> >
<div class="flex items-center gap-1.5"> <DragDropSensors />
<div>{language.t("session.tab.review")}</div> <ConstrainDragYAxis />
<Show when={hasReview()}> <Tabs value={activeTab()} onChange={openTab}>
<div>{reviewCount()}</div> <div class="sticky top-0 shrink-0 flex">
</Show> <Tabs.List
</div> ref={(el: HTMLDivElement) => {
</Tabs.Trigger> const stop = createFileTabListSync({ el, contextOpen })
</Show> onCleanup(stop)
<Show when={contextOpen()}> }}
<Tabs.Trigger >
value="context" <Show when={reviewTab()}>
closeButton={ <Tabs.Trigger value="review">
<TooltipKeybind <div class="flex items-center gap-1.5">
title={language.t("common.closeTab")} <div>{language.t("session.tab.review")}</div>
keybind={command.keybind("tab.close")} <Show when={hasReview()}>
placement="bottom" <div>{reviewCount()}</div>
gutter={10} </Show>
> </div>
<IconButton </Tabs.Trigger>
icon="close-small" </Show>
variant="ghost" <Show when={contextOpen()}>
class="h-5 w-5" <Tabs.Trigger
onClick={() => tabs().close("context")} value="context"
aria-label={language.t("common.closeTab")} closeButton={
/> <TooltipKeybind
</TooltipKeybind> title={language.t("common.closeTab")}
} keybind={command.keybind("tab.close")}
hideCloseButton placement="bottom"
onMiddleClick={() => tabs().close("context")} gutter={10}
> >
<div class="flex items-center gap-2"> <IconButton
<SessionContextUsage variant="indicator" /> icon="close-small"
<div>{language.t("session.tab.context")}</div> variant="ghost"
</div> class="h-5 w-5"
</Tabs.Trigger> onClick={() => tabs().close("context")}
</Show> aria-label={language.t("common.closeTab")}
<SortableProvider ids={openedTabs()}> />
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For> </TooltipKeybind>
</SortableProvider> }
<StickyAddButton> hideCloseButton
<TooltipKeybind onMiddleClick={() => tabs().close("context")}
title={language.t("command.file.open")} >
keybind={command.keybind("file.open")} <div class="flex items-center gap-2">
class="flex items-center" <SessionContextUsage variant="indicator" />
> <div>{language.t("session.tab.context")}</div>
<IconButton </div>
icon="plus-small" </Tabs.Trigger>
variant="ghost" </Show>
iconSize="large" <SortableProvider ids={openedTabs()}>
class="!rounded-md" <For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
onClick={() => dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)} </SortableProvider>
aria-label={language.t("command.file.open")} <StickyAddButton>
/> <TooltipKeybind
</TooltipKeybind> title={language.t("command.file.open")}
</StickyAddButton> keybind={command.keybind("file.open")}
</Tabs.List> class="flex items-center"
</div> >
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
class="!rounded-md"
onClick={() =>
dialog.show(() => <DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
}
aria-label={language.t("command.file.open")}
/>
</TooltipKeybind>
</StickyAddButton>
</Tabs.List>
</div>
<Show when={reviewTab()}> <Show when={reviewTab()}>
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict"> <Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "review"}>{props.reviewPanel()}</Show> <Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
</Tabs.Content> </Tabs.Content>
</Show>
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "empty"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" />
<div class="text-14-regular text-text-weak max-w-56">
{language.t("session.files.selectToOpen")}
</div>
</div>
</div>
</Show> </Show>
</Tabs.Content>
<Show when={contextOpen()}> <Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict"> <Show when={activeTab() === "empty"}>
<Show when={activeTab() === "context"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden"> <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<SessionContextTab /> <div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" />
<div class="text-14-regular text-text-weak max-w-56">
{language.t("session.files.selectToOpen")}
</div>
</div>
</div> </div>
</Show> </Show>
</Tabs.Content> </Tabs.Content>
</Show>
<Show when={activeFileTab()} keyed> <Show when={contextOpen()}>
{(tab) => <FileTabContent tab={tab} />} <Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
</Show> <Show when={activeTab() === "context"}>
</Tabs> <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<DragOverlay> <SessionContextTab />
<Show when={store.activeDraggable} keyed> </div>
{(tab) => { </Show>
const path = createMemo(() => file.pathFromTab(tab)) </Tabs.Content>
return ( </Show>
<div data-component="tabs-drag-preview">
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show> <Show when={activeFileTab()} keyed>
</div> {(tab) => <FileTabContent tab={tab} />}
) </Show>
}} </Tabs>
</Show> <DragOverlay>
</DragOverlay> <Show when={store.activeDraggable} keyed>
</DragDropProvider> {(tab) => {
const path = createMemo(() => file.pathFromTab(tab))
return (
<div data-component="tabs-drag-preview">
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
</div> </div>
</Show>
<Show when={layout.fileTree.opened()}> <div
<div id="file-tree-panel" class="relative shrink-0 h-full" style={{ width: `${layout.fileTree.width()}px` }}> id="file-tree-panel"
aria-hidden={!fileOpen()}
inert={!fileOpen()}
class="relative min-w-0 h-full shrink-0 overflow-hidden"
classList={{
"opacity-100": fileOpen(),
"opacity-0 pointer-events-none": !fileOpen(),
"transition-[width,opacity] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!props.size.active(),
}}
style={{ width: treeWidth() }}
>
<div <div
class="h-full flex flex-col overflow-hidden group/filetree" class="h-full flex flex-col overflow-hidden group/filetree"
classList={{ "border-l border-border-weaker-base": reviewOpen() }} classList={{ "border-l border-border-weaker-base": reviewOpen() }}
@@ -412,18 +454,25 @@ export function SessionSidePanel(props: {
</Tabs.Content> </Tabs.Content>
</Tabs> </Tabs>
</div> </div>
<ResizeHandle <Show when={fileOpen()}>
direction="horizontal" <div onPointerDown={() => props.size.start()}>
edge="start" <ResizeHandle
size={layout.fileTree.width()} direction="horizontal"
min={200} edge="start"
max={480} size={layout.fileTree.width()}
collapseThreshold={160} min={200}
onResize={layout.fileTree.resize} max={480}
onCollapse={layout.fileTree.close} collapseThreshold={160}
/> onResize={(width) => {
props.size.touch()
layout.fileTree.resize(width)
}}
onCollapse={layout.fileTree.close}
/>
</div>
</Show>
</div> </div>
</Show> </div>
</aside> </aside>
</Show> </Show>
) )

View File

@@ -17,7 +17,7 @@ import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useTerminal, type LocalPTY } from "@/context/terminal" import { useTerminal, type LocalPTY } from "@/context/terminal"
import { terminalTabLabel } from "@/pages/session/terminal-label" import { terminalTabLabel } from "@/pages/session/terminal-label"
import { focusTerminalById } from "@/pages/session/helpers" import { createPresence, createSizing, focusTerminalById } from "@/pages/session/helpers"
import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
export function TerminalPanel() { export function TerminalPanel() {
@@ -33,8 +33,11 @@ export function TerminalPanel() {
const opened = createMemo(() => view().terminal.opened()) const opened = createMemo(() => view().terminal.opened())
const open = createMemo(() => isDesktop() && opened()) const open = createMemo(() => isDesktop() && opened())
const panel = createPresence(open)
const size = createSizing()
const height = createMemo(() => layout.terminal.height()) const height = createMemo(() => layout.terminal.height())
const close = () => view().terminal.close() const close = () => view().terminal.close()
let root: HTMLDivElement | undefined
const [store, setStore] = createStore({ const [store, setStore] = createStore({
autoCreated: false, autoCreated: false,
@@ -67,7 +70,7 @@ export function TerminalPanel() {
on( on(
() => terminal.active(), () => terminal.active(),
(activeId) => { (activeId) => {
if (!activeId || !open()) return if (!activeId || !panel.open()) return
if (document.activeElement instanceof HTMLElement) { if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur() document.activeElement.blur()
} }
@@ -76,6 +79,14 @@ export function TerminalPanel() {
), ),
) )
createEffect(() => {
if (panel.open()) return
const active = document.activeElement
if (!(active instanceof HTMLElement)) return
if (!root?.contains(active)) return
active.blur()
})
createEffect(() => { createEffect(() => {
const dir = params.dir const dir = params.dir
if (!dir) return if (!dir) return
@@ -133,120 +144,142 @@ export function TerminalPanel() {
} }
return ( return (
<Show when={open()}> <Show when={panel.show()}>
<div <div
ref={root}
id="terminal-panel" id="terminal-panel"
role="region" role="region"
aria-label={language.t("terminal.title")} aria-label={language.t("terminal.title")}
class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base" aria-hidden={!panel.open()}
style={{ height: `${height()}px` }} inert={!panel.open()}
class="relative w-full shrink-0 overflow-hidden"
classList={{
"opacity-100": panel.open(),
"opacity-0 pointer-events-none": !panel.open(),
"transition-[height,opacity] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[height] motion-reduce:transition-none":
!size.active(),
}}
style={{ height: panel.open() ? `${height()}px` : "0px" }}
> >
<ResizeHandle <div class="size-full flex flex-col border-t border-border-weak-base">
direction="vertical" <div onPointerDown={() => size.start()}>
size={height()} <ResizeHandle
min={100} direction="vertical"
max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6} size={height()}
collapseThreshold={50} min={100}
onResize={layout.terminal.resize} max={typeof window === "undefined" ? 1000 : window.innerHeight * 0.6}
onCollapse={close} collapseThreshold={50}
/> onResize={(next) => {
<Show size.touch()
when={terminal.ready()} layout.terminal.resize(next)
fallback={ }}
<div class="flex flex-col h-full pointer-events-none"> onCollapse={close}
<div class="h-10 flex items-center gap-2 px-2 border-b border-border-weaker-base bg-background-stronger overflow-hidden"> />
<For each={handoff()}> </div>
{(title) => ( <Show
<div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40"> when={terminal.ready()}
{title} fallback={
</div> <div class="flex flex-col h-full pointer-events-none">
)} <div class="h-10 flex items-center gap-2 px-2 border-b border-border-weaker-base bg-background-stronger overflow-hidden">
</For> <For each={handoff()}>
<div class="flex-1" /> {(title) => (
<div class="text-text-weak pr-2"> <div class="px-2 py-1 rounded-md bg-surface-base text-14-regular text-text-weak truncate max-w-40">
{language.t("common.loading")} {title}
{language.t("common.loading.ellipsis")} </div>
)}
</For>
<div class="flex-1" />
<div class="text-text-weak pr-2">
{language.t("common.loading")}
{language.t("common.loading.ellipsis")}
</div>
</div>
<div class="flex-1 flex items-center justify-center text-text-weak">
{language.t("terminal.loading")}
</div> </div>
</div> </div>
<div class="flex-1 flex items-center justify-center text-text-weak">{language.t("terminal.loading")}</div> }
</div>
}
>
<DragDropProvider
onDragStart={handleTerminalDragStart}
onDragEnd={handleTerminalDragEnd}
onDragOver={handleTerminalDragOver}
collisionDetector={closestCenter}
> >
<DragDropSensors /> <DragDropProvider
<ConstrainDragYAxis /> onDragStart={handleTerminalDragStart}
<div class="flex flex-col h-full"> onDragEnd={handleTerminalDragEnd}
<Tabs onDragOver={handleTerminalDragOver}
variant="alt" collisionDetector={closestCenter}
value={terminal.active()} >
onChange={(id) => terminal.open(id)} <DragDropSensors />
class="!h-auto !flex-none" <ConstrainDragYAxis />
> <div class="flex flex-col h-full">
<Tabs.List class="h-10 border-b border-border-weaker-base"> <Tabs
<SortableProvider ids={ids()}> variant="alt"
<For each={ids()}> value={terminal.active()}
{(id) => ( onChange={(id) => terminal.open(id)}
<Show when={byId().get(id)}> class="!h-auto !flex-none"
{(pty) => <SortableTerminalTab terminal={pty()} onClose={close} />} >
</Show> <Tabs.List class="h-10 border-b border-border-weaker-base">
)} <SortableProvider ids={ids()}>
</For> <For each={ids()}>
</SortableProvider> {(id) => (
<div class="h-full flex items-center justify-center"> <Show when={byId().get(id)}>
<TooltipKeybind {(pty) => <SortableTerminalTab terminal={pty()} onClose={close} />}
title={language.t("command.terminal.new")} </Show>
keybind={command.keybind("terminal.new")} )}
class="flex items-center" </For>
> </SortableProvider>
<IconButton <div class="h-full flex items-center justify-center">
icon="plus-small" <TooltipKeybind
variant="ghost" title={language.t("command.terminal.new")}
iconSize="large" keybind={command.keybind("terminal.new")}
onClick={terminal.new} class="flex items-center"
aria-label={language.t("command.terminal.new")} >
/> <IconButton
</TooltipKeybind> icon="plus-small"
</div> variant="ghost"
</Tabs.List> iconSize="large"
</Tabs> onClick={terminal.new}
<div class="flex-1 min-h-0 relative"> aria-label={language.t("command.terminal.new")}
<Show when={terminal.active()} keyed> />
{(id) => ( </TooltipKeybind>
<Show when={byId().get(id)}> </div>
{(pty) => ( </Tabs.List>
<div id={`terminal-wrapper-${id}`} class="absolute inset-0"> </Tabs>
<Terminal pty={pty()} onCleanup={terminal.update} onConnectError={() => terminal.clone(id)} /> <div class="flex-1 min-h-0 relative">
<Show when={terminal.active()} keyed>
{(id) => (
<Show when={byId().get(id)}>
{(pty) => (
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
<Terminal
pty={pty()}
onCleanup={terminal.update}
onConnectError={() => terminal.clone(id)}
/>
</div>
)}
</Show>
)}
</Show>
</div>
</div>
<DragOverlay>
<Show when={store.activeDraggable}>
{(draggedId) => (
<Show when={byId().get(draggedId())}>
{(t) => (
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
{terminalTabLabel({
title: t().title,
titleNumber: t().titleNumber,
t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string,
})}
</div> </div>
)} )}
</Show> </Show>
)} )}
</Show> </Show>
</div> </DragOverlay>
</div> </DragDropProvider>
<DragOverlay> </Show>
<Show when={store.activeDraggable}> </div>
{(draggedId) => (
<Show when={byId().get(draggedId())}>
{(t) => (
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
{terminalTabLabel({
title: t().title,
titleNumber: t().titleNumber,
t: language.t as (key: string, vars?: Record<string, string | number | boolean>) => string,
})}
</div>
)}
</Show>
)}
</Show>
</DragOverlay>
</DragDropProvider>
</Show>
</div> </div>
</Show> </Show>
) )