diff options
Diffstat (limited to 'packages/app/src/pages/layout.tsx')
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 200 |
1 files changed, 162 insertions, 38 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index cf2c3b6c4..c9f6ae26f 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -155,6 +155,8 @@ export default function Layout(props: ParentProps) { const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)] const navLeave = { current: undefined as number | undefined } const [sortNow, setSortNow] = createSignal(Date.now()) + const [sizing, setSizing] = createSignal(false) + let sizet: number | undefined let sortNowInterval: ReturnType<typeof setInterval> | undefined const sortNowTimeout = setTimeout( () => { @@ -167,7 +169,7 @@ export default function Layout(props: ParentProps) { const aim = createAim({ enabled: () => !layout.sidebar.opened(), active: () => state.hoverProject, - el: () => state.nav, + el: () => state.nav?.querySelector<HTMLElement>("[data-component='sidebar-rail']") ?? state.nav, onActivate: (directory) => { globalSync.child(directory) setState("hoverProject", directory) @@ -179,9 +181,23 @@ export default function Layout(props: ParentProps) { if (navLeave.current !== undefined) clearTimeout(navLeave.current) clearTimeout(sortNowTimeout) if (sortNowInterval) clearInterval(sortNowInterval) + if (sizet !== undefined) clearTimeout(sizet) + if (peekt !== undefined) clearTimeout(peekt) aim.reset() }) + onMount(() => { + const stop = () => setSizing(false) + 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) + }) + }) + const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined) const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering()) const setHoverProject = (value: string | undefined) => { @@ -192,6 +208,27 @@ export default function Layout(props: ParentProps) { const clearHoverProjectSoon = () => queueMicrotask(() => setHoverProject(undefined)) const setHoverSession = (id: string | undefined) => setState("hoverSession", id) + const disarm = () => { + if (navLeave.current === undefined) return + clearTimeout(navLeave.current) + navLeave.current = undefined + } + + const arm = () => { + if (layout.sidebar.opened()) return + if (state.hoverProject === undefined) return + disarm() + navLeave.current = window.setTimeout(() => { + navLeave.current = undefined + setHoverProject(undefined) + setState("hoverSession", undefined) + }, 300) + } + + const [peek, setPeek] = createSignal<LocalProject | undefined>(undefined) + const [peeked, setPeeked] = createSignal(false) + let peekt: number | undefined + const hoverProjectData = createMemo(() => { const id = state.hoverProject if (!id) return @@ -199,6 +236,27 @@ export default function Layout(props: ParentProps) { }) createEffect(() => { + const p = hoverProjectData() + if (p) { + if (peekt !== undefined) { + clearTimeout(peekt) + peekt = undefined + } + setPeek(p) + setPeeked(true) + return + } + + setPeeked(false) + if (peek() === undefined) return + if (peekt !== undefined) clearTimeout(peekt) + peekt = window.setTimeout(() => { + peekt = undefined + setPeek(undefined) + }, 180) + }) + + createEffect(() => { if (!layout.sidebar.opened()) return setHoverProject(undefined) }) @@ -1123,6 +1181,12 @@ export default function Layout(props: ParentProps) { } const openSession = async (target: { directory: string; id: string }) => { if (!canOpen(target.directory)) return false + const [data] = globalSync.child(target.directory, { bootstrap: false }) + if (data.session.some((item) => item.id === target.id)) { + setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() }) + navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`) + return true + } const resolved = await globalSDK.client.session .get({ sessionID: target.id }) .then((x) => x.data) @@ -1813,7 +1877,8 @@ export default function Layout(props: ParentProps) { setHoverSession, } - const SidebarPanel = (panelProps: { project: LocalProject | undefined; mobile?: boolean }) => { + const SidebarPanel = (panelProps: { project: LocalProject | undefined; mobile?: boolean; merged?: boolean }) => { + const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened())) const projectName = createMemo(() => { const project = panelProps.project if (!project) return "" @@ -1839,10 +1904,17 @@ export default function Layout(props: ParentProps) { return ( <div classList={{ - "flex flex-col min-h-0 bg-background-stronger border border-b-0 border-border-weak-base rounded-tl-[12px]": true, + "flex flex-col min-h-0 min-w-0 rounded-tl-[12px] px-2": true, + "border border-b-0 border-border-weak-base": !merged(), + "border-l border-t border-border-weaker-base": merged(), + "bg-background-base": merged(), + "bg-background-stronger": !merged(), "flex-1 min-w-0": panelProps.mobile, + "max-w-full overflow-hidden": panelProps.mobile, + }} + style={{ + width: panelProps.mobile ? undefined : `${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px`, }} - style={{ width: panelProps.mobile ? undefined : `${Math.max(layout.sidebar.width() - 64, 0)}px` }} > <Show when={panelProps.project}> {(p) => ( @@ -2041,33 +2113,27 @@ export default function Layout(props: ParentProps) { return ( <div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"> <Titlebar /> - <div class="flex-1 min-h-0 flex"> + <div class="flex-1 min-h-0 relative overflow-x-hidden"> <nav aria-label={language.t("sidebar.nav.projectsAndSessions")} data-component="sidebar-nav-desktop" classList={{ "hidden xl:block": true, - "relative shrink-0": true, + "absolute inset-y-0 left-0": true, + "z-10": true, }} - style={{ width: layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "64px" }} + style={{ width: `${Math.max(layout.sidebar.width(), 244)}px` }} ref={(el) => { setState("nav", el) }} onMouseEnter={() => { - if (navLeave.current === undefined) return - clearTimeout(navLeave.current) - navLeave.current = undefined + disarm() }} onMouseLeave={() => { aim.reset() if (!sidebarHovering()) return - if (navLeave.current !== undefined) clearTimeout(navLeave.current) - navLeave.current = window.setTimeout(() => { - navLeave.current = undefined - setHoverProject(undefined) - setState("hoverSession", undefined) - }, 300) + arm() }} > <div class="@container w-full h-full contain-strict"> @@ -2094,28 +2160,36 @@ export default function Layout(props: ParentProps) { onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} renderPanel={() => ( <Show when={currentProject()} keyed> - {(project) => <SidebarPanel project={project} />} + {(project) => <SidebarPanel project={project} merged />} </Show> )} /> </div> - <Show when={!layout.sidebar.opened() ? hoverProjectData()?.worktree : undefined} keyed> - <div class="absolute inset-y-0 left-16 z-50 flex" onMouseEnter={aim.reset}> - <SidebarPanel project={hoverProjectData()} /> - </div> - </Show> <Show when={layout.sidebar.opened()}> - <ResizeHandle - direction="horizontal" - size={layout.sidebar.width()} - min={244} - max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64} - collapseThreshold={244} - onResize={layout.sidebar.resize} - onCollapse={layout.sidebar.close} - /> + <div onPointerDown={() => setSizing(true)}> + <ResizeHandle + direction="horizontal" + size={layout.sidebar.width()} + min={244} + max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64} + collapseThreshold={244} + onResize={(w) => { + setSizing(true) + if (sizet !== undefined) clearTimeout(sizet) + sizet = window.setTimeout(() => setSizing(false), 120) + layout.sidebar.resize(w) + }} + onCollapse={layout.sidebar.close} + /> + </div> </Show> </nav> + + <div + class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base" + style={{ left: "calc(4rem + 12px)" }} + /> + <div class="xl:hidden"> <div classList={{ @@ -2131,7 +2205,7 @@ export default function Layout(props: ParentProps) { aria-label={language.t("sidebar.nav.projectsAndSessions")} data-component="sidebar-nav-mobile" classList={{ - "@container fixed top-10 bottom-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true, + "@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true, "translate-x-0": layout.mobileSidebar.opened(), "-translate-x-full": !layout.mobileSidebar.opened(), }} @@ -2164,16 +2238,66 @@ export default function Layout(props: ParentProps) { </nav> </div> - <main + <div classList={{ - "size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base": true, - "xl:border-l xl:rounded-tl-[12px]": !layout.sidebar.opened(), + "absolute inset-0": true, + "xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true, + "z-20": true, + "transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none": + !sizing(), + }} + style={{ + "--main-left": layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "4rem", }} > - <Show when={!autoselecting()} fallback={<div class="size-full" />}> - {props.children} + <main + 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, + }} + > + <Show when={!autoselecting()} fallback={<div class="size-full" />}> + {props.children} + </Show> + </main> + </div> + + <div + classList={{ + "hidden xl:flex absolute inset-y-0 left-16 z-30": true, + "opacity-100 translate-x-0 pointer-events-auto": peeked() && !layout.sidebar.opened(), + "opacity-0 -translate-x-2 pointer-events-none": !peeked() || layout.sidebar.opened(), + "transition-[opacity,transform] motion-reduce:transition-none": true, + "duration-180 ease-out": peeked() && !layout.sidebar.opened(), + "duration-120 ease-in": !peeked() || layout.sidebar.opened(), + }} + onMouseMove={disarm} + onMouseEnter={() => { + disarm() + aim.reset() + }} + onPointerDown={disarm} + onMouseLeave={() => { + arm() + }} + > + <Show when={peek()} keyed> + {(project) => <SidebarPanel project={project} merged={false} />} </Show> - </main> + </div> + + <div + classList={{ + "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true, + "opacity-100 translate-x-0": peeked() && !layout.sidebar.opened(), + "opacity-0 -translate-x-2": !peeked() || layout.sidebar.opened(), + "transition-[opacity,transform] motion-reduce:transition-none": true, + "duration-180 ease-out": peeked() && !layout.sidebar.opened(), + "duration-120 ease-in": !peeked() || layout.sidebar.opened(), + }} + style={{ left: `calc(4rem + ${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px)` }} + > + <div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} /> + </div> </div> <Toast.Region /> </div> |
