From 5da8e3b7872ca34114e71289877eb8a238a973ca Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:45:03 +0800 Subject: [PATCH] feat(gui): animate sidebar collapse and grow the rail control set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...2-collapsed-sidebar-control-rail.i18n.yaml | 4 +- ...26-07-22-collapsed-sidebar-control-rail.md | 8 +- ...07-22-collapsed-sidebar-control-rail.zh.md | 8 +- apps/web/tests/smoke-fixture.e2e.ts | 24 ++++- packages/client/ui-layout/README.md | 2 +- .../ui-layout/src/client/AppFrame.module.css | 28 +++++ .../client/ui-layout/src/client/AppFrame.tsx | 20 ++-- .../client/ui-layout/src/client/columns.ts | 4 +- packages/client/ui-sidebar/README.md | 2 +- .../src/client/SidebarRoot.module.css | 22 ++-- .../ui-sidebar/src/client/SidebarRoot.tsx | 100 ++++++++++++++---- .../ui-sidebar/tests/sidebar-root.spec.tsx | 21 +++- packages/client/ui-theme/src/styles/base.css | 9 +- 13 files changed, 186 insertions(+), 66 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.i18n.yaml index 4cb3d0e859..5a06df6e59 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-22-collapsed-sidebar-control-rail.md: 9f244010a1ec14eeafe707aedfffcf7e5bbe7736 -2026-07-22-collapsed-sidebar-control-rail.zh.md: 53007bf717404b151d0a2d1f673c20f111ee8234 +2026-07-22-collapsed-sidebar-control-rail.md: 90039110f4c1e97002fe45ebe42697452b2c6155 +2026-07-22-collapsed-sidebar-control-rail.zh.md: e5cd1c4911bbe02e10d2f1506943024bde862f12 diff --git a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.md b/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.md index 9f244010a1..90039110f4 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.md +++ b/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.md @@ -10,11 +10,11 @@ The sidebar close action persisted a zero width preference, and the layout mappe ## Decision -The layout maps a closed sidebar (persisted width `0`) to the fixed `SIDEBAR_COLLAPSED` width of 60px: one 28px icon control between the sidebar's 16px horizontal paddings. The compact rail participates in the concession solver and retains its right border, while the stored expanded width remains untouched. +The layout maps a closed sidebar (persisted width `0`) to the fixed `SIDEBAR_COLLAPSED` width of 56px: a 24px icon column between the sidebar's 16px horizontal paddings. The compact rail participates in the concession solver and retains its right border, while the stored expanded width remains untouched. -`AppFrame` marks the sidebar collapsed from the persisted width preference rather than from the resolved track width, removes the resize handle while collapsed, and passes `collapsed` to the sidebar slot as owner props from the render site. +`AppFrame` marks the sidebar collapsed from the persisted width preference rather than from the resolved track width, removes the resize handle while collapsed, and passes `collapsed` to the sidebar slot as owner props from the render site. Collapse and expand animate: the frame transitions `grid-template-columns` (and the remaining handle its `left`) on the deepsuite sider curve — `--ds-ease-in-out` over `--ds-transition-duration-slow`, both supplied by ui-theme's base sheet; transitions pause during drags and under `prefers-reduced-motion`. -`SidebarRoot` reads the owner `collapsed` prop. Its collapsed render removes the brand, creation controls, search, and session tree from the rendered and accessibility trees — the body component unmounts, dropping its sessions subscription; the top control changes to `Expand sidebar`, and the bottom `Settings` control remains in the rail. +`SidebarRoot` reads the owner `collapsed` prop. Its collapsed render is the rail: expand toggle, new session, search, and new workspace icons (each aligned with its expanded counterpart's behavior — the search icon expands the sidebar and focuses the search box) plus the `Settings` foot. The brand, capsule button, search field, and session tree leave the rendered and accessibility trees — the body component unmounts, dropping its sessions subscription. ## Alternatives considered @@ -24,6 +24,6 @@ The layout maps a closed sidebar (persisted width `0`) to the fixed `SIDEBAR_COL ## Consequences -- A collapsed sidebar reserves 60px instead of yielding the entire width to the center column. Expanding restores the persisted width and drag behavior. +- A collapsed sidebar reserves 56px instead of yielding the entire width to the center column. Expanding restores the persisted width and drag behavior. - The settings entry remains visible but retains its existing placeholder behavior; this change does not introduce an account or settings screen. - Layout solver tests pin the compact width, sidebar component tests pin the visible controls, and the keyless real-bundle web smoke test pins collapse and recovery through the assembled client. diff --git a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.zh.md b/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.zh.md index 53007bf717..e5cd1c4911 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-07-22-collapsed-sidebar-control-rail.zh.md @@ -10,11 +10,11 @@ Status: implemented ## 决策 -布局将关闭的侧边栏(持久化宽度为 `0`)映射为固定的 `SIDEBAR_COLLAPSED` 宽度 60px:在侧边栏两侧各 16px 的水平内边距之间放置一个 28px 的图标控件。紧凑控制栏参与空间收缩求解,并保留右侧边框;已存储的展开宽度保持不变。 +布局将关闭的侧边栏(持久化宽度为 `0`)映射为固定的 `SIDEBAR_COLLAPSED` 宽度 56px:在侧边栏两侧各 16px 的水平内边距之间放置一列 24px 的图标控件。紧凑控制栏参与空间收缩求解,并保留右侧边框;已存储的展开宽度保持不变。 -`AppFrame` 根据持久化的宽度偏好标记侧边栏是否折叠,而不是根据求解后的轨道宽度来判断;折叠时移除尺寸调整手柄,并在渲染点把 `collapsed` 作为 owner props 传给侧边栏插槽。 +`AppFrame` 根据持久化的宽度偏好标记侧边栏是否折叠,而不是根据求解后的轨道宽度来判断;折叠时移除尺寸调整手柄,并在渲染点把 `collapsed` 作为 owner props 传给侧边栏插槽。折叠与展开带动画:frame 对 `grid-template-columns`(以及余下手柄的 `left`)应用 deepsuite 侧栏曲线过渡——`--ds-ease-in-out` 配 `--ds-transition-duration-slow`,两个变量由 ui-theme 的 base 表提供;拖拽期间和 `prefers-reduced-motion` 下过渡暂停。 -`SidebarRoot` 读取 owner 的 `collapsed` 属性。折叠状态下的渲染会将品牌标识、创建控件、搜索框和会话树从渲染树与可访问性树中移除——主体组件卸载,随之退订会话列表;顶部控件变为 `Expand sidebar`,底部的 `Settings` 控件则留在控制栏中。 +`SidebarRoot` 读取 owner 的 `collapsed` 属性。折叠渲染即控制栏:展开开关、新建会话、搜索、新建工作区四个图标(行为与展开态对应控件对齐——搜索图标会展开侧边栏并聚焦搜索框),加上底部的 `Settings`。品牌标识、胶囊按钮、搜索框和会话树离开渲染树与可访问性树——主体组件卸载,随之退订会话列表。 ## 曾考虑的替代方案 @@ -24,6 +24,6 @@ Status: implemented ## 后果 -- 折叠的侧边栏占用 60px,而不是把全部宽度让给中心列。展开时恢复持久化宽度与拖动行为。 +- 折叠的侧边栏占用 56px,而不是把全部宽度让给中心列。展开时恢复持久化宽度与拖动行为。 - 设置入口持续可见,但保留既有占位行为;本次改动不提供账户或设置页面。 - 布局求解器测试固定紧凑宽度,侧边栏组件测试固定可见控件,基于真实构建产物的无密钥 Web 冒烟测试则通过组装后的客户端固定折叠与恢复行为。 diff --git a/apps/web/tests/smoke-fixture.e2e.ts b/apps/web/tests/smoke-fixture.e2e.ts index 136a12599d..1d4fd671bb 100644 --- a/apps/web/tests/smoke-fixture.e2e.ts +++ b/apps/web/tests/smoke-fixture.e2e.ts @@ -136,18 +136,32 @@ describe('web boot chain success pass (keyless, six real bundles, ?fixture)', () expect(owners).toContain('@deepseek-ai/dsh-client-ui-sidebar') }) - it('collapsed sidebar keeps a 60px rail with expand and settings controls', async () => { + it('collapsed sidebar animates to a 56px rail with the four controls', async () => { onTestFailed(() => saveFailureShot(page, 'smoke-boot-collapsed-rail')) const frame = page.locator('[class*="frame"]') const firstTrack = async (): Promise => (await frame.evaluate( el => getComputedStyle(el).gridTemplateColumns)).split(' ')[0]! + // The tracks transition on the deepsuite curve; assert the animated + // settle rather than an instant jump. + const settledTrack = async (px: string): Promise => { + await expect.poll(firstTrack, { timeout: 2000 }).toBe(px) + } await page.getByRole('button', { name: 'Collapse sidebar' }).click() - expect(await firstTrack()).toBe('60px') - await expect(page.getByRole('button', { name: 'Expand sidebar' }).isVisible()).resolves.toBe(true) - await expect(page.getByRole('button', { name: 'Settings' }).isVisible()).resolves.toBe(true) + await settledTrack('56px') + for (const name of ['Expand sidebar', 'New session', 'Search sessions', 'New workspace', 'Settings']) { + await expect(page.getByRole('button', { name }).isVisible(), name).resolves.toBe(true) + } await page.getByRole('button', { name: 'Expand sidebar' }).click() - expect(await firstTrack()).toBe('300px') + await settledTrack('300px') await expect(page.getByRole('button', { name: 'Collapse sidebar' }).isVisible()).resolves.toBe(true) + // Rail search: collapse again, the search control expands and lands in the box. + await page.getByRole('button', { name: 'Collapse sidebar' }).click() + await settledTrack('56px') + await page.getByRole('button', { name: 'Search sessions' }).click() + await settledTrack('300px') + const focused = await page.evaluate(() => + (document.activeElement as HTMLInputElement | null)?.placeholder ?? '') + expect(focused).toContain('Search') }) it('stayed clean: no page errors across the whole load chain', () => { diff --git a/packages/client/ui-layout/README.md b/packages/client/ui-layout/README.md index c334e18903..6cb5fa29a4 100644 --- a/packages/client/ui-layout/README.md +++ b/packages/client/ui-layout/README.md @@ -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` 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'. diff --git a/packages/client/ui-layout/src/client/AppFrame.module.css b/packages/client/ui-layout/src/client/AppFrame.module.css index 8be0ef0107..631bb929db 100644 --- a/packages/client/ui-layout/src/client/AppFrame.module.css +++ b/packages/client/ui-layout/src/client/AppFrame.module.css @@ -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 { diff --git a/packages/client/ui-layout/src/client/AppFrame.tsx b/packages/client/ui-layout/src/client/AppFrame.tsx index c5d279021f..e40c94454d 100644 --- a/packages/client/ui-layout/src/client/AppFrame.tsx +++ b/packages/client/ui-layout/src/client/AppFrame.tsx @@ -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(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) => { 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} >
{/* Render-site slot call with live concession output: a closed @@ -155,8 +161,8 @@ export function AppFrame({ useStore, actions, renderSlot, SessionProvider }: App )} {/* The collapsed rail is fixed-width: no resize handle while closed. */} - {panels.sidebar > 0 && } - {cols.details > 0 && } + {panels.sidebar > 0 && } + {cols.details > 0 && }
) } diff --git a/packages/client/ui-layout/src/client/columns.ts b/packages/client/ui-layout/src/client/columns.ts index 73b23eb6a1..d7a63aafa2 100644 --- a/packages/client/ui-layout/src/client/columns.ts +++ b/packages/client/ui-layout/src/client/columns.ts @@ -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. */ diff --git a/packages/client/ui-sidebar/README.md b/packages/client/ui-sidebar/README.md index 441cc7477d..b398db9d3a 100644 --- a/packages/client/ui-sidebar/README.md +++ b/packages/client/ui-sidebar/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-client-ui-sidebar -Sidebar plugin: session multi-level tree (cwd grouping + parentId nesting), search, by-workspace grouping, state dots, three creation entries. The collapsed render keeps the expand control and settings entry in the layout-owned compact rail. Contract: the [slot system standard](../../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md). +Sidebar plugin: session multi-level tree (cwd grouping + parentId nesting), search, by-workspace grouping, state dots, three creation entries. The collapsed render is the layout-owned compact rail: expand / new session / search (expands and focuses the search box) / new workspace icons plus the settings entry. Contract: the [slot system standard](../../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md). `src/client/contract/slots.ts` is the single-domain contract file: `SidebarRootInjected` (the registrant's own injected share — plain service callbacks: onOpen/onCreate/onToggleSidebar) and `SidebarRootComponentProps = PropsRuntime<'sidebar'> & SidebarRootInjected` (owner `{collapsed,width}` plus the standard `useSessions` hook, resolved off ui-layout's SlotMap declaration, never re-stated). `apply` registers SidebarRoot cast-free against that composition; the inject factory closes over the plugin's own ctx. diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.module.css b/packages/client/ui-sidebar/src/client/SidebarRoot.module.css index c8f0a08964..571450d13f 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.module.css +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.module.css @@ -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; } diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx index da418b4d98..60c94d53a5 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx @@ -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 +type SidebarBodyProps = Pick & { + /** 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 ( +
+ + + + +
+ +
+
+ ) + } + return ( -
+
- {!collapsed && ( - - {/* Wordmark svg not extracted yet (figma 88:8932) — text stands in at the same ink. */} - - deepseek - HARNESS - - )} + + {/* Wordmark svg not extracted yet (figma 88:8932) — text stands in at the same ink. */} + + deepseek + HARNESS +
- {!collapsed && ( - - )} +
- {!collapsed && } +
- {!collapsed && Settings} + Settings
) diff --git a/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx b/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx index dace97a6b3..e9367a8388 100644 --- a/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx +++ b/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx @@ -75,7 +75,7 @@ function mount(...summaries: SessionSummary[]) { ) const onToggleSidebar = vi.fn(() => { collapsed = !collapsed - utils.rerender(view(collapsed ? 60 : 300)) + utils.rerender(view(collapsed ? 56 : 300)) }) const utils = render(view(300)) return { sessions, onOpen, onCreate, onToggleSidebar, ...utils } @@ -158,21 +158,36 @@ describe('SidebarRoot', () => { expect(onCreate).toHaveBeenLastCalledWith('/proj') }) - it('collapsed rail keeps the expand and settings controls', () => { - const { onToggleSidebar } = mount(...projectData()) + it('collapsed rail keeps the four controls and settings', () => { + const { onToggleSidebar, onCreate } = mount(...projectData()) act(() => { fireEvent.click(screen.getByLabelText('Collapse sidebar')) }) expect(onToggleSidebar).toHaveBeenCalledOnce() expect(screen.getByLabelText('Expand sidebar')).toBeTruthy() + expect(screen.getByLabelText('New session')).toBeTruthy() + expect(screen.getByLabelText('Search sessions')).toBeTruthy() + expect(screen.getByLabelText('New workspace')).toBeTruthy() expect(screen.getByLabelText('Settings')).toBeTruthy() expect(screen.queryByText('HARNESS')).toBeNull() expect(screen.queryByText('New Session')).toBeNull() expect(screen.queryByRole('tree')).toBeNull() + // Rail creation entries route like their expanded counterparts. + act(() => { fireEvent.click(screen.getByLabelText('New session')) }) + expect(onCreate).toHaveBeenLastCalledWith() act(() => { fireEvent.click(screen.getByLabelText('Expand sidebar')) }) expect(onToggleSidebar).toHaveBeenCalledTimes(2) expect(screen.getByLabelText('Collapse sidebar')).toBeTruthy() expect(screen.getByText('New Session')).toBeTruthy() }) + it('rail search expands the sidebar and focuses the search box', () => { + const { onToggleSidebar } = mount(...projectData()) + act(() => { fireEvent.click(screen.getByLabelText('Collapse sidebar')) }) + act(() => { fireEvent.click(screen.getByLabelText('Search sessions')) }) + expect(onToggleSidebar).toHaveBeenCalledTimes(2) + const input = screen.getByPlaceholderText('Search name, keywords...') + expect(document.activeElement).toBe(input) + }) + it('group-by menu behaves', () => { mount(...projectData()) expect(screen.queryByText('Update')).toBeNull() diff --git a/packages/client/ui-theme/src/styles/base.css b/packages/client/ui-theme/src/styles/base.css index 7fa58107d2..2d1acde71d 100644 --- a/packages/client/ui-theme/src/styles/base.css +++ b/packages/client/ui-theme/src/styles/base.css @@ -1,10 +1,13 @@ -/* Base font-family variables referenced by the token sheets but defined +/* Base variables referenced by the token sheets and component CSS but defined * upstream (deepsuite theme/global.css) — supplied here so the composite - * --dsw-font-* variables resolve. Code stack deliberately omits a bare - * `monospace` tail (Windows CJK falls back to SimSun otherwise). */ + * --dsw-font-* variables resolve and motion rides the upstream curve. Code + * font stack deliberately omits a bare `monospace` tail (Windows CJK falls + * back to SimSun otherwise). */ :root { --dsw-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif; --ds-font-family-code: 'SF Mono', 'JetBrains Mono', 'Fira Code', Consolas, 'Liberation Mono', Menlo, Courier, 'PingFang SC', 'Microsoft YaHei'; + --ds-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --ds-transition-duration-slow: 0.3s; }