From 302bd85e62edca9021332d39834766d6a7fa325a Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Sat, 8 Aug 2026 14:51:05 +0800 Subject: [PATCH] sidebar: squeeze the conversation layout instead of floating over it The open panel now occupies layout space: #root (the AppFrame grid) gives up the panel width on the right via margin-right driven by a --dsh-sidebar-width CSS variable, and only the center column is flexible, so the squeeze lands exactly on the agent output and the input bar (VSCode sidebar behavior). The fixed panel covers the vacated strip; expand/ collapse animates the margin and the slide on the same theme duration, width drags disable the transition, and prefers-reduced-motion is honored. --- src/client/Sidebar.tsx | 13 +++++++++++++ src/client/index.tsx | 1 + src/client/layout.css | 28 ++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/client/layout.css diff --git a/src/client/Sidebar.tsx b/src/client/Sidebar.tsx index ba5cc78..fd0d31c 100644 --- a/src/client/Sidebar.tsx +++ b/src/client/Sidebar.tsx @@ -101,6 +101,19 @@ export function Sidebar(props: { ctx: Context }) { const widthDrag = useRef({ startX: 0, startWidth: 0 }) const [draggingWidth, setDraggingWidth] = useState(false) + // Layout push: the app shell gives up the panel's width while it is open + // (0 while collapsed), so the conversation and input bar are squeezed + // instead of covered. Dragging disables the layout transition. + useEffect(() => { + const width = snapshot.state?.panelOpen === true ? snapshot.state.width : 0 + document.documentElement.style.setProperty('--dsh-sidebar-width', `${width}px`) + }, [snapshot.state?.panelOpen, snapshot.state?.width]) + useEffect(() => { + if (draggingWidth) document.body.setAttribute('data-dsh-sidebar-dragging', '') + else document.body.removeAttribute('data-dsh-sidebar-dragging') + }, [draggingWidth]) + + // Whether the panel currently fills the viewport (fullscreen expansion). const fullscreen = state !== undefined && window.innerWidth - state.width < 8 diff --git a/src/client/index.tsx b/src/client/index.tsx index 8259880..54b52c4 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -12,6 +12,7 @@ import { Sidebar } from './Sidebar.tsx' import { registerTurnTailInterception } from './intercept.tsx' import { t } from './locales.ts' import css from './sidebar.module.css' +import './layout.css' /** Services required before mounting (provided by the client runtime). */ export const inject = ['slots', 'sessions'] diff --git a/src/client/layout.css b/src/client/layout.css new file mode 100644 index 0000000..be80a10 --- /dev/null +++ b/src/client/layout.css @@ -0,0 +1,28 @@ +/** + * Layout push: when the sidebar is open it OCCUPIES the layout instead of + * floating over it — the app shell (#root, the AppFrame three-column grid) + * gives up the panel's width on the right. Only the center column is + * flexible (1fr), so the squeeze lands exactly on the conversation output + * and the input bar, like a VSCode sidebar. The fixed panel covers the + * vacated strip, which looks seamless because #root's background is the + * theme base. + * + * The width rides a CSS variable updated by the Sidebar shell (0 while + * collapsed); expand/collapse animates both the margin and the panel slide + * on the same theme duration. Width drags disable the transition so the + * layout tracks the pointer. + */ +#root { + margin-right: var(--dsh-sidebar-width, 0px); + transition: margin-right var(--ds-transition-duration-slow) var(--ds-ease-in-out); +} + +body[data-dsh-sidebar-dragging] #root { + transition: none; +} + +@media (prefers-reduced-motion: reduce) { + #root { + transition: none; + } +}