mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-18 22:54:41 +00:00
fix(app): all panels transition
This commit is contained in:
@@ -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" />}>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user