Skip to content

Commit 7449a01

Browse files
fix(frontend): move sidebar toggle beside filters (#1312)
Moves the desktop sidebar control out of the global header and into the session filter controls it affects. With the sidebar open, the filter is followed by the collapse action; when closed, the expand action precedes the relocated filter on both the analytics landing page and session detail breadcrumb. The mobile hamburger and drawer behavior remain unchanged. A shared icon button provides state-specific accessible labels, with the new open-sidebar copy kept in sync across every locale. <sup>generated by a clanker</sup> Co-authored-by: Marius van Niekerk <mariusvniekerk@users.noreply.github.com>
1 parent 5707542 commit 7449a01

23 files changed

Lines changed: 707 additions & 29 deletions

cmd/agentsview/pricing_schedule_test.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import (
1616
agentsync "go.kenn.io/agentsview/internal/sync"
1717
)
1818

19+
// Resync may spend up to five seconds draining SQLite connections before a
20+
// swap, particularly on Windows where open handles prevent the rename.
21+
const pricingResyncTestTimeout = 10 * time.Second
22+
1923
type pricingCatalogTransport struct {
2024
requests chan *http.Request
2125
}
@@ -122,7 +126,7 @@ func TestStartPeriodicPricingRefreshWaitsForResyncSwap(t *testing.T) {
122126
default:
123127
return false
124128
}
125-
}, time.Second, time.Millisecond)
129+
}, pricingResyncTestTimeout, time.Millisecond)
126130

127131
requests := make(chan *http.Request, 1)
128132
originalTransport := http.DefaultTransport
@@ -146,7 +150,7 @@ func TestStartPeriodicPricingRefreshWaitsForResyncSwap(t *testing.T) {
146150
default:
147151
return false
148152
}
149-
}, time.Second, time.Millisecond)
153+
}, pricingResyncTestTimeout, time.Millisecond)
150154
})
151155

152156
assert.Never(t, func() bool {
@@ -162,12 +166,12 @@ func TestStartPeriodicPricingRefreshWaitsForResyncSwap(t *testing.T) {
162166
default:
163167
return false
164168
}
165-
}, time.Second, time.Millisecond)
169+
}, pricingResyncTestTimeout, time.Millisecond)
166170
require.NoError(t, swapErr)
167171
require.Eventually(t, func() bool {
168172
price, err := database.GetModelPricing("scheduled-model")
169173
return err == nil && price != nil
170-
}, time.Second, time.Millisecond)
174+
}, pricingResyncTestTimeout, time.Millisecond)
171175
}
172176

173177
func TestSeedPricingWaitsForResyncSwap(t *testing.T) {
@@ -208,7 +212,7 @@ func TestSeedPricingWaitsForResyncSwap(t *testing.T) {
208212
default:
209213
return false
210214
}
211-
}, time.Second, time.Millisecond)
215+
}, pricingResyncTestTimeout, time.Millisecond)
212216

213217
seedDone := make(chan struct{})
214218
go func() {
@@ -233,7 +237,7 @@ func TestSeedPricingWaitsForResyncSwap(t *testing.T) {
233237
default:
234238
return false
235239
}
236-
}, time.Second, time.Millisecond)
240+
}, pricingResyncTestTimeout, time.Millisecond)
237241
require.NoError(t, swapErr)
238242
require.Eventually(t, func() bool {
239243
select {
@@ -242,7 +246,7 @@ func TestSeedPricingWaitsForResyncSwap(t *testing.T) {
242246
default:
243247
return false
244248
}
245-
}, time.Second, time.Millisecond)
249+
}, pricingResyncTestTimeout, time.Millisecond)
246250
price, err = database.GetModelPricing("gpt-5.5")
247251
require.NoError(t, err)
248252
require.NotNil(t, price)
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Sidebar Toggle Placement Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans
4+
> to implement this plan directly in the current agent, task-by-task. Never use
5+
> subagent-driven development. Steps use checkbox (`- [ ]`) syntax for tracking.
6+
7+
**Goal:** Move the desktop sidebar toggle next to the session filter while
8+
preserving the existing mobile hamburger behavior.
9+
10+
**Approved spec/design:** This plan is based directly on the user's approved
11+
requirements: with the desktop sidebar open, render the collapse control to the
12+
right of the filter; with it closed, render the expand control to the left of
13+
the relocated filter; keep the current mobile hamburger and drawer behavior.
14+
15+
**Architecture:** Add one shared state-aware sidebar toggle built from kit-ui's
16+
`IconButton`, then render it at the two session-list boundaries that own the
17+
filter. The global header keeps the hamburger only on mobile. Session detail
18+
receives the same closed-state toggle/filter pair in its breadcrumb. Focus moves
19+
to the newly visible counterpart after a keyboard-initiated toggle, and each
20+
control exposes the sidebar relationship through `aria-expanded` and
21+
`aria-controls`, so closing the sidebar never strands the user.
22+
23+
**Tech Stack:** Svelte 5, TypeScript, kit-ui, Lucide icons, Paraglide JS, Vitest
24+
through Vite+
25+
26+
## Global Constraints
27+
28+
- Do not change the existing mobile hamburger route and drawer behavior, and do
29+
not render the relocated controls on mobile.
30+
- Use shared controls rather than adding one-off button chrome.
31+
- Keep every locale catalog's message keys identical.
32+
- Tests must assert rendered order and click behavior, not source text.
33+
34+
______________________________________________________________________
35+
36+
### Task 1: Protect the responsive placement contract
37+
38+
**Files:**
39+
40+
- Modify: `frontend/src/lib/components/layout/AppHeader.test.ts`
41+
- Modify: `frontend/src/lib/components/sidebar/SessionList.test.ts`
42+
- Modify: `frontend/src/lib/components/analytics/AnalyticsPage.test.ts`
43+
- Modify: `frontend/src/lib/components/layout/SessionBreadcrumb.test.ts`
44+
- Create: `frontend/src/lib/components/layout/SidebarToggleButton.test.ts`
45+
46+
**Interfaces:**
47+
48+
- Consumes: current `ui.sidebarOpen`, `ui.isMobileViewport`, and rendered filter
49+
controls.
50+
51+
- Produces: regression coverage for desktop header removal, both mobile
52+
hamburger branches, open-state filter/collapse order, closed-state
53+
expand/filter order, and keyboard focus handoff in both directions.
54+
55+
- [ ] **Step 1: Install the pinned frontend dependencies**
56+
57+
Run:
58+
59+
```bash
60+
cd frontend && vp install -- --allow-git=root
61+
```
62+
63+
Expected: dependencies install successfully without changing the pinned
64+
dependency set.
65+
66+
- [ ] **Step 2: Write failing rendered-behavior tests**
67+
68+
Add tests that query the localized `aria-label` values, compare sibling order
69+
around `.filter-btn`, click the toggle, and assert the observable
70+
`ui.sidebarOpen` state change. Focused-button tests must assert that focus moves
71+
to the newly visible counterpart after both collapse and reopen. Mobile
72+
AppHeader tests must cover closing the drawer on the sessions route and
73+
navigating from another route to open it. Mobile SessionList, Analytics, and
74+
Breadcrumb tests must prove the relocated controls are absent. Each desktop
75+
relocated control must retain the existing localized `Toggle sidebar (b)` title
76+
so the keyboard shortcut remains discoverable.
77+
78+
- [ ] **Step 3: Run the focused tests and verify RED**
79+
80+
Run:
81+
82+
```bash
83+
cd frontend && PATH="$(pwd)/node_modules/.bin:$PATH" vp test src/lib/components/layout/AppHeader.test.ts src/lib/components/layout/SidebarToggleButton.test.ts src/lib/components/sidebar/SessionList.test.ts src/lib/components/analytics/AnalyticsPage.test.ts src/lib/components/layout/SessionBreadcrumb.test.ts
84+
```
85+
86+
Expected: assertion failures because the desktop hamburger is still global and
87+
the filter-adjacent controls do not exist.
88+
89+
### Task 2: Add the shared sidebar toggle and place it around the filter
90+
91+
**Files:**
92+
93+
- Create: `frontend/src/lib/components/layout/SidebarToggleButton.svelte`
94+
- Modify: `frontend/src/lib/icons.ts`
95+
- Modify: `frontend/src/lib/icons.test.ts`
96+
- Modify: `frontend/src/lib/components/layout/AppHeader.svelte`
97+
- Modify: `frontend/src/lib/components/layout/ThreeColumnLayout.svelte`
98+
- Modify: `frontend/src/lib/components/sidebar/SessionList.svelte`
99+
- Modify: `frontend/src/lib/components/analytics/AnalyticsPage.svelte`
100+
- Modify: `frontend/src/lib/components/layout/SessionBreadcrumb.svelte`
101+
- Modify: `frontend/messages/en.json`
102+
- Modify: `frontend/messages/zh-CN.json`
103+
- Modify: `frontend/messages/zh-TW.json`
104+
- Modify: `frontend/messages/ko.json`
105+
- Modify: `frontend/messages/fr.json`
106+
107+
**Interfaces:**
108+
109+
- Consumes: `ui.sidebarOpen`, `ui.toggleSidebar()`, `m.nav_open_sidebar()`, and
110+
`m.nav_close_sidebar()`.
111+
112+
- Produces: `SidebarToggleButton`, a shared icon-only button that renders the
113+
correct panel-open/panel-close icon and accessible state-specific label.
114+
115+
- [ ] **Step 1: Add localized open-sidebar copy**
116+
117+
Add `nav_open_sidebar` to every locale next to `nav_close_sidebar`, with an
118+
accurate translation in each catalog.
119+
120+
- [ ] **Step 2: Implement the shared control**
121+
122+
Create `SidebarToggleButton.svelte` with kit-ui `IconButton`. It calls
123+
`ui.toggleSidebar()`, labels itself from the current state, retains
124+
`m.nav_toggle_sidebar_shortcut()` as its title, and uses `PanelLeftCloseIcon`
125+
while open and `PanelLeftOpenIcon` while closed. Export both icons through the
126+
app icon facade and add them to its allowlist. Identify whether each toggle is
127+
in the sidebar or content region, expose `aria-expanded` and `aria-controls`,
128+
and move keyboard focus to the opposite region's toggle after the state change.
129+
Give the layout sidebar the stable ID referenced by the controls.
130+
131+
- [ ] **Step 3: Relocate the desktop control**
132+
133+
Render the AppHeader hamburger only when `ui.isMobileViewport`. On desktop only,
134+
place `SidebarToggleButton` immediately after `SessionFilterControl` in
135+
`SessionList`. On desktop with the sidebar collapsed, place
136+
`SidebarToggleButton` immediately before `SessionFilterControl` in the Analytics
137+
toolbar and SessionBreadcrumb. Give the breadcrumb pair a positioned flex
138+
wrapper, and configure its filter with `showDisplay={false}`,
139+
`showStarred={false}`, and `align="left"` so its dropdown has a valid anchor and
140+
exposes only working filters.
141+
142+
- [ ] **Step 4: Run the focused tests and verify GREEN**
143+
144+
Run the same focused test command from Task 1. Expected: all selected tests
145+
pass.
146+
147+
### Task 3: Verify the frontend, commit, push, and open the pull request
148+
149+
**Files:**
150+
151+
- Verify all files listed above.
152+
153+
**Interfaces:**
154+
155+
- Consumes: the completed responsive placement implementation.
156+
157+
- Produces: compiled locale output, repository-required frontend validation, one
158+
focused commit, a pushed feature branch, and an open pull request.
159+
160+
- [ ] **Step 1: Compile localization and run frontend checks**
161+
162+
```bash
163+
cd frontend && npm run i18n:compile && PATH="$(pwd)/node_modules/.bin:$PATH" vp check && PATH="$(pwd)/node_modules/.bin:$PATH" vp test && PATH="$(pwd)/node_modules/.bin:$PATH" vp run check:kit-ui
164+
```
165+
166+
Expected: locale compilation, formatting, linting, type checking, the full
167+
frontend suite (including locale key parity), and the kit-ui contract check all
168+
exit successfully.
169+
170+
- [ ] **Step 2: Re-run the focused regression tests**
171+
172+
```bash
173+
cd frontend && PATH="$(pwd)/node_modules/.bin:$PATH" vp test src/lib/components/layout/AppHeader.test.ts src/lib/components/sidebar/SessionList.test.ts src/lib/components/analytics/AnalyticsPage.test.ts src/lib/components/layout/SessionBreadcrumb.test.ts
174+
```
175+
176+
Expected: all selected tests pass with no failures.
177+
178+
- [ ] **Step 3: Check the responsive layout visually**
179+
180+
At the minimum desktop width, verify the open and collapsed control groups in
181+
English and a locale with longer labels. Confirm that the sidebar header and
182+
analytics toolbar do not overlap, clip, or wrap unexpectedly.
183+
184+
- [ ] **Step 4: Review the diff and commit**
185+
186+
Stage only the plan, component, catalog, and test changes, then create a focused
187+
conventional commit explaining why the toggle belongs with the panel controls
188+
and why mobile remains unchanged.
189+
190+
- [ ] **Step 5: Push and open the pull request**
191+
192+
The user already authorized the commit-push-PR workflow. Push the current
193+
feature branch to `origin`, run the private-data scrub on the proposed
194+
title/body, then create a pull request whose description summarizes the behavior
195+
and rationale without a test-plan section or checklist.

frontend/messages/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"app_undo_undo": "Undo",
99
"nav_toggle_sidebar": "Toggle sidebar",
1010
"nav_toggle_sidebar_shortcut": "Toggle sidebar (b)",
11+
"nav_open_sidebar": "Open sidebar",
1112
"nav_close_sidebar": "Close sidebar",
1213
"nav_resize_sidebar": "Resize sidebar",
1314
"nav_home": "Home",

frontend/messages/fr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"app_undo_undo": "Annuler",
99
"nav_toggle_sidebar": "Afficher/masquer la barre latérale",
1010
"nav_toggle_sidebar_shortcut": "Afficher/masquer la barre latérale (b)",
11+
"nav_open_sidebar": "Ouvrir la barre latérale",
1112
"nav_close_sidebar": "Fermer la barre latérale",
1213
"nav_resize_sidebar": "Redimensionner la barre latérale",
1314
"nav_home": "Accueil",

frontend/messages/ko.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"app_undo_undo": "실행 취소",
99
"nav_toggle_sidebar": "사이드바 전환",
1010
"nav_toggle_sidebar_shortcut": "사이드바 전환 (b)",
11+
"nav_open_sidebar": "사이드바 열기",
1112
"nav_close_sidebar": "사이드바 닫기",
1213
"nav_resize_sidebar": "사이드바 크기 조정",
1314
"nav_home": "",

frontend/messages/zh-CN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"app_undo_undo": "撤销",
99
"nav_toggle_sidebar": "切换侧边栏",
1010
"nav_toggle_sidebar_shortcut": "切换侧边栏 (b)",
11+
"nav_open_sidebar": "打开侧边栏",
1112
"nav_close_sidebar": "关闭侧边栏",
1213
"nav_resize_sidebar": "调整侧边栏大小",
1314
"nav_home": "首页",

frontend/messages/zh-TW.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"app_undo_undo": "復原",
99
"nav_toggle_sidebar": "切換側邊欄",
1010
"nav_toggle_sidebar_shortcut": "切換側邊欄 (b)",
11+
"nav_open_sidebar": "開啟側邊欄",
1112
"nav_close_sidebar": "關閉側邊欄",
1213
"nav_resize_sidebar": "調整側邊欄大小",
1314
"nav_home": "首頁",

frontend/src/lib/components/analytics/AnalyticsPage.svelte

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import TopSessions from "./TopSessions.svelte";
2323
import ActiveFilters from "./ActiveFilters.svelte";
2424
import SessionFilterControl from "../filters/SessionFilterControl.svelte";
25+
import SidebarToggleButton from "../layout/SidebarToggleButton.svelte";
2526
import FilterDropdown from "../usage/FilterDropdown.svelte";
2627
import { analytics } from "../../stores/analytics.svelte.js";
2728
import { analyticsPageDates } from "../../stores/analyticsPageDates.js";
@@ -508,8 +509,12 @@
508509

509510
<div class="analytics-page">
510511
<div class="analytics-toolbar">
511-
{#if !ui.sidebarOpen}
512-
<div class="toolbar-filter-anchor">
512+
{#if !ui.isMobileViewport && !ui.sidebarOpen}
513+
<div
514+
class="toolbar-filter-anchor"
515+
data-sidebar-focus-region="content"
516+
>
517+
<SidebarToggleButton placement="content" />
513518
<SessionFilterControl
514519
showDisplay={false}
515520
showStarred={false}

0 commit comments

Comments
 (0)