Skip to content

Commit a349413

Browse files
0xjjjjjjclaude
andauthored
feat: mobile responsive layout (#211)
## Summary - **Collapsible sidebar**: sidebar collapses to a fixed overlay on viewports below 768px, toggled via hamburger button or `b` keyboard shortcut. On desktop, pressing `b` hides the sidebar and shows the hamburger to reopen it. - **Responsive header**: nav button labels hidden below 1024px (icons only), nav buttons and project typeahead hidden below 768px, export/publish buttons hidden on mobile. - **Touch targets**: header buttons enlarged to 44px minimum on `pointer: coarse` devices per Apple HIG. - **Content overflow**: code blocks constrained to viewport width on mobile, breadcrumb metadata (time, tokens, session ID) and status bar stats hidden on narrow screens. - **Reactive viewport tracking**: `matchMedia` listener in UIStore tracks the 768px breakpoint, auto-closing sidebar on mobile and auto-opening on desktop resize. Includes proper cleanup. - **Accessibility**: sidebar backdrop uses a `<button>` with `aria-label` instead of a plain div. No new dependencies. No changes to the Go backend. Desktop layout (>= 1024px) is unchanged. ## Test plan - [x] `cd frontend && npx vitest run` -- 740/740 tests pass (10 new tests added) - [x] `cd frontend && npx vite build` -- builds cleanly - [x] `go vet -tags fts5 ./...` -- no issues - [x] Browser devtools responsive mode at 375px (phone), 768px (tablet), 1024px, 1440px (desktop) - [x] Press `b` to toggle sidebar at all widths; hamburger appears when sidebar is closed - [x] Resize browser across 768px boundary without reload (simulates window manager retiling) - [x] Select a session on mobile; sidebar auto-closes - [x] All existing keyboard shortcuts (j/k/o/l/r/e/p/s/c/?) still work - [x] No visual changes at >= 1024px (regression check) --------- Co-authored-by: 0xjjjjjj <0xjjjjjj@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f3fb05e commit a349413

File tree

11 files changed

+430
-9
lines changed

11 files changed

+430
-9
lines changed

frontend/src/App.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@
7272
ui.pendingScrollSession = null;
7373
}
7474
if (id) {
75+
if (ui.isMobileViewport) {
76+
ui.closeSidebar();
77+
}
7578
messages.loadSession(id);
7679
sessions.loadChildSessions(id);
7780
sync.watchSession(id, () => {

frontend/src/app.css

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
"Fira Mono", Menlo, Consolas, monospace;
5050
--viewport-indicator: rgba(0, 0, 0, 0.08);
5151
--overlay-bg: rgba(0, 0, 0, 0.3);
52+
--header-height: 40px;
53+
--status-bar-height: 24px;
5254
color-scheme: light;
5355
}
5456

@@ -87,6 +89,8 @@
8789
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.4);
8890
--viewport-indicator: rgba(255, 255, 255, 0.08);
8991
--overlay-bg: rgba(0, 0, 0, 0.6);
92+
--header-height: 40px;
93+
--status-bar-height: 24px;
9094
color-scheme: dark;
9195
}
9296

@@ -319,4 +323,11 @@ mark.search-highlight--current {
319323
to {
320324
transform: rotate(360deg);
321325
}
322-
}
326+
}
327+
328+
/* Enlarge header on touch devices so layout offsets stay in sync */
329+
@media (pointer: coarse) {
330+
:root {
331+
--header-height: 44px;
332+
}
333+
}

frontend/src/lib/components/content/CodeBlock.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,10 @@
5151
.code-content code {
5252
font-family: inherit;
5353
}
54+
55+
@media (max-width: 767px) {
56+
.code-content {
57+
max-width: calc(100vw - 32px);
58+
}
59+
}
5460
</style>

frontend/src/lib/components/layout/AppHeader.svelte

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,24 @@
6969

7070
<header class="header">
7171
<div class="header-left">
72+
<button
73+
class="hamburger"
74+
onclick={() => {
75+
if (ui.isMobileViewport && router.route !== "sessions") {
76+
sessions.deselectSession();
77+
router.navigate("sessions");
78+
ui.sidebarOpen = true;
79+
} else {
80+
ui.toggleSidebar();
81+
}
82+
}}
83+
title="Toggle sidebar (b)"
84+
aria-label="Toggle sidebar"
85+
>
86+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
87+
<path d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 010 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 010 1.5H1.75A.75.75 0 011 7.75zm0 5a.75.75 0 01.75-.75h12.5a.75.75 0 010 1.5H1.75a.75.75 0 01-.75-.75z"/>
88+
</svg>
89+
</button>
7290
<button
7391
class="header-home"
7492
onclick={() => {
@@ -101,49 +119,53 @@
101119
router.navigate("sessions");
102120
}}
103121
title="Sessions"
122+
aria-label="Sessions"
104123
>
105124
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
106125
<path d="M0 1.5A1.5 1.5 0 011.5 0h2A1.5 1.5 0 015 1.5v2A1.5 1.5 0 013.5 5h-2A1.5 1.5 0 010 3.5v-2zm6 0A1.5 1.5 0 017.5 0h2A1.5 1.5 0 0111 1.5v2A1.5 1.5 0 019.5 5h-2A1.5 1.5 0 016 3.5v-2zm5 0A1.5 1.5 0 0112.5 0h2A1.5 1.5 0 0116 1.5v2A1.5 1.5 0 0114.5 5h-2A1.5 1.5 0 0111 3.5v-2zM0 7.5A1.5 1.5 0 011.5 6h2A1.5 1.5 0 015 7.5v2A1.5 1.5 0 013.5 11h-2A1.5 1.5 0 010 9.5v-2zm6 0A1.5 1.5 0 017.5 6h2A1.5 1.5 0 0111 7.5v2A1.5 1.5 0 019.5 11h-2A1.5 1.5 0 016 9.5v-2zm5 0A1.5 1.5 0 0112.5 6h2A1.5 1.5 0 0116 7.5v2a1.5 1.5 0 01-1.5 1.5h-2A1.5 1.5 0 0111 9.5v-2zM0 13.5A1.5 1.5 0 011.5 12h2A1.5 1.5 0 015 13.5v2A1.5 1.5 0 013.5 17h-2A1.5 1.5 0 010 15.5v-2zm6 0A1.5 1.5 0 017.5 12h2a1.5 1.5 0 011.5 1.5v2A1.5 1.5 0 019.5 17h-2A1.5 1.5 0 016 15.5v-2zm5 0a1.5 1.5 0 011.5-1.5h2a1.5 1.5 0 011.5 1.5v2a1.5 1.5 0 01-1.5 1.5h-2a1.5 1.5 0 01-1.5-1.5v-2z"/>
107126
</svg>
108-
Sessions
127+
<span class="nav-label">Sessions</span>
109128
</button>
110129

111130
<button
112131
class="nav-btn"
113132
class:active={router.route === "pinned"}
114133
onclick={() => router.navigate("pinned")}
115134
title="Pinned Messages"
135+
aria-label="Pinned"
116136
>
117137
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
118138
<path d="M4.146.146A.5.5 0 014.5 0h7a.5.5 0 01.5.5c0 .68-.342 1.174-.646 1.479-.126.125-.25.224-.354.298v4.431l.078.048c.203.127.476.314.751.555C12.36 7.775 13 8.527 13 9.5a.5.5 0 01-.5.5H8.5v5.5a.5.5 0 01-1 0V10H3.5a.5.5 0 01-.5-.5c0-.973.64-1.725 1.17-2.189A6 6 0 015 6.708V2.277a3 3 0 01-.354-.298C4.342 1.674 4 1.179 4 .5a.5.5 0 01.146-.354z"/>
119139
</svg>
120-
Pinned
140+
<span class="nav-label">Pinned</span>
121141
</button>
122142

123143
<button
124144
class="nav-btn"
125145
class:active={router.route === "insights"}
126146
onclick={() => router.navigate("insights")}
127147
title="Insights"
148+
aria-label="Insights"
128149
>
129150
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
130151
<path d="M14.5 3a.5.5 0 01.5.5v9a.5.5 0 01-.5.5h-13a.5.5 0 01-.5-.5v-9a.5.5 0 01.5-.5h13zm-13-1A1.5 1.5 0 000 3.5v9A1.5 1.5 0 001.5 14h13a1.5 1.5 0 001.5-1.5v-9A1.5 1.5 0 0014.5 2h-13z"/>
131152
<path d="M3 5.5a.5.5 0 01.5-.5h9a.5.5 0 010 1h-9a.5.5 0 01-.5-.5zM3 8a.5.5 0 01.5-.5h9a.5.5 0 010 1h-9A.5.5 0 013 8zm0 2.5a.5.5 0 01.5-.5h6a.5.5 0 010 1h-6a.5.5 0 01-.5-.5z"/>
132153
</svg>
133-
Insights
154+
<span class="nav-label">Insights</span>
134155
</button>
135156

136157
<button
137158
class="nav-btn"
138159
class:active={router.route === "trash"}
139160
onclick={() => router.navigate("trash")}
140161
title="Trash"
162+
aria-label="Trash"
141163
>
142164
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
143165
<path d="M5.5 5.5A.5.5 0 016 6v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm2.5 0a.5.5 0 01.5.5v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm3 .5a.5.5 0 00-1 0v6a.5.5 0 001 0V6z"/>
144166
<path fill-rule="evenodd" d="M14.5 3a1 1 0 01-1 1H13v9a2 2 0 01-2 2H5a2 2 0 01-2-2V4h-.5a1 1 0 01-1-1V2a1 1 0 011-1H5.5l1-1h3l1 1h2.5a1 1 0 011 1v1zM4.118 4L4 4.059V13a1 1 0 001 1h6a1 1 0 001-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
145167
</svg>
146-
Trash
168+
<span class="nav-label">Trash</span>
147169
</button>
148170
</div>
149171

@@ -340,7 +362,7 @@
340362

341363
<style>
342364
.header {
343-
height: 40px;
365+
height: var(--header-height, 40px);
344366
display: flex;
345367
align-items: center;
346368
justify-content: space-between;
@@ -610,4 +632,57 @@
610632
.block-filter-reset:hover {
611633
color: var(--text-primary);
612634
}
635+
636+
.hamburger {
637+
display: flex;
638+
width: 28px;
639+
height: 28px;
640+
align-items: center;
641+
justify-content: center;
642+
border-radius: var(--radius-sm);
643+
color: var(--text-muted);
644+
transition: background 0.12s, color 0.12s;
645+
}
646+
647+
.hamburger:hover {
648+
background: var(--bg-surface-hover);
649+
color: var(--text-primary);
650+
}
651+
652+
@media (max-width: 1023px) {
653+
.nav-label {
654+
display: none;
655+
}
656+
657+
.search-hint-text {
658+
display: none;
659+
}
660+
661+
.search-hint-kbd {
662+
display: none;
663+
}
664+
665+
.hamburger {
666+
display: flex;
667+
}
668+
}
669+
670+
@media (max-width: 767px) {
671+
.header-left .nav-btn {
672+
display: none;
673+
}
674+
675+
.header-left :global(.typeahead) {
676+
display: none;
677+
}
678+
}
679+
680+
@media (pointer: coarse) {
681+
.header-btn,
682+
.nav-btn,
683+
.hamburger {
684+
min-width: 44px;
685+
min-height: 44px;
686+
}
687+
}
613688
</style>

frontend/src/lib/components/layout/SessionBreadcrumb.svelte

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,4 +817,22 @@
817817
transparent
818818
);
819819
}
820+
821+
@media (max-width: 767px) {
822+
.breadcrumb-meta {
823+
gap: 4px;
824+
}
825+
826+
.session-time {
827+
display: none;
828+
}
829+
830+
.token-badge {
831+
display: none;
832+
}
833+
834+
.session-id {
835+
display: none;
836+
}
837+
}
820838
</style>

frontend/src/lib/components/layout/StatusBar.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108

109109
<style>
110110
.status-bar {
111-
height: 24px;
111+
height: var(--status-bar-height, 24px);
112112
display: flex;
113113
align-items: center;
114114
justify-content: space-between;
@@ -207,4 +207,10 @@
207207
background: var(--bg-surface-hover);
208208
color: var(--text-secondary);
209209
}
210+
211+
@media (max-width: 767px) {
212+
.status-left {
213+
display: none;
214+
}
215+
}
210216
</style>

0 commit comments

Comments
 (0)