Accent colour #46

Closed
opened 2026-10-06 23:18:32 +02:00 by EduarD3V · 3 comments
Owner

URL:http://192.168.31.41:5173/operator/personalization

Create that new page for the user to select the accent color. the modal will keep showing system,dark and light, but the accent color can be changed there. like a tint

URL:http://192.168.31.41:5173/operator/personalization Create that new page for the user to select the accent color. the modal will keep showing system,dark and light, but the accent color can be changed there. like a tint
gotoi-agent added reference issue/46-accent-colour 2026-10-07 14:34:19 +02:00
Collaborator

Claimed — branch issue/46-accent-colour. Doing: new /operator/personalization page that picks the accent (tint) for --primary/--accent, persisted browser-local alongside the existing system/light/dark selector.

Claimed — branch `issue/46-accent-colour`. Doing: new `/operator/personalization` page that picks the accent (tint) for `--primary`/`--accent`, persisted browser-local alongside the existing system/light/dark selector.
Collaborator

Issue #46 — Accent colour: plan

Restated goal

Add dashboard/src/routes/operator/personalization/+page.svelte — the URL the issue names,
/operator/personalization — where the operator picks the console's accent (a tint). The accent is
a hue that recolours the shipped brand token --primary (plus the two tokens that must follow it
in lockstep, --accent and --sidebar-ring), so chips, focus rings, links, the active nav pill,
--info, and --chart-1 all move with it — there is no second brand colour. The pick is stored in
localStorage and re-applied before first paint, so a reload keeps the tint with no flash. The
existing system/light/dark appearance modal is untouched and keeps its three options; this page adds
the accent control and nothing else. "Done" for the operator: open the page, click a swatch, the whole
console re-tints immediately, reload, still tinted, and a Reset returns it to Polar Indigo.

Scope

Files changed (dashboard only, no middleware, no agent, no schema, no API):

File Change
dashboard/src/app.css add --accent-hue: 260 to :root; replace the hardcoded hue in the 5 tinted literals with var(--accent-hue)
dashboard/src/lib/theme.ts accent storage key, palette, store, parseAccentHue / applyAccentHue / setAccentHue; bump Last
dashboard/src/app.html boot IIFE also reads gotoi-accent and sets --accent-hue before first paint
dashboard/src/routes/operator/personalization/+page.svelte new page
dashboard/src/lib/nav.ts Operator item Personalization + palette icon in ITEM_ICONS; bump Last
dashboard/src/lib/page-help.ts /operator/personalization help copy; bump Last
dashboard/src/lib/dashboard/destinations.ts ['/operator/personalization', paletteIcon] prefix
scripts/e2e/nav_selenium.py add the href to NAV_HREFS
dashboard/src/lib/usecases-frontend.test.ts update the two operator href assertions; add accent tests; bump Last
docs/use-cases/frontend/UC-FE-98-accent-personalization.md new UC
docs/use-cases/frontend/UC-FE-08-nav.md, UC-FE-80-users-organizations.md, UC-FE-85-selenium-nav-smoke.md related: + ## Related link to UC-FE-98 (bidirectional)

Explicitly NOT touched: middleware (apps.ts, types.ts, any route), the database, the agent,
dashboard/src/lib/types.ts (no wire shape changes → type-mirror untouched), dashboard/src/routes/settings/+page.svelte
(the appearance modal stays exactly as shipped), dashboard/src/lib/components/aspa/theme/*, the
semantic state tokens --success / --warning / --destructive (they are state, not brand),
--chart-2..5, and plans/.

Design decision

One hue custom property, not a JS palette. --primary already fans out everywhere
(--ring, --info, --chart-1, --sidebar-primary, color-mix(in oklch, var(--primary) …)).
So the tint is expressed as --accent-hue and applied with a single
document.documentElement.style.setProperty('--accent-hue', '230'). Light/dark lightness and chroma
stays in app.css where it belongs, the CSS cascade does the theming, and the boot script needs no
palette knowledge. Setting the property is idempotent for the default 260 — the shipped token value
is reproduced exactly.

Consequence: dark --primary hue moves 262 → 260 (2°), imperceptible, so both themes share one knob.
Required by the single-knob design; recorded here rather than adding a second var.

Contrast is safe by construction for every hue 0–360, measured oklch → linear sRGB → WCAG relative
luminance: light --primary-foreground (oklch(0.99 0.005 260)) on oklch(0.52 0.12 H) bottoms out at
4.82:1 (H≈192, AA ≥ 4.5); dark --primary-foreground (oklch(0.16 0.03 262)) on oklch(0.76 0.10 H)
bottoms out at 8.64:1. Low-chroma hues (teal ~190, azure ~230) fall slightly outside sRGB at C=0.12;
the browser gamut-clamps them and the figure above is computed post-clamp, so it still holds.

Palette: 7 curated anchors, no free-hue slider. Indigo 260 (default), Azure 230, Teal 190,
Emerald 150, Amber 75, Rose 15, Violet 300. A slider needs a native <input type="range"> (no
installed component represents it) and adds a control that keeps the One Voice Rule harder to audit;
deferred, recorded in the report.

UC decision

New UC: docs/use-cases/frontend/UC-FE-98-accent-personalization.md, copied from _TEMPLATE.md
(UC-FE-98 is the next free frontend id; FE-09/FE-10 are in the graveyard).

  • Implementation: dashboard/src/routes/operator/personalization/+page.svelte,
    dashboard/src/lib/theme.ts setAccentHue, dashboard/src/lib/nav.ts NAV_GROUPS.
  • Tests: dashboard/src/lib/usecases-frontend.test.ts UC-FE-98.
  • related: [UC-FE-08, UC-FE-80, UC-FE-85], mirrored in the ## Related heading and back-linked from
    each of those three (the checker rejects one-way relations).

Ordered tasks

# Task Verify
1 Plan file written; post it as the Plan comment python3 $FJ comment 46 --file …/issue-46.plan.md exits 0
2 app.css: --accent-hue + 5 var() substitutions; theme.ts: accent model; app.html: boot read cd dashboard && bun run check
3 usecases-frontend.test.ts: accent tests (palette, storage round-trip, applyAccentHue writes --accent-hue, app.css declares the var and no longer hardcodes the hue) cd dashboard && bun run test
4 The page itself cd dashboard && bun run check
5 Wire-up: nav.ts, page-help.ts, destinations.ts, nav_selenium.py; fix the two operator href assertions cd dashboard && bun run test && bun run check
6 UC-FE-98 + bidirectional related on FE-08/FE-80/FE-85 python3 docs/use-cases/_check.py
7 Headers + all gates python3 scripts/check-ai-headers.py, check-type-mirror.py, rg -n 'Showing lines'
8 Browser smoke on /operator/personalization; report + Done comment manual observation

Open questions and the assumption taken

  1. /operator/* requires instanceOperator (operator/+layout.server.ts 303s others to
    /settings). The issue names that URL, so the page inherits the gate. Assumed intended — an
    instance-level personalization page. If it should be per-user instead, that is a one-line move to
    /settings/personalization; noted in the report.
  2. "the modal will keep showing system,dark and light" — read as the existing Settings appearance
    popup staying as-is, accent moving to the new page. Assumed; the new page does not carry a second
    theme selector.
  3. Per-browser, not per-account. No middleware storage for a cosmetic preference; localStorage,
    same as gotoi-theme. Assumed.
  4. Palette anchors vs free hue — anchors assumed sufficient for "like a tint".
  5. Dark hue 262 → 260 — a 2° shift required by the single-knob design. Assumed acceptable.

Risk and fallback

  • Gate risk: two usecases-frontend.test.ts assertions pin the exact operator href list and will
    go red the moment the nav item lands — updated in the same commit as the nav change (task 5), not
    after. pageHelp('/settings') appears in a test; its body is not asserted, but the copy stays.
  • Svelte risk: rule 17/18/21 — the new page is a single closed tree; verified with bun run check.
  • CSS risk: if oklch(… var(--accent-hue)) fails to parse in the check pipeline, the boring
    fallback is to keep app.css literals and set the five custom properties from theme.ts inline;
    the store and page API are unchanged, so the fallback is confined to theme.ts + one consumer.
  • app.css is read by three existing tests (control-chat.test.ts ×2, plan-editor.test.ts) for
    unrelated selectors — the substitutions here do not touch those selectors.
# Issue #46 — Accent colour: plan ## Restated goal Add `dashboard/src/routes/operator/personalization/+page.svelte` — the URL the issue names, `/operator/personalization` — where the operator picks the console's accent (a tint). The accent is a **hue** that recolours the shipped brand token `--primary` (plus the two tokens that must follow it in lockstep, `--accent` and `--sidebar-ring`), so chips, focus rings, links, the active nav pill, `--info`, and `--chart-1` all move with it — there is no second brand colour. The pick is stored in `localStorage` and re-applied before first paint, so a reload keeps the tint with no flash. The existing system/light/dark appearance modal is untouched and keeps its three options; this page adds the accent control and nothing else. "Done" for the operator: open the page, click a swatch, the whole console re-tints immediately, reload, still tinted, and a Reset returns it to Polar Indigo. ## Scope **Files changed (dashboard only, no middleware, no agent, no schema, no API):** | File | Change | |---|---| | `dashboard/src/app.css` | add `--accent-hue: 260` to `:root`; replace the hardcoded hue in the 5 tinted literals with `var(--accent-hue)` | | `dashboard/src/lib/theme.ts` | accent storage key, palette, store, `parseAccentHue` / `applyAccentHue` / `setAccentHue`; bump `Last` | | `dashboard/src/app.html` | boot IIFE also reads `gotoi-accent` and sets `--accent-hue` before first paint | | `dashboard/src/routes/operator/personalization/+page.svelte` | **new** page | | `dashboard/src/lib/nav.ts` | Operator item `Personalization` + `palette` icon in `ITEM_ICONS`; bump `Last` | | `dashboard/src/lib/page-help.ts` | `/operator/personalization` help copy; bump `Last` | | `dashboard/src/lib/dashboard/destinations.ts` | `['/operator/personalization', paletteIcon]` prefix | | `scripts/e2e/nav_selenium.py` | add the href to `NAV_HREFS` | | `dashboard/src/lib/usecases-frontend.test.ts` | update the two operator href assertions; add accent tests; bump `Last` | | `docs/use-cases/frontend/UC-FE-98-accent-personalization.md` | **new** UC | | `docs/use-cases/frontend/UC-FE-08-nav.md`, `UC-FE-80-users-organizations.md`, `UC-FE-85-selenium-nav-smoke.md` | `related:` + `## Related` link to UC-FE-98 (bidirectional) | **Explicitly NOT touched:** middleware (`apps.ts`, `types.ts`, any route), the database, the agent, `dashboard/src/lib/types.ts` (no wire shape changes → type-mirror untouched), `dashboard/src/routes/settings/+page.svelte` (the appearance modal stays exactly as shipped), `dashboard/src/lib/components/aspa/theme/*`, the semantic state tokens `--success` / `--warning` / `--destructive` (they are state, not brand), `--chart-2..5`, and `plans/`. ## Design decision **One hue custom property, not a JS palette.** `--primary` already fans out everywhere (`--ring`, `--info`, `--chart-1`, `--sidebar-primary`, `color-mix(in oklch, var(--primary) …)`). So the tint is expressed as `--accent-hue` and applied with a single `document.documentElement.style.setProperty('--accent-hue', '230')`. Light/dark lightness and chroma stays in `app.css` where it belongs, the CSS cascade does the theming, and the boot script needs no palette knowledge. Setting the property is idempotent for the default 260 — the shipped token value is reproduced exactly. Consequence: dark `--primary` hue moves 262 → 260 (2°), imperceptible, so both themes share one knob. Required by the single-knob design; recorded here rather than adding a second var. **Contrast is safe by construction** for every hue 0–360, measured oklch → linear sRGB → WCAG relative luminance: light `--primary-foreground` (`oklch(0.99 0.005 260)`) on `oklch(0.52 0.12 H)` bottoms out at **4.82:1** (H≈192, AA ≥ 4.5); dark `--primary-foreground` (`oklch(0.16 0.03 262)`) on `oklch(0.76 0.10 H)` bottoms out at **8.64:1**. Low-chroma hues (teal ~190, azure ~230) fall slightly outside sRGB at C=0.12; the browser gamut-clamps them and the figure above is computed post-clamp, so it still holds. **Palette: 7 curated anchors, no free-hue slider.** Indigo 260 (default), Azure 230, Teal 190, Emerald 150, Amber 75, Rose 15, Violet 300. A slider needs a native `<input type="range">` (no installed component represents it) and adds a control that keeps the One Voice Rule harder to audit; deferred, recorded in the report. ## UC decision New UC: `docs/use-cases/frontend/UC-FE-98-accent-personalization.md`, copied from `_TEMPLATE.md` (UC-FE-98 is the next free frontend id; FE-09/FE-10 are in the graveyard). - Implementation: `dashboard/src/routes/operator/personalization/+page.svelte`, `dashboard/src/lib/theme.ts` `setAccentHue`, `dashboard/src/lib/nav.ts` `NAV_GROUPS`. - Tests: `dashboard/src/lib/usecases-frontend.test.ts` UC-FE-98. - `related: [UC-FE-08, UC-FE-80, UC-FE-85]`, mirrored in the `## Related` heading and back-linked from each of those three (the checker rejects one-way relations). ## Ordered tasks | # | Task | Verify | |---|---|---| | 1 | Plan file written; post it as the Plan comment | `python3 $FJ comment 46 --file …/issue-46.plan.md` exits 0 | | 2 | `app.css`: `--accent-hue` + 5 `var()` substitutions; `theme.ts`: accent model; `app.html`: boot read | `cd dashboard && bun run check` | | 3 | `usecases-frontend.test.ts`: accent tests (palette, storage round-trip, `applyAccentHue` writes `--accent-hue`, `app.css` declares the var and no longer hardcodes the hue) | `cd dashboard && bun run test` | | 4 | The page itself | `cd dashboard && bun run check` | | 5 | Wire-up: `nav.ts`, `page-help.ts`, `destinations.ts`, `nav_selenium.py`; fix the two operator href assertions | `cd dashboard && bun run test && bun run check` | | 6 | UC-FE-98 + bidirectional `related` on FE-08/FE-80/FE-85 | `python3 docs/use-cases/_check.py` | | 7 | Headers + all gates | `python3 scripts/check-ai-headers.py`, `check-type-mirror.py`, `rg -n 'Showing lines'` | | 8 | Browser smoke on `/operator/personalization`; report + Done comment | manual observation | ## Open questions and the assumption taken 1. **`/operator/*` requires `instanceOperator`** (`operator/+layout.server.ts` 303s others to `/settings`). The issue names that URL, so the page inherits the gate. Assumed intended — an instance-level personalization page. If it should be per-user instead, that is a one-line move to `/settings/personalization`; noted in the report. 2. **"the modal will keep showing system,dark and light"** — read as the existing Settings appearance popup staying as-is, accent moving to the new page. Assumed; the new page does not carry a second theme selector. 3. **Per-browser, not per-account.** No middleware storage for a cosmetic preference; `localStorage`, same as `gotoi-theme`. Assumed. 4. **Palette anchors vs free hue** — anchors assumed sufficient for "like a tint". 5. **Dark hue 262 → 260** — a 2° shift required by the single-knob design. Assumed acceptable. ## Risk and fallback - **Gate risk:** two `usecases-frontend.test.ts` assertions pin the exact operator href list and will go red the moment the nav item lands — updated in the same commit as the nav change (task 5), not after. `pageHelp('/settings')` appears in a test; its body is not asserted, but the copy stays. - **Svelte risk:** rule 17/18/21 — the new page is a single closed tree; verified with `bun run check`. - **CSS risk:** if `oklch(… var(--accent-hue))` fails to parse in the check pipeline, the boring fallback is to keep `app.css` literals and set the five custom properties from `theme.ts` inline; the store and page API are unchanged, so the fallback is confined to `theme.ts` + one consumer. - **`app.css` is read by three existing tests** (`control-chat.test.ts` ×2, `plan-editor.test.ts`) for unrelated selectors — the substitutions here do not touch those selectors.
Collaborator

DONE — /operator/personalization ships; operators pick the accent tint, it persists, Reset returns Polar Indigo.

Commits (branch issue/46-accent-colour, on ea26498):

  • f44945f dashboard: accent hue token and browser-local accent store
  • 9f6730c dashboard: /operator/personalization accent page, nav, and tests
  • c5e0eb7 docs: UC-FE-98 accent personalization, linked from nav UCs
  • dca834a dashboard: accent copy names the tokens it actually tints
  • 7232cbb docs: index UC-FE-98 in the use-case README
  • 37da458 dashboard: describe the personalization page header precisely

Design: one hue custom property --accent-hue; app.css rebuilds --primary/--accent/--sidebar-ring from it, so --ring, --info, --chart-1, the soft wash, focus rings and links follow for free. Default 260 reproduces the shipped Polar Indigo exactly. Dark --primary hue moved 262 → 260 (2°) so one knob serves both themes. Pick is browser-local (localStorage), applied before first paint from app.html.

Gates:

  • python3 docs/use-cases/_check.py — pass (356 UC files, all conform)
  • python3 scripts/check-ai-headers.py — pass (572 ok, 0 missing, 0 malformed)
  • python3 scripts/check-type-mirror.py — pass (exit 0)
  • cd dashboard && bun run test — pass (16 files, 249 tests)
  • cd dashboard && bun run check — pass (2346 files, 0 errors, 7 pre-existing warnings)
  • cd dashboard && bun run build — pass (adapter-node, exit 0)
  • rg -n 'Showing lines' — only pre-existing doc quotes; no source file
  • cd middleware && bun test — RED, pre-existing, unrelated: my diff touches 0 middleware files (git diff --name-only ea26498 HEAD -- middleware empty). UC-MW-19 fails 201/500 because the gitignored middleware/data/plans/restart-nginx.git left on disk makes POST /plans {id:'restart-nginx'} hit an already-initialized repo ("nothing to commit"). Removing middleware/data/ → 10 pass / 0 fail, but the run recreates it, so it is red again next time. Reproduces on the untouched branch base.
  • cd agent && go test ./... — not run (no agent file touched); gofmt -l agent empty.

Browser smoke (real Chromium, worktree dev server → middleware): fresh browser shows no inline override and --primary: oklch(0.52 0.12 260), Reset disabled; click Azure → inline 230, computed --primary: oklch(0.52 0.12 230), reload keeps 230 with no flash and no console/hydration errors; Reset removes the inline property; dark theme keeps its own lightness/chroma (oklch(0.76 0.10 230)). Settings still offers System/Light/Dark.

Assumptions to confirm: /operator/* is instance-operator gated so the page inherits that gate (a move to /settings/personalization is one line if it should be per-user); "the modal keeps showing system,dark and light" read as the Settings appearance control staying exactly as shipped.

Refs #46

DONE — `/operator/personalization` ships; operators pick the accent tint, it persists, Reset returns Polar Indigo. Commits (branch `issue/46-accent-colour`, on `ea26498`): - f44945f dashboard: accent hue token and browser-local accent store - 9f6730c dashboard: /operator/personalization accent page, nav, and tests - c5e0eb7 docs: UC-FE-98 accent personalization, linked from nav UCs - dca834a dashboard: accent copy names the tokens it actually tints - 7232cbb docs: index UC-FE-98 in the use-case README - 37da458 dashboard: describe the personalization page header precisely Design: one hue custom property `--accent-hue`; app.css rebuilds `--primary`/`--accent`/`--sidebar-ring` from it, so `--ring`, `--info`, `--chart-1`, the soft wash, focus rings and links follow for free. Default 260 reproduces the shipped Polar Indigo exactly. Dark `--primary` hue moved 262 → 260 (2°) so one knob serves both themes. Pick is browser-local (`localStorage`), applied before first paint from `app.html`. Gates: - `python3 docs/use-cases/_check.py` — pass (356 UC files, all conform) - `python3 scripts/check-ai-headers.py` — pass (572 ok, 0 missing, 0 malformed) - `python3 scripts/check-type-mirror.py` — pass (exit 0) - `cd dashboard && bun run test` — pass (16 files, 249 tests) - `cd dashboard && bun run check` — pass (2346 files, 0 errors, 7 pre-existing warnings) - `cd dashboard && bun run build` — pass (adapter-node, exit 0) - `rg -n 'Showing lines'` — only pre-existing doc quotes; no source file - `cd middleware && bun test` — **RED, pre-existing, unrelated**: my diff touches 0 middleware files (`git diff --name-only ea26498 HEAD -- middleware` empty). `UC-MW-19` fails `201/500` because the gitignored `middleware/data/plans/restart-nginx.git` left on disk makes `POST /plans {id:'restart-nginx'}` hit an already-initialized repo ("nothing to commit"). Removing `middleware/data/` → 10 pass / 0 fail, but the run recreates it, so it is red again next time. Reproduces on the untouched branch base. - `cd agent && go test ./...` — not run (no agent file touched); `gofmt -l agent` empty. Browser smoke (real Chromium, worktree dev server → middleware): fresh browser shows no inline override and `--primary: oklch(0.52 0.12 260)`, Reset disabled; click Azure → inline `230`, computed `--primary: oklch(0.52 0.12 230)`, reload keeps 230 with no flash and no console/hydration errors; Reset removes the inline property; dark theme keeps its own lightness/chroma (`oklch(0.76 0.10 230)`). Settings still offers System/Light/Dark. Assumptions to confirm: `/operator/*` is instance-operator gated so the page inherits that gate (a move to `/settings/personalization` is one line if it should be per-user); "the modal keeps showing system,dark and light" read as the Settings appearance control staying exactly as shipped. Refs #46
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
EduarD3V/gotoi#46
No description provided.