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:
@@ -15,24 +15,24 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Closed state is a persistent rail: the layout reserves exactly the root's
|
||||
horizontal padding plus one icon control. */
|
||||
/* Closed state is a persistent rail: a 24px icon column between the 16px
|
||||
horizontal paddings (SIDEBAR_COLLAPSED = 56). Controls mirror their
|
||||
expanded counterparts top-down: expand, new session, search, new
|
||||
workspace; settings keeps the foot. */
|
||||
.root.collapsed {
|
||||
gap: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 16px 6px;
|
||||
}
|
||||
|
||||
.collapsed .headerBlock {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.collapsed .logoRow {
|
||||
justify-content: center;
|
||||
padding-inline: 0;
|
||||
.collapsed .iconButton {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.collapsed .foot {
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
width: 24px;
|
||||
margin-top: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
* standard useSessions hook, viewing state (expansion, search) is local
|
||||
* component state, and rows are derived in render via useMemo (slot design
|
||||
* section 6: derived data is a pure function, no materializing store).
|
||||
* The collapsed render keeps only the rail controls (expand toggle +
|
||||
* Settings); the body unmounts, dropping its sessions subscription.
|
||||
* The collapsed render is the compact rail: expand / new session / search /
|
||||
* new workspace icons plus the Settings foot; the body unmounts, dropping
|
||||
* its sessions subscription. Rail search expands and focuses the search box.
|
||||
*/
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
FishLogo,
|
||||
@@ -33,10 +34,13 @@ function toggled(list: readonly string[], key: string): string[] {
|
||||
return list.includes(key) ? list.filter((k) => k !== key) : [...list, key]
|
||||
}
|
||||
|
||||
type SidebarBodyProps = Pick<SidebarRootComponentProps, 'useSessions' | 'onOpen' | 'onCreate'>
|
||||
type SidebarBodyProps = Pick<SidebarRootComponentProps, 'useSessions' | 'onOpen' | 'onCreate'> & {
|
||||
/** Focus the search input on mount (rail search control expands into search). */
|
||||
autoFocusSearch: boolean
|
||||
}
|
||||
|
||||
/** Expanded-only content; unmounting drops the sessions subscription and viewing state while the rail is collapsed. */
|
||||
function SidebarBody({ useSessions, onOpen, onCreate }: SidebarBodyProps) {
|
||||
function SidebarBody({ useSessions, onOpen, onCreate, autoFocusSearch }: SidebarBodyProps) {
|
||||
const list = useSessions((s) => s)
|
||||
// Wave-2 seam: row highlight expects `current` on the sessions list
|
||||
// snapshot (sessions.current lives with the runtime sessions service).
|
||||
@@ -99,6 +103,7 @@ function SidebarBody({ useSessions, onOpen, onCreate }: SidebarBodyProps) {
|
||||
type="text"
|
||||
placeholder="Search name, keywords..."
|
||||
value={query}
|
||||
autoFocus={autoFocusSearch}
|
||||
onChange={(e) => { setQuery(e.target.value) }}
|
||||
/>
|
||||
{query !== '' && (
|
||||
@@ -152,41 +157,90 @@ function SidebarBody({ useSessions, onOpen, onCreate }: SidebarBodyProps) {
|
||||
* @returns the sidebar element tree.
|
||||
*/
|
||||
export function SidebarRoot({ collapsed, useSessions, onOpen, onCreate, onToggleSidebar }: SidebarRootComponentProps) {
|
||||
// Rail search = expand + land in the search box: the flag arms right before
|
||||
// the expand toggle, the remounting SidebarBody autofocuses its input, and
|
||||
// the post-commit effect disarms so later remounts stay unfocused.
|
||||
const [searchOnExpand, setSearchOnExpand] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!collapsed && searchOnExpand) setSearchOnExpand(false)
|
||||
}, [collapsed, searchOnExpand])
|
||||
|
||||
if (collapsed) {
|
||||
// Rail (figma parity with deepsuite CollapsedSider): the four controls
|
||||
// mirror their expanded counterparts top-down; actions that need the
|
||||
// expanded surface expand first.
|
||||
return (
|
||||
<div className={clsx(css.root, css.collapsed)}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label="Expand sidebar"
|
||||
onClick={() => { onToggleSidebar() }}
|
||||
>
|
||||
<IconPanelLeftOutline16 />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label="New session"
|
||||
onClick={() => { onCreate() }}
|
||||
>
|
||||
<IconNewChatOutline16 />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label="Search sessions"
|
||||
onClick={() => { setSearchOnExpand(true); onToggleSidebar() }}
|
||||
>
|
||||
<IconSearchOutline16 />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label="New workspace"
|
||||
onClick={() => { onCreate() }}
|
||||
>
|
||||
<IconProjectAddOutline16 />
|
||||
</button>
|
||||
<div className={css.foot} role="button" tabIndex={0} aria-label="Settings">
|
||||
<IconSettingsOutline14 />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(css.root, collapsed && css.collapsed)}>
|
||||
<div className={css.root}>
|
||||
<div className={css.headerBlock}>
|
||||
<div className={css.logoRow}>
|
||||
{!collapsed && (
|
||||
<span className={css.brand}>
|
||||
{/* Wordmark svg not extracted yet (figma 88:8932) — text stands in at the same ink. */}
|
||||
<FishLogo size={23} />
|
||||
<span className={css.wordmark}>deepseek</span>
|
||||
<span className={css.badge}>HARNESS</span>
|
||||
</span>
|
||||
)}
|
||||
<span className={css.brand}>
|
||||
{/* Wordmark svg not extracted yet (figma 88:8932) — text stands in at the same ink. */}
|
||||
<FishLogo size={23} />
|
||||
<span className={css.wordmark}>deepseek</span>
|
||||
<span className={css.badge}>HARNESS</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
aria-label="Collapse sidebar"
|
||||
onClick={() => { onToggleSidebar() }}
|
||||
>
|
||||
<IconPanelLeftOutline16 />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!collapsed && (
|
||||
<button type="button" className={css.newSession} onClick={() => { onCreate() }}>
|
||||
<IconNewChatOutline16 size={14} />
|
||||
New Session
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className={css.newSession} onClick={() => { onCreate() }}>
|
||||
<IconNewChatOutline16 size={14} />
|
||||
New Session
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!collapsed && <SidebarBody useSessions={useSessions} onOpen={onOpen} onCreate={onCreate} />}
|
||||
<SidebarBody useSessions={useSessions} onOpen={onOpen} onCreate={onCreate} autoFocusSearch={searchOnExpand} />
|
||||
|
||||
<div className={css.foot} role="button" tabIndex={0} aria-label="Settings">
|
||||
<IconSettingsOutline14 />
|
||||
{!collapsed && <span>Settings</span>}
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user