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:
imccyu
2026-07-23 11:45:54 +08:00
parent a6649fb545
commit 5da8e3b787
13 files changed
+186 -66

No files matched your search

@@ -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
@@ -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.
@@ -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 冒烟测试则通过组装后的客户端固定折叠与恢复行为。
+19 -5
View File
@@ -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<string> => (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<void> => {
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', () => {
+1 -1
View File
@@ -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. */
+1 -1
View File
@@ -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.
@@ -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>
)
@@ -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()
+6 -3
View File
@@ -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;
}