phase: 91_admin_theme_tab
All verification is complete — this pass needed no code changes. Final report: **Phase 91 — Admin Theme tab: final verification pass (all 6 tasks already in `complete/`)** - Verified pre-paint theming end-to-end: `ui_settings` store + resolver, admin `GET/PUT /api/ui-settings`, `CachingMiddleware` inline-`<style id="bor-theme">` injection before `</head>` (incl. `/shared/<token>` prefix branch, unit-pinned), CSP sha256 exemption for the inline tag, Theme tab shell + `theme.js` editor, CSS-file theming fully retired. - No defects found; zero changes made — working tree left exactly as the task executors left it. - Tests: `uv run pytest --cov=app` → 1841 passed, 0 failed (TOTAL coverage **99%**; theming/ui_settings/caching all 100%); `uv run pytest tests/e2e/test_admin_theme_tab.py -v --no-cov` → **5 passed** in isolation. - Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings. - Criteria: (1) unset deployment byte-identical, no `#bor-theme` anywhere — ✓ (unit no-op test + E2E reset byte-compare); `rg "BOR_THEME|themes/"` → single hit is the permitted doc-history comment in `frontend/index.html`. (2) admin-only gate + 403s for anonymous and token users — ✓ (E2E test 3). (3) saved theme inline before `</head>` on every page incl. `/shared/<token>`, computed `--brand` on first paint for admin + anonymous — ✓ (E2E test 2 + unit). (4) reset → byte-identical; 5 contrast pairs warn <4.5:1, non-blocking — ✓ (E2E tests 4–5). (5) suite green, >90% coverage, lint clean — ✓. (6) commit deferred to harness per rules. - Notable: `.agents/PLAN.md` is absent from the repo — the phase overview's Design section was used as the binding spec; no deviation resulted. - Next pending phase: **none** — 91 is the last phase in `todo/`.
This commit is contained in:
@@ -2951,6 +2951,185 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ---------- Theme view (phase 91, tasks 04 + 05) ----------
|
||||
The shell's seventh view (#view-theme): the admin palette + branding
|
||||
editor (task 05). The centered 46rem column (the .tuning-shell
|
||||
language — this is a form view, the tuning-view width pattern), the
|
||||
form card (the #tune-form language: surface fill, --line hairline,
|
||||
radius, shadow), the fieldset groups (Branding / Palette) with the
|
||||
.theme-note sub-copy, the 3-column (desktop) / 1-column (mobile)
|
||||
palette grid with the color swatch inputs sized for touch (44px —
|
||||
the label above is the second tap affordance), and the Save/Reset
|
||||
row (Save = the brand pill family, --bg ink on --brand 5.2:1 AA;
|
||||
Reset = the ghost button family, --line border; the §7.4
|
||||
disabled-while-in-flight look is the :disabled pair below). The
|
||||
feedback lines: error/result are the house role=status/alert line
|
||||
languages; #theme-contrast (the WCAG warning) uses the --err-*
|
||||
STATE family — states are semantic colors, never themed from the
|
||||
tab (B3). Every pair reuses the Phase-08 AA palette; :focus-visible
|
||||
via the global 3px outline rule. No CDN, system fonts. */
|
||||
.theme-shell {
|
||||
max-width: 46rem;
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
/* The static form — one card holding the two fieldset groups + the
|
||||
actions row (the #tune-form card language). */
|
||||
#theme-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 1rem 1.1rem 1.1rem;
|
||||
}
|
||||
/* The fieldset groups: reset the UA border (the card IS the group's
|
||||
frame), the legend rides the group's title line (the
|
||||
.tuning-panel-title language — ink, 1rem, 700). */
|
||||
.theme-group {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.theme-group-title {
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
}
|
||||
/* The branding text inputs: the #token-label language (surface is
|
||||
already the card fill — transparent field, --line hairline, ≥44px
|
||||
target, the global :focus-visible ring). */
|
||||
#theme-form fieldset label {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
#theme-form fieldset input[type="text"] {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-size: 0.93rem;
|
||||
}
|
||||
/* The palette note under the branding legend (task 05): the strings
|
||||
apply on the next page load (B4) — the live preview covers the
|
||||
palette only. */
|
||||
.theme-note {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.35;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
/* The color-input grid (task 05): the 8 swatches in 3 columns
|
||||
(3 + 3 + 2 rows) — the cell is a .theme-color (label above the
|
||||
swatch); 1 column at the mobile breakpoint (below). */
|
||||
.theme-colors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.6rem 0.9rem;
|
||||
}
|
||||
.theme-color {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.theme-color label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink-soft);
|
||||
line-height: 1.25;
|
||||
}
|
||||
.theme-color input[type="color"] {
|
||||
/* Touch-sized swatch (task 05): 44px tall (the WCAG 2.5.8 target —
|
||||
the label above is the second tap affordance). */
|
||||
inline-size: 56px;
|
||||
block-size: 44px;
|
||||
padding: 3px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Save (primary) + Reset (secondary): the pill + ghost families
|
||||
(.history-refresh / .token-revoke languages — the global
|
||||
:focus-visible ring applies, no button-scoped focus override). */
|
||||
.theme-actions {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
#theme-save {
|
||||
min-height: 44px;
|
||||
padding: 0.4rem 1.2rem;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--brand);
|
||||
color: var(--bg); /* dark ink on brand: 5.2:1 (never white on brand) */
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
#theme-save:hover:not(:disabled) { background: #f55a72; color: var(--bg); }
|
||||
#theme-save:disabled { opacity: 0.6; cursor: wait; }
|
||||
.theme-reset {
|
||||
min-height: 44px;
|
||||
padding: 0.4rem 1rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--ink-soft);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.theme-reset:hover:not(:disabled) { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
|
||||
.theme-reset:disabled { opacity: 0.6; cursor: wait; }
|
||||
/* The three feedback lines (task 05): error (role=alert — the
|
||||
server's 422 detail) and result (role=status) are the house line
|
||||
languages; min-height holds the layout so a line never reflows the
|
||||
form. #theme-contrast (the WCAG warning, task 05) takes the
|
||||
.tuning-error box treatment in the --err-* STATE family — states
|
||||
are semantic colors, never themed from the tab (B3): a failing
|
||||
pair reads as a warning and the box is warning-only (Save is never
|
||||
disabled by it). */
|
||||
.theme-error,
|
||||
.theme-result,
|
||||
.theme-contrast {
|
||||
display: block;
|
||||
margin: 0;
|
||||
min-height: 1.2em;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.8rem;
|
||||
padding-block: 0.25rem;
|
||||
}
|
||||
.theme-error { color: var(--err-ink); }
|
||||
.theme-result { color: var(--ok-ink); }
|
||||
.theme-contrast {
|
||||
background: var(--err-bg);
|
||||
color: var(--err-ink);
|
||||
border: 1px solid var(--err-line);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.45rem 0.8rem;
|
||||
}
|
||||
|
||||
/* ---------- Shared page (phase 51, task 03) ----------
|
||||
/shared/<token>: the anonymous read-only conversation (owner-locked
|
||||
2026-08-29, TODO.md L6). The shell maps to the PLAN §7 centered
|
||||
@@ -3979,6 +4158,13 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
#token-once-copy { width: 100%; }
|
||||
.tokens-actions-cell { white-space: normal; }
|
||||
.tokens-actions { flex-wrap: wrap; }
|
||||
/* Phase 91 task 05: the Theme form squeezes — the color grid drops
|
||||
to 1 column (the label + swatch pairs keep their ≥44px targets)
|
||||
and the actions row stacks (Save full width, Reset under it). */
|
||||
.theme-colors { grid-template-columns: 1fr; }
|
||||
.theme-actions { flex-direction: column; align-items: stretch; }
|
||||
#theme-save { width: 100%; }
|
||||
.theme-reset { width: 100%; }
|
||||
/* Phase 51: the shared page squeezes like the chat column — the
|
||||
title and the note step down (the empty-state-title family); the
|
||||
shell keeps its base 46rem column (the >=1500px 92rem override
|
||||
|
||||
Reference in New Issue
Block a user