phase: 93_theme_semantic_completion
All verification is complete. Final report: **Phase 93 — Theme semantic completion: FINAL VERIFICATION PASS — ALL GREEN** - Verified full implementation in tree: migration `0016` (8 nullable semantic columns, applied at head), 17-var `BUILTIN_COLORS`/`COLOR_FIELDS`/`effective_settings`, API validation, `#view-theme` State-colors fieldset (17 pickers), `theme.js` FIELDS/PAIRS (5→8), `.page-head` surface panel (6 shell views + doc-edit + shared.html; login card / document sticky header audited as already-surfaced), mock_llm `content: None` fix - Fixed 2 pre-existing defects (both fail identically on baseline `d4f38ad`, proven via worktree A/B): `test_nav_rename_sources` — expected nav tail missing the phase-91 "Theme" link; `test_stale_ui_copy` — now truncates `saved_chats` before/after (house `test_suggestion_chips` pattern) so the seed-chip contract is deterministic on the shared dev DB (owner's 22 saved chats triggered phase-80 last-3-questions) - Tests: `uv run pytest --cov=app --cov-report=term-missing` → **1868 passed, app/ 99%** (>90% ✓); `uv run ruff check .` → clean; `uv run pyright` → **0 errors** - E2E: dedicated `uv run pytest tests/e2e/test_theme_semantic_completion.py -v --no-cov` → **8/8 in isolation** (all-gray 17-color theme: zero residual color on saved-result/Stale/Revoked/Local/tool-call elements, text labels intact, gray heads non-transparent, pre-paint tag, Reset → byte-identical no-tag); 15 theme/header/nav/responsive suites green in isolation; full 85-file combined run: only the 2 fixed pre-existing failures + 1 combined-run artifact (`test_sync_upload_progress`, green in isolation) - Completion criteria: (1) monochrome E2E ✓ (2) default byte-identical, no `#bor-theme` tag ✓ (3) all page heads on solid surface ✓ (4) suite/coverage/lint/E2E green ✓ (5) phases 01–92 no behavior change ✓ (6) commit left to harness per protocol - Notable: cleaned stray uvicorn leftovers from prior implementation pass (owner's `--reload` dev server untouched); no deviations from the phase design - Next pending phase: `94_ls_tree_drilldown`
This commit is contained in:
@@ -1533,6 +1533,25 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
gap: 1.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
/* Phase 93 task 03 (TODO.md L3): "the header and description of each
|
||||
page needs a background - the grid makes it hard to read." Every
|
||||
page's h1 + description sits on a solid surface panel — the house
|
||||
card language (var(--surface) fill, itself tab-controlled, so a
|
||||
monochrome theme grays it automatically; the 1px --line border; the
|
||||
house radius). No new color literal, no blur (the phase-08 perf
|
||||
anchor). One rule covers the six shell views' .page-head (Global
|
||||
Tuning, Knowledge base, Git sources, Saved chats, Access tokens,
|
||||
Theme) + doc-edit.html, and the shared page's head (its h1 + lede
|
||||
are wrapped in the same class in shared.html). #view-chat carries
|
||||
no .page-head — its head is the navbar (audited, unchanged). The
|
||||
History page-head keeps its flex row (scoped below — the padding
|
||||
lives on the flex container, not the children). */
|
||||
.page-head {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
.page-head h1 { margin: 0 0 0.25rem; font-size: 1.7rem; }
|
||||
.page-head-row {
|
||||
display: flex;
|
||||
@@ -3187,7 +3206,7 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
}
|
||||
/* Page title (JS-filled with the shared chat's title; the static
|
||||
fallback is "Shared conversation") — the page-head h1 size. */
|
||||
#shared-title { margin: 0; font-size: 1.7rem; }
|
||||
#shared-title { margin: 0 0 0.25rem; font-size: 1.7rem; }
|
||||
/* The muted meta line under the h1 ("Shared via … — read-only."):
|
||||
ink-soft on the page bg ≥8.6:1, the page-sub language. */
|
||||
.shared-note {
|
||||
|
||||
+57
-29
@@ -17,13 +17,13 @@
|
||||
* gate (the #nav-theme link is already hidden by header.js — the
|
||||
* gate is the DIRECT-URL case, the #tokens-gate pattern). No
|
||||
* /api/ui-settings request is ever made outside the admin branch.
|
||||
* • load — GET /api/ui-settings → populate the 12 inputs with the
|
||||
* • load — GET /api/ui-settings → populate the 20 inputs with the
|
||||
* EFFECTIVE values (the resolver's DB-over-env / DB-over-built-in
|
||||
* merge): the tab always shows the live theme — env defaults when
|
||||
* the row is empty. A failed fetch keeps the static form (the
|
||||
* built-in values ship in the inputs) and shows #theme-error with
|
||||
* a retry (the loadHealth house style — never a blanked panel).
|
||||
* • live preview (colors only, B4) — on `input` of any of the 9
|
||||
* • live preview (colors only, B4) — on `input` of any of the 17
|
||||
* color pickers the value is written straight onto <html> as an
|
||||
* inline custom property, so the WHOLE page repaints (every view,
|
||||
* the header) while the owner is picking. Text fields have NO page
|
||||
@@ -41,8 +41,8 @@
|
||||
* text — themeRootContent is byte-identical to the INNER content
|
||||
* of app.core.theming.theme_style_tag (the tag is removed when
|
||||
* the palette is the built-in one — the server's no-op case) — so
|
||||
* the document mirrors what the next load serves. (2) The 9
|
||||
* identity variables as inline custom properties on <html> (CSSOM
|
||||
* the document mirrors what the next load serves. (2) The 17
|
||||
* palette variables as inline custom properties on <html> (CSSOM
|
||||
* setProperty / removeProperty — the live preview's mechanism) —
|
||||
* THIS half is what repaints the open page, because Chromium
|
||||
* re-checks a <style> element's content against style-src on
|
||||
@@ -59,7 +59,7 @@
|
||||
* the admin opens the tab (a normal load is a no-op — the served
|
||||
* tag and the overrides agree).
|
||||
* • Save — the §7.4 never-stale lifecycle: disable + "Saving…" →
|
||||
* PUT /api/ui-settings with the 12 form values (a cleared/empty
|
||||
* PUT /api/ui-settings with the 20 form values (a cleared/empty
|
||||
* text field → null; colors always their current hex — the
|
||||
* server's built-in→NULL normalization keeps the row empty when
|
||||
* the owner saves the defaults) → 200: #theme-result "Theme
|
||||
@@ -75,25 +75,30 @@
|
||||
* field), the form is KEPT (the owner fixes + retries); any other
|
||||
* non-2xx: the fixed error line; a network error: the "is the app
|
||||
* reachable?" line.
|
||||
* • Reset — the same lifecycle ("Resetting…") with all 12 values
|
||||
* • Reset — the same lifecycle ("Resetting…") with all 20 values
|
||||
* null (the API's documented "defaults" operation) → #theme-result
|
||||
* "Reset to the built-in theme." → refetch + re-populate (the
|
||||
* env/built-in defaults) → reconcile the open document: the tag
|
||||
* is REMOVED (effective = the built-ins → content null) and the
|
||||
* <html> overrides are dropped (a failed refetch still drops
|
||||
* them — the picks are stale once the reset landed).
|
||||
* • WCAG contrast (the 00_phase design's five pairs — the pairs the
|
||||
* layout actually pairs, see app/core/theming.py's docstring):
|
||||
* ink on bg, ink on surface, ink-soft on surface, bg on brand
|
||||
* (the text on brand buttons is the dark background ink — never
|
||||
* white on brand), brand-ink on surface. Evaluated on every color
|
||||
* `input` and after every load/save over the CURRENT form values,
|
||||
* via WCAG relative luminance (sRGB → linear → L). Any pair under
|
||||
* 4.5:1 is listed in #theme-contrast (role=alert) as "--ink on
|
||||
* --bg: 3.2:1 — needs 4.5:1"; all pass → the warning hides.
|
||||
* WARNING-ONLY: it never disables Save (the owner's homelab
|
||||
* palette — the built-in stays AA, so the default deployment is
|
||||
* warning-free).
|
||||
* • WCAG contrast (the phase-93 design's EIGHT pairs — the pairs
|
||||
* the layout actually pairs, see app/core/theming.py's docstring —
|
||||
* this file's PAIRS is its client-side mirror, and the two must
|
||||
* never diverge; tests/unit/test_theming.py pins the mirror):
|
||||
* the five identity pairs (ink on bg, ink on surface, ink-soft on
|
||||
* surface, bg on brand (the text on brand buttons is the dark
|
||||
* background ink — never white on brand), brand-ink on surface)
|
||||
* plus the three semantic ink-on-bg pairs (phase 93 — the two
|
||||
* *_line borders are decorative, no contrast duty): ok-ink on
|
||||
* ok-bg, err-ink on err-bg, accent-ink on accent-bg. Evaluated on
|
||||
* every color `input` and after every load/save over the CURRENT
|
||||
* form values, via WCAG relative luminance (sRGB → linear → L).
|
||||
* Any pair under 4.5:1 is listed in #theme-contrast (role=alert)
|
||||
* as "--ink on --bg: 3.2:1 — needs 4.5:1"; all pass → the warning
|
||||
* hides. WARNING-ONLY: it never disables Save (the owner's
|
||||
* homelab palette — the built-in stays AA, so the default
|
||||
* deployment is warning-free).
|
||||
* • re-show — the phase-77 hook: a user-initiated re-show of this
|
||||
* already-mounted view makes the router dispatch bor:view-refresh
|
||||
* on the section — re-run the load then (the tab always shows the
|
||||
@@ -123,13 +128,17 @@ export async function mount(root) {
|
||||
const SAVE_LABEL = "Save theme";
|
||||
const RESET_LABEL = "Reset to defaults";
|
||||
|
||||
/* The 12 form fields, in the form's order: `field` is the API key
|
||||
/* The 20 form fields, in the form's order: `field` is the API key
|
||||
(the input's name attribute), `id` the E2E-stable element id,
|
||||
`kind` how the value is read for a PUT — a string field that is
|
||||
empty after the trim sends null (the server stores NULL = "use
|
||||
the default"); a color field always sends its current #rrggbb
|
||||
(the server's built-in→NULL normalization keeps the row empty
|
||||
when the owner saves the defaults). */
|
||||
when the owner saves the defaults). The 17 color fields follow
|
||||
the server's theming.COLOR_FIELDS order (the 9 identity
|
||||
variables, then the 8 semantic state variables — phase 93), so
|
||||
everything FIELDS-driven (live preview, collectBody,
|
||||
applyServedTheme's tag content) stays in COLOR_FIELDS order. */
|
||||
const FIELDS = [
|
||||
{ field: "app_name", id: "theme-app-name", kind: "string" },
|
||||
{ field: "input_placeholder", id: "theme-placeholder", kind: "string" },
|
||||
@@ -143,6 +152,17 @@ export async function mount(root) {
|
||||
{ field: "brand", id: "theme-brand", kind: "color" },
|
||||
{ field: "brand_soft", id: "theme-brand-soft", kind: "color" },
|
||||
{ field: "brand_ink", id: "theme-brand-ink", kind: "color" },
|
||||
/* The 8 semantic state colors (phase 93 — B3 revised): success,
|
||||
error, and notice. The *_line fields are decorative borders —
|
||||
no contrast duty (not in PAIRS, like line / grid_line). */
|
||||
{ field: "ok_bg", id: "theme-ok-bg", kind: "color" },
|
||||
{ field: "ok_ink", id: "theme-ok-ink", kind: "color" },
|
||||
{ field: "err_bg", id: "theme-err-bg", kind: "color" },
|
||||
{ field: "err_ink", id: "theme-err-ink", kind: "color" },
|
||||
{ field: "err_line", id: "theme-err-line", kind: "color" },
|
||||
{ field: "accent_bg", id: "theme-accent-bg", kind: "color" },
|
||||
{ field: "accent_ink", id: "theme-accent-ink", kind: "color" },
|
||||
{ field: "accent_line", id: "theme-accent-line", kind: "color" },
|
||||
];
|
||||
|
||||
const inputs = {};
|
||||
@@ -151,13 +171,14 @@ export async function mount(root) {
|
||||
const isHex = (v) => typeof v === "string" && /^#[0-9a-fA-F]{6}$/.test(v);
|
||||
const cssVar = (field) => "--" + field.replace(/_/g, "-");
|
||||
|
||||
/* ---------- WCAG contrast (the five pairs) ----------
|
||||
/* ---------- WCAG contrast (the eight pairs) ----------
|
||||
* Relative luminance per WCAG 2.1: each sRGB channel is linearized
|
||||
* (the 0.04045 threshold) then weighted (0.2126 / 0.7152 / 0.0722);
|
||||
* the ratio is (L_lighter + 0.05) / (L_darker + 0.05). The five
|
||||
* the ratio is (L_lighter + 0.05) / (L_darker + 0.05). The eight
|
||||
* pairs (foreground, background) are exactly the ones the layout
|
||||
* pairs — app/core/theming.py's docstring is the authoritative
|
||||
* table. */
|
||||
* table; PAIRS is its client-side mirror (never diverge —
|
||||
* tests/unit/test_theming.py pins it). */
|
||||
function channelLuminance(channel) {
|
||||
const s = channel / 255;
|
||||
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
||||
@@ -187,9 +208,14 @@ export async function mount(root) {
|
||||
["ink_soft", "surface"],
|
||||
["bg", "brand"],
|
||||
["brand_ink", "surface"],
|
||||
/* The three semantic ink-on-bg pairs (phase 93) — the two
|
||||
*_line borders stay excluded (decorative, no contrast duty). */
|
||||
["ok_ink", "ok_bg"],
|
||||
["err_ink", "err_bg"],
|
||||
["accent_ink", "accent_bg"],
|
||||
];
|
||||
|
||||
/* Re-evaluate the five pairs over the CURRENT form values. Any pair
|
||||
/* Re-evaluate the eight pairs over the CURRENT form values. Any pair
|
||||
under 4.5:1 is listed in #theme-contrast (one line per failing
|
||||
pair, " · "-joined — textContent, never HTML); all pass → the
|
||||
warning hides. A pair whose input is not a valid hex (defensive —
|
||||
@@ -268,7 +294,7 @@ export async function mount(root) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Drop all 9 preview overrides so the page paints the served
|
||||
/* Drop all 17 preview overrides so the page paints the served
|
||||
(injected) theme — the "never stale" half of the contract: after
|
||||
a save / reset / re-show the page shows what the server serves,
|
||||
not a pick that was never (or no longer) saved. */
|
||||
@@ -294,7 +320,7 @@ export async function mount(root) {
|
||||
/* The :root string the server would inject on the NEXT load for
|
||||
these effective values. null when every color field equals its
|
||||
captured BUILTINS value — the server's no-op case (no tag served,
|
||||
none to keep). Otherwise all 9 colors in FIELDS order (== the
|
||||
none to keep). Otherwise all 17 colors in FIELDS order (== the
|
||||
server's COLOR_FIELDS order) — byte-identical to the INNER
|
||||
content of app.core.theming.theme_style_tag's tag (lowercased hex
|
||||
from the resolver), so a saved theme never jumps between the
|
||||
@@ -375,9 +401,9 @@ export async function mount(root) {
|
||||
}
|
||||
}
|
||||
|
||||
/* GET /api/ui-settings → populate the 12 inputs with the EFFECTIVE
|
||||
/* GET /api/ui-settings → populate the 20 inputs with the EFFECTIVE
|
||||
values (the tab always shows the live theme — env defaults when
|
||||
the row is empty) and re-check the five pairs (a SAVED palette
|
||||
the row is empty) and re-check the eight pairs (a SAVED palette
|
||||
can itself fail AA — the warning then tracks it). A failed fetch
|
||||
keeps the static form + shows #theme-error with a retry (the
|
||||
loadHealth house style — never a blanked panel). Returns the
|
||||
@@ -411,6 +437,8 @@ export async function mount(root) {
|
||||
/* ---------- the PUT (Save + Reset share it) ---------- */
|
||||
|
||||
function collectBody() {
|
||||
/* All 20 fields — the 3 strings + the 17 colors (the 9 identity
|
||||
+ the 8 semantic state, phase 93). */
|
||||
const body = {};
|
||||
for (const f of FIELDS) {
|
||||
const input = inputs[f.field];
|
||||
@@ -510,7 +538,7 @@ export async function mount(root) {
|
||||
if (gateEl) gateEl.hidden = true;
|
||||
if (contentEl) contentEl.hidden = false;
|
||||
|
||||
/* The 9 built-in hexes, captured from the color inputs' STATIC
|
||||
/* The 17 built-in hexes, captured from the color inputs' STATIC
|
||||
values — at the top of the admin branch, BEFORE the first
|
||||
loadSettings() below repopulates them with the EFFECTIVE values.
|
||||
The static values ARE the built-ins (the house contract — the
|
||||
|
||||
Reference in New Issue
Block a user