feat(gui): animate sidebar collapse and grow the rail control set
The collapsed rail becomes a 56px icon column (24px controls between 16px paddings) carrying expand, new session, search, and new workspace — each aligned with its expanded counterpart; rail search expands the sidebar and focuses the search box. Collapse/expand now animates: the frame transitions grid-template-columns (and the surviving handle its left) on the deepsuite sider curve — --ds-ease-in-out over --ds-transition-duration-slow, supplied by ui-theme's base sheet. Transitions pause during drags (data-dragging on the frame, set for the whole gesture) and under prefers-reduced-motion.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# @deepseek-ai/dsh-client-ui-layout
|
||||
|
||||
Shell plugin: three-column AppFrame (drag handles, concession chain) + ctx.layout viewing-state service (nav, panel widths, persist); defines the sidebar/conversation/details/conversation.empty slots. A closed sidebar retains a 60px control rail while details closes to zero width. Contract: api-contracts v3 §5.
|
||||
Shell plugin: three-column AppFrame (drag handles, concession chain) + ctx.layout viewing-state service (nav, panel widths, persist); defines the sidebar/conversation/details/conversation.empty slots. A closed sidebar retains a 56px control rail while details closes to zero width; collapse/expand animates the grid tracks on the deepsuite sider curve. Contract: api-contracts v3 §5.
|
||||
|
||||
Slot declarations use the composed-props entry form (`owner` share, no full `props`): the exported OwnerShare contracts are `SidebarOwnerProps` / `ConvOwnerProps` / `DetailsOwnerProps` / `EmptyOwnerProps` — registrants reference them via `OwnerOf<'sidebar' | ...>` and compose their own injected share locally. No entry declares `children` (declaring it requires the registered component to carry the slots face — reserved for future business slots): delegation authority is the component-side whitelist, i.e. AppFrame's `ScopedSlots<FrameSlotKey>` face over sidebar/conversation/details/conversation.empty. Since the root-slot rework the frame itself registers into 'root' and renders those child slots at its own render sites; the shell only renders 'root'.
|
||||
|
||||
|
||||
@@ -5,6 +5,21 @@
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--dsw-alias-bg-base);
|
||||
/* Collapse/expand animates the tracks on the deepsuite sider curve
|
||||
(--ds-ease-in-out / --ds-transition-duration-slow, ui-theme base.css). */
|
||||
transition: grid-template-columns var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
/* Dragging writes widths at pointer cadence; easing them would detach the
|
||||
column from the handle. */
|
||||
.frame[data-dragging] {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.frame {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebarCol {
|
||||
@@ -46,6 +61,19 @@
|
||||
cursor: col-resize;
|
||||
z-index: 2;
|
||||
touch-action: none;
|
||||
/* Rides the same curve as the tracks so the pill stays on the moving
|
||||
border during collapse/expand; paused while dragging (frame rule). */
|
||||
transition: left var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.frame[data-dragging] .handle {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.handle {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.handle::after {
|
||||
|
||||
@@ -35,13 +35,13 @@ function DetailsColumn(props: { children?: ReactNode }) {
|
||||
}
|
||||
|
||||
/** One drag handle: pointer capture, rAF-throttled dx reports against the drag-start origin. */
|
||||
function DragHandle(props: { left: number; onStart: () => void; onDrag: (dx: number) => void }) {
|
||||
function DragHandle(props: { left: number; onStart: () => void; onDrag: (dx: number) => void; onEnd: () => void }) {
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const origin = useRef(0)
|
||||
const latest = useRef(0)
|
||||
const frame = useRef<number | null>(null)
|
||||
const callbacks = useRef({ onStart: props.onStart, onDrag: props.onDrag })
|
||||
callbacks.current = { onStart: props.onStart, onDrag: props.onDrag }
|
||||
const callbacks = useRef({ onStart: props.onStart, onDrag: props.onDrag, onEnd: props.onEnd })
|
||||
callbacks.current = { onStart: props.onStart, onDrag: props.onDrag, onEnd: props.onEnd }
|
||||
|
||||
const onPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
@@ -65,6 +65,7 @@ function DragHandle(props: { left: number; onStart: () => void; onDrag: (dx: num
|
||||
if (frame.current !== null) { cancelAnimationFrame(frame.current); frame.current = null }
|
||||
callbacks.current.onDrag(latest.current - origin.current)
|
||||
setDragging(false)
|
||||
callbacks.current.onEnd()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -114,8 +115,12 @@ export function AppFrame({ useStore, actions, renderSlot, SessionProvider }: App
|
||||
// it stays frozen for the whole gesture so dx deltas do not compound.
|
||||
const sidebarBase = useRef(0)
|
||||
const detailsBase = useRef(0)
|
||||
const onSidebarStart = useCallback(() => { sidebarBase.current = colsRef.current.sidebar }, [])
|
||||
const onDetailsStart = useCallback(() => { detailsBase.current = colsRef.current.details }, [])
|
||||
// Track-level transitions pause for the whole gesture: eased tracks would
|
||||
// detach the column edge from the pointer (AppFrame.module.css).
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const onDragEnd = useCallback(() => { setDragging(false) }, [])
|
||||
const onSidebarStart = useCallback(() => { sidebarBase.current = colsRef.current.sidebar; setDragging(true) }, [])
|
||||
const onDetailsStart = useCallback(() => { detailsBase.current = colsRef.current.details; setDragging(true) }, [])
|
||||
const onSidebarDrag = useCallback((dx: number) => {
|
||||
actions.setSidebar(sidebarBase.current + dx)
|
||||
}, [actions])
|
||||
@@ -130,6 +135,7 @@ export function AppFrame({ useStore, actions, renderSlot, SessionProvider }: App
|
||||
style={{ gridTemplateColumns: `${cols.sidebar}px minmax(0, 1fr) ${cols.details}px` }}
|
||||
data-sidebar-collapsed={panels.sidebar === 0 || undefined}
|
||||
data-details-collapsed={cols.details === 0 || undefined}
|
||||
data-dragging={dragging || undefined}
|
||||
>
|
||||
<div className={css.sidebarCol}>
|
||||
{/* Render-site slot call with live concession output: a closed
|
||||
@@ -155,8 +161,8 @@ export function AppFrame({ useStore, actions, renderSlot, SessionProvider }: App
|
||||
)}
|
||||
</SessionProvider>
|
||||
{/* The collapsed rail is fixed-width: no resize handle while closed. */}
|
||||
{panels.sidebar > 0 && <DragHandle left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} />}
|
||||
{cols.details > 0 && <DragHandle left={viewport - cols.details} onStart={onDetailsStart} onDrag={onDetailsDrag} />}
|
||||
{panels.sidebar > 0 && <DragHandle left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} onEnd={onDragEnd} />}
|
||||
{cols.details > 0 && <DragHandle left={viewport - cols.details} onStart={onDetailsStart} onDrag={onDetailsDrag} onEnd={onDragEnd} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ export const SIDEBAR_MIN = 240
|
||||
export const SIDEBAR_MAX = 420
|
||||
/** Sidebar width before any user drag. */
|
||||
export const SIDEBAR_DEFAULT = 300
|
||||
/** Closed-sidebar rail: one 28px control between 16px horizontal paddings. */
|
||||
export const SIDEBAR_COLLAPSED = 60
|
||||
/** Closed-sidebar rail: a 24px icon column between 16px horizontal paddings. */
|
||||
export const SIDEBAR_COLLAPSED = 56
|
||||
/** Details drag clamp floor. */
|
||||
export const DETAILS_MIN = 300
|
||||
/** Details drag clamp ceiling. */
|
||||
|
||||
Reference in New Issue
Block a user