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; + } +}