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/).
111 lines
5.6 KiB
Markdown
111 lines
5.6 KiB
Markdown
# 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
|
||
1. **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.
|
||
2. **Menu:** `.app-nav` (now `id="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-hidden `hidden` attributes the whoami gate
|
||
already drives).
|
||
3. **Animation:** opening/closing animates (slide-down + fade, ≈180ms);
|
||
`prefers-reduced-motion: reduce` stills it (no transition).
|
||
4. **Behavior:** toggle flips `aria-expanded`; `Esc` closes while open;
|
||
a link click navigates **and** closes the menu; resizing back to
|
||
>640px closes it (the inline nav reappears, no stale state).
|
||
5. **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-nav` gaps are replaced by the
|
||
roomier menu; the bar fits 360px with the brand intact or clipped as
|
||
today.
|
||
6. **All six pages** get the identical toggle + menu (the phase-34
|
||
"same bar on every page" contract).
|
||
|
||
## Owner-confirmed (2026-08-27, roadmap A5)
|
||
1. **The hamburger contains the nav links only** (Chat / Sources / Git
|
||
sources / Tuning). The action pills stay in the bar.
|
||
2. **Animation:** slide-down + fade, 180ms; `prefers-reduced-motion`
|
||
stills it.
|
||
3. **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-toggle` button inserted before `<nav class="app-nav"
|
||
aria-label="Primary">` (which gains `id="app-nav"`); the nav keeps
|
||
its existing links and `hidden` attributes 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-nav` becomes 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`; a
|
||
`prefers-reduced-motion` override kills the transition. The old
|
||
nav-pill squeeze rules (`.nav-link` 0.72rem, `.app-nav` gap 0.05rem)
|
||
are superseded for the menu rows.
|
||
- **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;
|
||
`document` keydown `Esc` closes while open; a
|
||
`matchMedia("(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):
|
||
1. `test_mobile_hamburger_visible_and_bar_roomy` — 375×812: `#nav-toggle`
|
||
visible with `aria-expanded="false"`; the inline nav links are not
|
||
visible in the bar (menu closed); the header has no horizontal
|
||
overflow.
|
||
2. `test_anonymous_menu_contents` — anonymous: open the menu → exactly
|
||
"Chat" is visible; open/close flips `aria-expanded`.
|
||
3. `test_admin_menu_contents` — login: open the menu → Chat / Sources /
|
||
Git sources / Tuning all visible (auth contract inside the menu).
|
||
4. `test_link_click_navigates_and_closes` — open, click "Sources"
|
||
(admin): navigates to `/sources.html`, and the menu on the arrival
|
||
page is closed.
|
||
5. `test_esc_and_backdrop_close` — open, `Esc` closes (aria-expanded
|
||
false); open again, click outside the panel closes.
|
||
6. `test_animation_and_reduced_motion` — with motion allowed, the menu
|
||
has a transition (computed `transition-duration` ≈180ms on the
|
||
opacity/transform pair); with `reducedMotion: "reduce"` emulated,
|
||
the transition is none/0s and the menu still opens/closes.
|
||
7. `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_consistency` pass in the
|
||
regression pass).
|