feat(theme): two-layer theme + accent picker, app-wide (MAPPS-259) #262

Merged
vas2000-work merged 3 commits from feat/MAPPS-259-theme-picker into main 2026-06-17 22:37:11 +02:00
Owner

Implements MAPPS-259: a two-layer theme picker for the entire Mokosh SPA.

What it adds (3 commits):

  1. Foundation - a semantic CSS-variable token layer mapped through Tailwind v4 @theme inline (bg-app/surface/surface-2/raised, text-content/muted/subtle, border-line, bg-accent/text-on-accent, bg-accent-{50..950}); a curated 14-accent catalog with per-base fill/on-accent and WCAG contrast math (tests assert AA on both light and dark bases); a shared ThemePicker (base mode Light/Dark/System + accent grid with a contrast lock + live preview); a header swatch modal next to the bell; a Settings > Appearance page; and the old profile Light/Dark radios superseded.
  2. Migration - ~37 SPA files moved off hardcoded neutral grays and the brand blue to the semantic tokens, so a theme/accent change recolors the whole app live (nav chrome follows base mode, active nav uses the accent). Adds scripts/check-theme-tokens.sh + a just check-theme-tokens guard (wired into just check) that fails on new hardcoded neutral color classes; semantic state colors (red/green/yellow/amber) and a few intentional exceptions are allow-listed.
  3. Account sync - per-user theme persistence: on first authenticated mount it pulls theme_base_mode + theme_accent_id from /auth/me and reconciles into localStorage (server wins), then re-applies; the picker pushes changes back fire-and-forget. localStorage stays the flash-free boot cache.

Phase 2 (named palettes like Sepia/Midnight/Forest) is deferred and purely additive on the same token layer.

Verified in the dev container: cargo check --target wasm32-unknown-unknown, cargo test (146 passed, incl the accent AA suite), cargo clippy --all-targets -D warnings, just css-build, and the theme-token guard.

Depends on mokosh-server PMS-410 (PR !301) for the per-user fields; merge/deploy that first so the account sync has somewhere to read/write.

#MAPPS-259

Implements MAPPS-259: a two-layer theme picker for the entire Mokosh SPA. What it adds (3 commits): 1. Foundation - a semantic CSS-variable token layer mapped through Tailwind v4 @theme inline (bg-app/surface/surface-2/raised, text-content/muted/subtle, border-line, bg-accent/text-on-accent, bg-accent-{50..950}); a curated 14-accent catalog with per-base fill/on-accent and WCAG contrast math (tests assert AA on both light and dark bases); a shared ThemePicker (base mode Light/Dark/System + accent grid with a contrast lock + live preview); a header swatch modal next to the bell; a Settings > Appearance page; and the old profile Light/Dark radios superseded. 2. Migration - ~37 SPA files moved off hardcoded neutral grays and the brand blue to the semantic tokens, so a theme/accent change recolors the whole app live (nav chrome follows base mode, active nav uses the accent). Adds scripts/check-theme-tokens.sh + a just check-theme-tokens guard (wired into just check) that fails on new hardcoded neutral color classes; semantic state colors (red/green/yellow/amber) and a few intentional exceptions are allow-listed. 3. Account sync - per-user theme persistence: on first authenticated mount it pulls theme_base_mode + theme_accent_id from /auth/me and reconciles into localStorage (server wins), then re-applies; the picker pushes changes back fire-and-forget. localStorage stays the flash-free boot cache. Phase 2 (named palettes like Sepia/Midnight/Forest) is deferred and purely additive on the same token layer. Verified in the dev container: cargo check --target wasm32-unknown-unknown, cargo test (146 passed, incl the accent AA suite), cargo clippy --all-targets -D warnings, just css-build, and the theme-token guard. Depends on mokosh-server PMS-410 (PR !301) for the per-user fields; merge/deploy that first so the account sync has somewhere to read/write. #MAPPS-259
Add a semantic CSS-variable token layer in input.css mapped through Tailwind v4 `@theme inline` (bg-surface, bg-surface-2, text-content, text-muted, border-line, bg-accent, text-on-accent, ring-accent, bg-accent-{50..950}); light values on :root, dark on .dark, default Teal accent. Components now read tokens instead of hardcoded colors.

Add modules/theme: a curated 14-accent catalog (canonical ramp + per-base fill/on-accent) plus WCAG contrast math. Tests assert every accent meets AA on both light and dark bases, so the picker's auto-fit-shade guarantee holds and no curated swatch locks.

Extend hooks/theme.rs to persist and apply the accent (inject the ramp/fill/on-accent as inline <html> CSS vars; base surfaces stay class-driven), with current_accent/set_accent/current_is_dark.

Add the shared ThemePicker component (segmented base mode, locked phase-2 palette tiles, accent grid with a runtime contrast lock, live preview) and a SwatchIcon. Surface it from a top-bar swatch modal next to the bell and from a new Settings > Appearance page (Route::SettingsAppearance); supersede the profile Light/Dark radios with a pointer to the picker.

Base mode is Light/Dark/System now; named palettes are deferred (phase 2). Per-user account sync of the preference is tracked separately (PMS-410). The ~38-file component migration off hardcoded colors and a lint guard follow in subsequent commits.

#MAPPS-259

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate ~37 SPA files (~510 class strings) from hardcoded neutral grays and the brand blue to the semantic token utilities (bg-app, bg-surface, bg-surface-2, bg-raised, text-content, text-muted, text-subtle, border-line, bg-accent, text-on-accent, text-accent, border-accent, ring-accent, bg-accent-{50..950}), so a theme or accent change recolors the entire app live. The nav chrome (sidebar + top bar) now follows the base mode and the active nav indicator uses the accent.

Migrate the shared style layer in input.css to tokens too: .card, .btn-primary/secondary, .badge-blue/gray, .table-header/cell, form inputs, the focus ring, and the global scrollbars.

Keep semantic state colors (red danger, green success, yellow/amber/orange warning), status and data-viz palettes, overlay scrims, and the public marketing hero as intentional exceptions. The few genuine neutral exceptions (event-type dot palette, a neutral status-bar fill, the two overlay scrims, the marketing CTAs) are annotated `// theme-guard-allow`.

Add scripts/check-theme-tokens.sh and a `just check-theme-tokens` recipe (wired into `just check`) that fails on new hardcoded neutral color classes outside the allowlist, so coverage stays complete over time.

Verified in the dev container: cargo check (wasm32), cargo test (146 passed), cargo clippy --all-targets -D warnings, just css-build, and the token guard all pass.

#MAPPS-259

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(theme): sync per-user theme + accent to the account (MAPPS-259)
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 2m37s
Create release / Create release from merged PR (pull_request) Has been skipped
80737c79ef
Add hooks/theme_sync.rs. On the first authenticated mount it pulls theme_base_mode and theme_accent_id from GET /auth/me and reconciles them into localStorage (server wins), then re-applies, so the choice follows the user across devices. Whenever the picker changes the base mode or accent it pushes them to PUT /auth/me fire-and-forget; a failure is logged, never blocking, because localStorage already holds the value and remains the flash-free boot cache.

Wire use_theme_sync() at the app root next to use_apply_theme(), and call theme_sync::save_to_account() from the picker's base-mode and accent handlers. Reconcile uses theme::set / theme::set_accent (local only), so there is no save loop.

The backend fields these read and write land in PMS-410 (mokosh-server), reached through the same /auth/me endpoint the profile screen already uses.

#MAPPS-259

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
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
psa-systems/mokosh-apps!262
No description provided.