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
8.2 KiB
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-contentbecomesdisplay: flex; flex-direction: column; gap: 1.25rem;— the same token.settings-shellalready uses (L4664). The children (the disabled banner, the status line, the error line, the search row, the.table-wrapcard, the.settings-actionsrow) 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. Themin-heightrules 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 thgets a band that is distinct from the row cells at a glance:background: var(--brand-soft)— the same solid tint the stickytheadrule already uses (L2847; B3: no new palette variable),font-weight: 700(the 0.82rem / uppercase / letter-spacing stay), andborder-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--inkon--brand-soft— the pairing the sticky-th rule already documents. Static, text-only, no animation (nothing forprefers-reduced-motionto still). - D3 — the cursor (task 03).
#settings-save:disabled→cursor: default— the.settings-reset:disabledidiom (L4775); "just disabled", per the owner. The §7.4 in-flight state is the dimmed:disabled(opacity: 0.6stays) +aria-busy— thecursor: waitwas 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-wrapbounded-scroll + sticky-th contract (L2825–2848), the#settings-status/#settings-errorregions, therestart requiredflag, 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 abandoned169settings status card never landed — there is nothing to coordinate with.
Tasks
01_content_vertical_rhythm.md—#settings-contentbecomes a flex column with the shell's1.25remgap (fixes the search/table overlap and the smushed action row); the pin added.02_group_band_distinction.md—.settings-group-row thgets the--brand-softband + 700 weight + top hairline, so groups read at a glance.03_disabled_save_cursor.md—#settings-save:disabled→cursor: default(the house idiom), killing the loading cursor.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-contentflex-column + gap rule, the band's--brand-softbackground + weight + top border,#settings-save:disabledcursor: default(andcursor: waitabsent from the settings rules), plus the existing pins (the no-CDN pin, therouter.jsmapping, 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 noapp/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(notwait); the §7.4 dimmed-aria-busyin-flight state is intact. uv run pytestgreen,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-covgreen in isolation;test_admin_settings_panel.py(phase 168) +test_wide_desktop_column.pygreen in isolation.- One atomic
--no-gpg-signConventional 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 stickytheadalready uses — B3: no new palette variable) + a--linetop 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
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"