docs(phases): convert TODO.md L3-L4 into phases 170-171 (edit bubble, settings layout)

170_edit_bubble_sizing — the edit form renders inside the user bubble
(same box, no shrink) and its submit label reads "Send", not "Save"
(TODO.md L3).
171_admin_settings_layout — settings page CSS fixes: #settings-content
vertical rhythm (search/table overlap + smushed action row), distinct
group bands, a properly disabled Save cursor (TODO.md L4).

Numbering note: 169 (embedding generation migration) was created,
executed and abandoned before this conversion; its number is not
reused. TODO.md is cleared — the items now live in .agents/phases/.

LLM-Generated: true
This commit is contained in:
2026-10-09 23:42:15 -04:00
parent 542e568d25
commit abe7388dda
9 changed files with 327 additions and 0 deletions
@@ -0,0 +1,46 @@
# Phase 171 — Admin settings page: row rhythm, distinct group bands, a properly disabled Save
**Source:** `TODO.md` L4 — "In the admin settings page: The search settings text box overlaps settings table. The settings category headers are not differentiated from the settings themselves, making them confusing at a glance. The save settings and clear all overrides are smushed right up against the settings table. The save settings shows a loading cursor when moused over rather than just being disabled."
**Story:** n/a (TODO-derived, owner roadmap confirmation 2026-10-10)
**Context:** `frontend/index.html` — `#view-settings` (L1499), `#settings-content` (L1535, a plain block — its children `#settings-disabled`, `#settings-status`, `#settings-error`, `.settings-search` (L1562), the `.table-wrap` card (L1576, `#settings-tbody` created by settings.js), and `.settings-actions` (L1607: `#settings-save` disabled by default + `#settings-clear-all` + `#settings-dirty`) stack with **zero vertical rhythm** — the root of the "overlap" and the "smushed" complaints), `frontend/assets/settings.js` (637 lines — the group-row render L319–335: `tr.settings-group-row` + `th[colspan=4][scope=colgroup]` with the group name, the client-side search filter, the §7.4 Save lifecycle), `frontend/assets/styles.css` — `.settings-shell` (L4660–4665: `display:flex; flex-direction:column; gap: 1.25rem;`), `.settings-group-row th` (L4684–4690: `background: var(--surface)` — the **same** fill as the `.table-wrap` card (L2825–2831) → the band is effectively invisible, the "not differentiated" complaint), the sticky-th tint rule `.table-wrap table thead th { background: var(--brand-soft); }` (L2841–2848, the solid-tint precedent), `.settings-search` (L4780–4786), `.settings-actions` (L4804–4809), `#settings-save` (L4810–4824, incl. `:disabled { opacity: 0.6; cursor: wait; }` L4824 — the loading-cursor complaint), `.settings-reset:disabled { opacity: 0.5; cursor: default; }` (L4775 — the house idiom), the mobile media queries (L6396–6400: `.settings-search` / `.settings-actions` `flex-direction: column`, `#settings-save` `width: 100%`), `tests/unit/test_settings_frontend_view.py` (the phase-168 static pins, incl. the L561 media-query pin), `tests/e2e/test_admin_settings_panel.py` (the phase-168 suite — the regression set).
## Objective
The four layout complaints on the Settings page — the search box overlapping the table card, the group headers indistinguishable from the rows, the Save / Clear row smushed against the table, and the disabled Save showing a loading cursor — are fixed with **CSS only** (plus the pin updates): one rhythm rule, one band rule, one cursor value. The phase-168 contract (the gate, the catalogue, the §7.4 Save lifecycle, the API, the bounded scroll + sticky header) stays behavior-identical — no `app/` code, no JS logic change.
## Design (shared by all tasks — the executor reads this, not the chat)
- **D1 — the rhythm (task 01).** `#settings-content` becomes `display: flex; flex-direction: column; gap: 1.25rem;` — the **same** token `.settings-shell` already uses (L4664). The children (the disabled banner, the status line, the error line, the search row, the `.table-wrap` card, the `.settings-actions` row) then stack with one uniform gap: the search box clears the table card (the "overlap" dies) and the Save / Clear row is separated from the table (the "smushed" dies) in the same change. The `min-height` rules on `#settings-status` / `#settings-error` (L4849–4862) already reserve their lines — the gap is additive and uniform, no per-element margins are introduced.
- **D2 — the band (task 02).** `.settings-group-row th` gets a band that is distinct from the row cells at a glance: `background: var(--brand-soft)` — the **same solid tint** the sticky `thead` rule already uses (L2847; B3: no new palette variable), `font-weight: 700` (the 0.82rem / uppercase / letter-spacing stay), and `border-top: 1px solid var(--line)` so a band never merges with the card's top edge or a previous band. The row cells stay on the card's `--surface`. The group text stays `--ink` on `--brand-soft` — the pairing the sticky-th rule already documents. Static, text-only, no animation (nothing for `prefers-reduced-motion` to still).
- **D3 — the cursor (task 03).** `#settings-save:disabled` → `cursor: default` — the `.settings-reset:disabled` idiom (L4775); "just disabled", per the owner. The §7.4 in-flight state is the **dimmed** `:disabled` (`opacity: 0.6` stays) + `aria-busy` — the `cursor: wait` was never part of the contract; it was the "loading cursor on hover" the owner saw.
- **NOT touched:** `settings.js` (the render / filter / save / reset / clear-all logic), `app/api/admin_settings.py`, the sign-in gate, the nav reveal, the `.table-wrap` bounded-scroll + sticky-th contract (L2825–2848), the `#settings-status` / `#settings-error` regions, the `restart required` flag, the mobile media queries (L6396–6400 — they compose with the new gap and stay byte-identical), `app/` server code.
## Dependencies
- `168_admin_settings_panel` (in flight — the code + the E2E suite are committed; only its directory move + final commit are pending) — the Settings view this phase polishes. Note: the abandoned `169` settings status card never landed — there is nothing to coordinate with.
## Tasks
1. `01_content_vertical_rhythm.md` — `#settings-content` becomes a flex column with the shell's `1.25rem` gap (fixes the search/table overlap **and** the smushed action row); the pin added.
2. `02_group_band_distinction.md` — `.settings-group-row th` gets the `--brand-soft` band + 700 weight + top hairline, so groups read at a glance.
3. `03_disabled_save_cursor.md` — `#settings-save:disabled` → `cursor: default` (the house idiom), killing the loading cursor.
4. `04_e2e_settings_layout.md` — `tests/e2e/test_admin_settings_layout.py` (the phase's dedicated Playwright suite, AGENTS rule 4), the regression set, the full gates, the atomic commit.
## Testing & Quality
- **Unit:** `tests/unit/test_settings_frontend_view.py` (the phase-168 static pin set, extended in tasks 01/02/03): the `#settings-content` flex-column + gap rule, the band's `--brand-soft` background + weight + top border, `#settings-save:disabled` `cursor: default` (and `cursor: wait` absent from the settings rules), plus the existing pins (the no-CDN pin, the `router.js` mapping, the media-query pin L561) staying green.
- **E2E (AGENTS rule 4, task 04):** `tests/e2e/test_admin_settings_layout.py`, run **in isolation** — the four layout contracts, proven in a browser.
- **Coverage:** **>90%** on `app/` (this phase changes no `app/` code — the gate must stay green, not drop).
- **Lint/types:** `uv run ruff check . && uv run pyright`.
## Completion Criteria
- [ ] The search box and the table card do not intersect; the Save / Clear row is visibly separated from the table (the E2E bounding-box pins, at 1440 px and 375 px).
- [ ] A settings group band is visually distinct from its rows (computed background ≠ the row cells' background; weight 700) and no band merges with the card edge.
- [ ] A disabled **Save settings** shows `cursor: default` (not `wait`); the §7.4 dimmed-`aria-busy` in-flight state is intact.
- [ ] `uv run pytest` green, `app/` coverage >90% (`uv run pytest --cov=app --cov-report=term-missing`), ruff + pyright clean.
- [ ] `uv run pytest tests/e2e/test_admin_settings_layout.py -v --no-cov` green **in isolation**; `test_admin_settings_panel.py` (phase 168) + `test_wide_desktop_column.py` green in isolation.
- [ ] One atomic `--no-gpg-sign` Conventional Commit; the phase dir moves to `.agents/phases/complete/`.
## Locked decisions (owner-confirmed 2026-10-10, roadmap confirmation)
- **A3 — the band tint is `--brand-soft`** (the solid tint the sticky `thead` already uses — B3: no new palette variable) + a `--line` top hairline + weight 700; static, no animation.
- **A4 — the rhythm reuses the shell's existing `gap: 1.25rem`** — no new spacing value; the existing mobile media queries (L6396–6400) keep working unchanged.
## Commit
```bash
git add frontend/ tests/ .agents/phases/ && git commit --no-gpg-sign -m "fix(settings): settings page layout — row rhythm, distinct group bands, a properly disabled Save"
```