Converts the 9 TODO items into an executable phase roadmap (Protocol B, appended after phase 39): - 40 tuning toggle anonymous flash (TODO L3) - 41 sync fail-fast + modal when a model is down (TODO L4) - 42 no reply autoscroll (TODO L5) - 43 thinking scroll back — user scroll + gated autoscroll (TODO L7) - 44 markdown tables (TODO L6) - 45 agent unlimited tool calls behind BOR_AGENT_MAX_ROUNDS (TODO L8) - 46 mobile hamburger nav (TODO L9) - 47 quadlet + jinja import formats, A9 revision (TODO L10–L11) Each phase carries a user story, a dedicated Playwright E2E suite plan, and owner-locked decisions (R1 A9 format extension, R2 phase-37 budget revision, A1–A5 scope decisions) confirmed 2026-08-27. Also records the completed phases 30–39 todo/ -> complete/ moves that were pending in the working tree. TODO.md is cleared (items now live in .agent/phases/todo/).
5.6 KiB
Story: Mobile hamburger nav
Phase: 46_mobile_hamburger_nav · Source: TODO.md L9 ·
E2E: tests/e2e/test_mobile_hamburger_nav.py
Bug report (verbatim, TODO.md L9)
"The navbar on mobile is way too squished. Make it a hamburger dropdown menu with a nice animation"
Narrative
As a mobile user, the 58px header currently crams up to four text
nav pills (Chat / Sources / Git sources / Tuning) next to the brand and
four icon action pills — the phase-34/35 squeeze at 360–375px leaves
0.72rem-font pills that are hard to hit and hard to read. The nav links
move into a hamburger dropdown menu on small screens: one
#nav-toggle button in the bar, and the links open as an animated
panel below the header with comfortable touch targets.
- Given a viewport ≤640px
- When I tap the hamburger
- Then the nav menu drops down with a short slide+fade animation and full-size links; tapping a link navigates and closes the menu.
- Given a viewport >640px
- When the page loads
- Then nothing changes — the inline nav pills render exactly as today.
Acceptance criteria
- Mobile (≤640px): the inline nav pills are hidden from the bar; a
hamburger button (
#nav-toggle,aria-label="Menu",aria-controls="app-nav",aria-expanded) appears — 44px touch target, icon-only. - Menu:
.app-nav(nowid="app-nav") becomes a dropdown panel below the header — vertical full-width rows, ≥44px targets, readable font size; the auth visibility contract is preserved inside the menu (anonymous: only "Chat"; admin: Chat / Sources / Git sources / Tuning — the same ship-hiddenhiddenattributes the whoami gate already drives). - Animation: opening/closing animates (slide-down + fade, ≈180ms);
prefers-reduced-motion: reducestills it (no transition). - Behavior: toggle flips
aria-expanded;Esccloses while open; a link click navigates and closes the menu; resizing back to640px closes it (the inline nav reappears, no stale state).
- Bar layout: the action pills (Tuning toggle, Sync, New chat,
Sign in/out) stay in the bar icon-only — the phase-35 tightest
squeeze rules on
.nav-link/.app-navgaps are replaced by the roomier menu; the bar fits 360px with the brand intact or clipped as today. - All six pages get the identical toggle + menu (the phase-34 "same bar on every page" contract).
Owner-confirmed (2026-08-27, roadmap A5)
- The hamburger contains the nav links only (Chat / Sources / Git sources / Tuning). The action pills stay in the bar.
- Animation: slide-down + fade, 180ms;
prefers-reduced-motionstills it. - Breakpoint: the existing ≤640px mobile block (no new breakpoint).
UI Visualization & Structure
- Markup (all six pages —
index.html,sources.html,document.html,git-sources.html,login.html,tuning.html): a#nav-togglebutton inserted before<nav class="app-nav" aria-label="Primary">(which gainsid="app-nav"); the nav keeps its existing links andhiddenattributes byte-identically. - CSS (
frontend/assets/styles.css, the@media (max-width: 640px)block):.nav-toggle { display: none }outside,display: inline-flex+ 44px target inside;.app-navbecomes the dropdown: absolute below the header,flex-direction: column, surface background- bottom border/shadow, full-width row links; closed state
(
visibility: hidden; opacity: 0; transform: translateY(-8px); pointer-events: none) →.is-open(visible; opacity: 1; transform: none),transition: opacity/transform 180ms ease; aprefers-reduced-motionoverride kills the transition. The old nav-pill squeeze rules (.nav-link0.72rem,.app-navgap 0.05rem) are superseded for the menu rows.
- bottom border/shadow, full-width row links; closed state
(
- JS (
frontend/assets/header.js, module-import binding like sign-out): null-safe#nav-toggle/#app-nav— click toggles.is-open+aria-expanded; delegated click on nav links closes it;documentkeydownEsccloses while open; amatchMedia("(max-width: 640px)")change listener closes on desktop. No other header.js behavior touched. - Non-goals: no change to the 900px tablet rules, the action pills, the viewer's title bar height, or the no-CDN/A11 constraints.
Playwright Mapping Rule
Test Scenario → tests/e2e/test_mobile_hamburger_nav.py (mock
LLM; DB up):
test_mobile_hamburger_visible_and_bar_roomy— 375×812:#nav-togglevisible witharia-expanded="false"; the inline nav links are not visible in the bar (menu closed); the header has no horizontal overflow.test_anonymous_menu_contents— anonymous: open the menu → exactly "Chat" is visible; open/close flipsaria-expanded.test_admin_menu_contents— login: open the menu → Chat / Sources / Git sources / Tuning all visible (auth contract inside the menu).test_link_click_navigates_and_closes— open, click "Sources" (admin): navigates to/sources.html, and the menu on the arrival page is closed.test_esc_and_backdrop_close— open,Esccloses (aria-expanded false); open again, click outside the panel closes.test_animation_and_reduced_motion— with motion allowed, the menu has a transition (computedtransition-duration≈180ms on the opacity/transform pair); withreducedMotion: "reduce"emulated, the transition is none/0s and the menu still opens/closes.test_desktop_unchanged(regression) — 1280×800: no hamburger, inline nav pills exactly as before (the phase-34/35 bar contract,test_nav_consistency/test_header_consistencypass in the regression pass).