From 73c9b685a7aedfecddb07c0fdfaa46914054d4d5 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Sat, 7 Mar 2026 06:48:32 -0600 Subject: [PATCH] fix(app): all panels transition --- packages/app/src/pages/layout.tsx | 2 +- packages/app/src/pages/session.tsx | 35 +- packages/app/src/pages/session/helpers.ts | 104 +++++- .../src/pages/session/session-side-panel.tsx | 323 ++++++++++-------- .../app/src/pages/session/terminal-panel.tsx | 239 +++++++------ 5 files changed, 449 insertions(+), 254 deletions(-) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 9c359aafb..70114623e 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -2252,7 +2252,7 @@ export default function Layout(props: ParentProps) { >
}> diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 077ab544d..cba49f5fb 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -33,7 +33,7 @@ import { usePrompt } from "@/context/prompt" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" 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 { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab" import { createScrollSpy } from "@/pages/session/scroll-spy" @@ -332,6 +332,7 @@ export default function Page() { ) const isDesktop = createMediaQuery("(min-width: 768px)") + const size = createSizing() const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen()) @@ -1252,9 +1253,9 @@ export default function Page() { {/* Session panel */}
- +
size.start()}> + { + size.touch() + layout.session.resize(width) + }} + /> +
- + diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index 60b26cdf4..be9656900 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -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) => { 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 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 + +export const createPresence = (open: Accessor, 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, + } +} diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index ffb6ab2e7..173b3db36 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -23,7 +23,7 @@ import { useLayout } from "@/context/layout" import { useSync } from "@/context/sync" import { createFileTabListSync } from "@/pages/session/file-tab-scroll" 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 { setSessionHandoff } from "@/pages/session/handoff" @@ -31,6 +31,7 @@ export function SessionSidePanel(props: { reviewPanel: () => JSX.Element activeDiff?: string focusReviewDiff: (path: string) => void + size: Sizing }) { const params = useParams() const layout = useLayout() @@ -46,8 +47,20 @@ export function SessionSidePanel(props: { const view = createMemo(() => layout.view(sessionKey)) 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 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 diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) @@ -210,146 +223,175 @@ export function SessionSidePanel(props: { }) return ( - + ) diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 69c8aefcc..d5eac2322 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -17,7 +17,7 @@ import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" import { useTerminal, type LocalPTY } from "@/context/terminal" 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" export function TerminalPanel() { @@ -33,8 +33,11 @@ export function TerminalPanel() { const opened = createMemo(() => view().terminal.opened()) const open = createMemo(() => isDesktop() && opened()) + const panel = createPresence(open) + const size = createSizing() const height = createMemo(() => layout.terminal.height()) const close = () => view().terminal.close() + let root: HTMLDivElement | undefined const [store, setStore] = createStore({ autoCreated: false, @@ -67,7 +70,7 @@ export function TerminalPanel() { on( () => terminal.active(), (activeId) => { - if (!activeId || !open()) return + if (!activeId || !panel.open()) return if (document.activeElement instanceof HTMLElement) { 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(() => { const dir = params.dir if (!dir) return @@ -133,120 +144,142 @@ export function TerminalPanel() { } return ( - +
- - -
- - {(title) => ( -
- {title} -
- )} -
-
-
- {language.t("common.loading")} - {language.t("common.loading.ellipsis")} +
+
size.start()}> + { + size.touch() + layout.terminal.resize(next) + }} + onCollapse={close} + /> +
+ +
+ + {(title) => ( +
+ {title} +
+ )} +
+
+
+ {language.t("common.loading")} + {language.t("common.loading.ellipsis")} +
+
+
+ {language.t("terminal.loading")}
-
{language.t("terminal.loading")}
-
- } - > - - - -
- terminal.open(id)} - class="!h-auto !flex-none" - > - - - - {(id) => ( - - {(pty) => } - - )} - - -
- - - -
-
-
-
- - {(id) => ( - - {(pty) => ( -
- terminal.clone(id)} /> + + + +
+ terminal.open(id)} + class="!h-auto !flex-none" + > + + + + {(id) => ( + + {(pty) => } + + )} + + +
+ + + +
+
+
+
+ + {(id) => ( + + {(pty) => ( +
+ terminal.clone(id)} + /> +
+ )} +
+ )} +
+
+
+ + + {(draggedId) => ( + + {(t) => ( +
+ {terminalTabLabel({ + title: t().title, + titleNumber: t().titleNumber, + t: language.t as (key: string, vars?: Record) => string, + })}
)}
)}
-
-
- - - {(draggedId) => ( - - {(t) => ( -
- {terminalTabLabel({ - title: t().title, - titleNumber: t().titleNumber, - t: language.t as (key: string, vars?: Record) => string, - })} -
- )} -
- )} -
-
- - + + + +
)