feat(gui): morph the sidebar collapse instead of swapping renders
The collapse read as a hard cut: only the track width animated while the panel content swapped instantly and the rail icons bore no relation to the expanded layout. Now the four control rows persist across the transition — collapse toggle, new session, new workspace, search, the same top-down order as their expanded rows — and morph their geometry (row heights, paddings, margins, capsule borders) on the deepsuite curve, so each rail icon is its expanded control converging onto the 56px axis. Wide-only content (brand, labels, input, session tree) cross-fades over 200ms, stays mounted while the collapse animates, and unmounts at the 300ms settle, still dropping the sessions subscription. The search query moves up to the root and survives the round trip; rail search focuses the surviving input after expand instead of remount-autofocus.
This commit is contained in:
@@ -158,34 +158,69 @@ describe('SidebarRoot', () => {
|
||||
expect(onCreate).toHaveBeenLastCalledWith('/proj')
|
||||
})
|
||||
|
||||
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('collapse fades the wide content out, then the rail keeps the four controls', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { onToggleSidebar, onCreate } = mount(...projectData())
|
||||
act(() => { fireEvent.click(screen.getByLabelText('Collapse sidebar')) })
|
||||
expect(onToggleSidebar).toHaveBeenCalledOnce()
|
||||
// Fade window: the wide chrome is still mounted while it fades.
|
||||
expect(screen.getByText('HARNESS')).toBeTruthy()
|
||||
expect(screen.getByRole('tree')).toBeTruthy()
|
||||
// Settle: wide content unmounts, the rail controls remain.
|
||||
act(() => { vi.advanceTimersByTime(300) })
|
||||
expect(screen.queryByText('HARNESS')).toBeNull()
|
||||
expect(screen.queryByText('New Session')).toBeNull()
|
||||
expect(screen.queryByRole('tree')).toBeNull()
|
||||
// Rail order mirrors the expanded rows: expand, new session, new workspace, search.
|
||||
const rail = ['Expand sidebar', 'New session', 'New workspace', 'Search sessions', 'Settings']
|
||||
.map((label) => screen.getByLabelText(label))
|
||||
for (let i = 1; i < rail.length; i++) {
|
||||
expect(rail[i - 1]!.compareDocumentPosition(rail[i]!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
}
|
||||
// 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()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { onToggleSidebar } = mount(...projectData())
|
||||
act(() => { fireEvent.click(screen.getByLabelText('Collapse sidebar')) })
|
||||
act(() => { vi.advanceTimersByTime(300) })
|
||||
act(() => { fireEvent.click(screen.getByLabelText('Search sessions')) })
|
||||
expect(onToggleSidebar).toHaveBeenCalledTimes(2)
|
||||
const input = screen.getByPlaceholderText('Search name, keywords...')
|
||||
expect(document.activeElement).toBe(input)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('the search query survives a collapse/expand round trip', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
mount(...projectData())
|
||||
const input = screen.getByPlaceholderText('Search name, keywords...')
|
||||
act(() => { fireEvent.change(input, { target: { value: 'forked' } }) })
|
||||
act(() => { fireEvent.click(screen.getByLabelText('Collapse sidebar')) })
|
||||
act(() => { vi.advanceTimersByTime(300) })
|
||||
act(() => { fireEvent.click(screen.getByLabelText('Expand sidebar')) })
|
||||
const restored = screen.getByPlaceholderText('Search name, keywords...') as HTMLInputElement
|
||||
expect(restored.value).toBe('forked')
|
||||
expect(screen.getByText('forked child')).toBeTruthy()
|
||||
expect(screen.queryByText('elsewhere')).toBeNull()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('group-by menu behaves', () => {
|
||||
|
||||
Reference in New Issue
Block a user