chore(agent): phase roadmap from TODO.md — 8 phases (40–47), 24 tasks

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/).
This commit is contained in:
2026-08-27 18:25:53 -04:00
parent 492d8275e7
commit 02c76ad328
66 changed files with 1906 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
# 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).