Files
Lumotia/docs/release/v0.2-frontend-overhaul.md
Jake 66e25aa778 v0.2 Phase 3: additive semantic tokens + KI-05 resolution
Additive token grammar (no renames, no replacements):

- --color-caution (dark #e8be4a, light #a08a1f) becomes the canonical
  name for tuned-amber notice surfaces in the new wrapper grammar
- --color-warning is kept as a CSS var() alias of --color-caution so
  every existing text-warning / bg-warning call site stays valid
- --color-info (dark #7a9ec0, light #3d6a8a) is the soft blue-grey
  signal for the new LumotiaNotice info variant
- --color-accent-environment (dark #8fae9a, light #4a7058) is an
  optional sage/moss support token for empty-state illustrations
  and environment-neutral status dots. NOT a brand swap — amber/
  copper --color-accent stays primary

Mirrored in src/design-system/colors_and_type.css (the buildless
preview pages bypass Tailwind so the duplication is intentional).

KI-05 resolved in the same commit, per the plan:

- src/lib/types/app.ts: drop `theme` from SettingsState
- src/lib/stores/page.svelte.ts: drop `theme: "Dark"` from defaults
- src/routes/+layout.svelte: drop the migration $effect, add a
  one-shot migrateLegacyTheme() on mount that copies any historical
  lumotia_settings.theme into preferences.theme and strips the
  legacy field. Idempotent — subsequent loads short-circuit
- src/routes/{float,viewer,preview}/+layout@.svelte: drop the same
  $effect; secondary windows inherit theme via PREFERENCES_CHANGED_EVENT
- src/lib/pages/SettingsPage.svelte: both SegmentedButton bindings
  (quick-settings row at :1272, Appearance group at :2606) now use
  Svelte 5 function bindings ({ get, set }) backed by prefs.theme
  and updatePreferences. No SegmentedButton API change

Phase 3 per-page gate green: npm run check (0/0/4129 files),
npm test (13/13), npm run test:e2e (16/16). No regressions in
the Phase 1 smoke baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:35:54 +01:00

262 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: v0.2-frontend-overhaul
type: release
tags: [release, v0.2, frontend, coherence-pass, no-skeleton, bits-ui, formsnap]
description: "v0.2 frontend coherence pass. NOT a redesign. Lands one button grammar, one status grammar, one notice/error grammar, one settings-row grammar across every page without disturbing the brand, identity surfaces, or token system. Tooling-first: Playwright + axe + @vitest/browser + rollup-plugin-visualizer + cargo-nextest installed before any UI work. Long-lived feat/v0.2-frontend-overhaul branch; main keeps shipping v0.1.x bugfixes. Ships as v0.2."
---
# Lumotia v0.2 — frontend overhaul (coherence pass, tooling-first)
**Source of truth.** All implementation, gate, and decision detail for the v0.2 frontend overhaul lives in this single file. Per-page status, resolved tooling pins, sacred-behaviour test list, wrapper catalogue, and verification matrix are all tracked here.
## 1. Why — coherence pass, not redesign
The current UI is already ~8590% cohesive: token-driven warm-amber dark system, named shadow scale, 5 bundled fonts, sensory-zone CSS, per-region accessibility controls. The gap is **grammar inconsistency** — one button style here, another there; one notice pattern in Files, a different one in History; ~95 ad-hoc form controls in `SettingsPage`. The fix is to land one button grammar, one status grammar, one notice/error grammar, one settings-row grammar — across every page — **without disturbing the brand or the bespoke identity surfaces.**
**Aesthetic target.** Warm brutalist notebook cockpit. Calm, local, tactile, low-noise. Not SaaS, not garden-game, not generic Tailwind, not a wholesale sage rebrand.
**Tooling is Phase 1, not an afterthought.** Verification and performance tooling are installed before any UI work. If a tool is needed, install/configure it instead of simulating the check manually.
## 2. Hard rules
1. **Do not install Skeleton.**
2. **Do not replace Lumotia's token system or brand direction.** Amber/copper stays primary `--color-accent`; sage/moss enters as an optional support token only.
3. **Do not rewrite bespoke identity surfaces:** recording controls, waveform/timer, bionic transcript surfaces, FocusTimer, MicroSteps, TaskSidebar, ModelDownloader, HotkeyRecorder, ZonePicker.
4. **Use exact-pinned packages.**
5. **If a tool is needed, install/configure it instead of simulating the check manually.**
## 3. Tooling baseline (resolved pins)
Installed in Phase 1 of this overhaul. Pinned exactly via `--save-exact`.
| Package | Version | Role |
|---|---|---|
| `@playwright/test` | 1.60.0 | E2E test runner |
| `playwright` | 1.60.0 | Vitest browser-mode peer dep |
| `@axe-core/playwright` | 4.11.3 | Accessibility scan inside Playwright |
| `rollup-plugin-visualizer` | 7.0.1 | Bundle-size analyser (ANALYZE=1 vite build) |
| `@vitest/browser` | 4.1.6 | Vitest browser-mode core |
| `@vitest/browser-playwright` | 4.1.6 | Vitest browser-mode provider |
| `vitest-browser-svelte` | 2.1.1 | Svelte 5 runes-aware bridge |
| `cargo-nextest` | latest (cargo install) | Fast Rust test runner |
| `bits-ui` | 2.18.1 | Headless Svelte 5 primitives |
| `formsnap` | 2.0.1 | Form field + label + error wrapper |
| `sveltekit-superforms` | 2.30.1 | Form state + validation |
| `zod` | 4.4.3 | Schema validation |
| `@internationalized/date` | 3.12.1 | Bits UI peer dep |
`npm run` scripts added: `test:e2e`, `test:e2e:ui`, `test:browser`, `analyze`, `test:rust:fast`, `guard:no-skeleton`.
Vite plugin: `rollup-plugin-visualizer` registered behind `ANALYZE=1` env flag in `vite.config.js`; emits `reports/bundle-stats.html`.
Tauri-IPC acceptance rule: frontend-only Playwright tests must not require Tauri IPC. Any Tauri-only feature gets either a graceful browser-preview fallback (mock the invoke boundary in dev) or is skipped in Playwright with a documented reason and verified manually + by `cargo test` instead.
## 4. Stack additions vs. existing
Additive only. No replacements.
- Existing: Svelte 5.53.12, Vite 6.4.2, SvelteKit 2.58.0, Tauri 2.10.1, Tailwind 4.2.1, vitest 4.1.6 (jsdom).
- Added: Playwright + axe-core (E2E), `@vitest/browser` + `vitest-browser-svelte` (component-mode browser tests), bits-ui + formsnap + superforms + zod (headless primitives + form layer), rollup-plugin-visualizer (build-time only), cargo-nextest (Rust test runner).
Superforms runs client-side only (Tauri uses static adapter; no SvelteKit server actions). Zod runs in-process. Bits UI's Floating-UI portals target `document.body`; the `/preview` window uses **zero** portaled primitives to keep its `WindowTypeHint::Utility` DOM flat.
## 5. Sacred behaviours (contract tests)
Each becomes a Playwright or `@vitest/browser` test. Code may move during the overhaul (notably during Phase 6's shell split), but the **behaviour must remain verbatim**.
| # | Behaviour | Source today | Moves to | Test approach |
|---|---|---|---|---|
| 1 | Recording-state nav fade | `src/lib/Sidebar.svelte:108-157` | `AppChrome.svelte` | Playwright: simulate recording, screenshot + ARIA assert + axe scan |
| 2 | Global hotkey + 120ms debounce | `+layout.svelte:70-198` | `AppRuntime.svelte` | Browser-mode covers frontend debounce (via extracted `hotkeyDebounce.ts`); OS registration covered by `cargo test` |
| 3 | `+layout@.svelte` secondary windows skip shell | `routes/{float,viewer,preview}/+layout@.svelte` | unchanged | Playwright: navigate to `/float`, assert no sidebar/titlebar |
| 4 | Cross-window preference sync (`PREFERENCES_CHANGED_EVENT`) | `+layout.svelte:296-305` | `AppRuntime.svelte` | Browser-mode: emit event, assert listener fires |
| 5 | 5-font system (woff2 bundled, font-family wiring) | `src/app.css:9-47` | unchanged | Playwright: cycle all 5 fonts, screenshot transcript surface each |
| 6 | Bionic reading mode | `src/lib/actions/bionicReading.ts` | unchanged | Component test: action mounts, transforms text |
| 7 | Per-region accessibility controls (`--font-size-body`, `--letter-spacing-body`, `--line-height-body`) | `src/lib/utils/accessibilityTypography.ts` | unchanged | Component test: change pref, assert CSS vars on root |
| 8 | `prefers-reduced-motion` | `src/app.css:510-530` + `data-reduce-motion="true"` | unchanged | Playwright: set `prefers-reduced-motion: reduce`, assert no fade transitions |
| 9 | Sensory-zone tinting (cave/energy/reset) | `:root[data-zone="…"]` overrides | unchanged | Playwright: cycle 3 zones × 2 themes = 6 surface sets |
| 10 | Sidebar hotkeys (`[`, `Ctrl+K`, `Ctrl+,`) | `+layout.svelte` handleKeydown | `AppRuntime.svelte` | Browser-mode: press each, assert state change |
## 6. Wrapper catalogue
Filled progressively during Phases 45. Wrappers live in `src/lib/ui/`; bespoke identity surfaces stay in `src/lib/components/`.
### 6.1 Phase 4 alias wrappers (thin, same props)
| Wrapper | Wraps | Status |
|---|---|---|
| `LumotiaCard` | `Card.svelte` | pending |
| `LumotiaStatusPill` | `StatusPill.svelte` | pending |
| `LumotiaToggle` | `Toggle.svelte` | pending |
| `LumotiaSettingsGroup` | `SettingsGroup.svelte` | pending |
| `LumotiaEmptyState` | `EmptyState.svelte` | pending |
| `LumotiaPostCaptureCard` | `PostCaptureCard.svelte` | pending |
### 6.2 Phase 5 new primitives
| Wrapper | Implementation | Status |
|---|---|---|
| `LumotiaButton` | native + `.btn-*` class system; primary / secondary / tertiary / destructive | pending |
| `LumotiaIconButton` | native + lucide-svelte; standard sizing + tooltip slot | pending |
| `LumotiaNotice` | custom; info / caution / danger / success inline notices | pending |
| `LumotiaProgress` | native `<progress>` + tokens; fall back to `role="progressbar"` only if native styling proves inconsistent | pending |
| `LumotiaField` | native + Formsnap Field integration; text + textarea + label + error wrapper | pending |
| `LumotiaSelect` | Bits UI Select | pending |
| `LumotiaCombobox` | Bits UI Combobox | pending |
| `LumotiaDialog` | Bits UI Dialog | pending |
| `LumotiaTabs` | Bits UI Tabs | pending |
| `LumotiaTooltip` | Bits UI Tooltip | pending |
| `LumotiaMenu` / `LumotiaPopover` | Bits UI DropdownMenu / Popover | pending |
### 6.3 Bespoke surfaces — DO NOT wrap, DO NOT rewrite
These are Lumotia's identity. Pages import them; they stay outside `src/lib/ui/`:
- Record controls + recording-state UI on `DictationPage`
- Waveform / `VisualTimer` SVG ring
- Bionic transcript surfaces (`use:bionic` action consumers)
- `FocusTimer`
- `MicroSteps`
- `TaskSidebar`
- `ModelDownloader`
- `HotkeyRecorder`
- `ZonePicker`
- `CompletionSparkline`
- `EnergyChip`, `LlmStatusChip`, `SpeakerButton`, `UnicodeSpinner`, `ResizeHandles`
If a page using one of these needs grammar normalisation, normalise the surrounding chrome (buttons, notices, cards), not the identity component.
## 7. Per-page migration checklist
Order: smallest → riskiest. Dictation **before** Settings so DictationPage sets the grammar Settings inherits.
| # | Page | LOC | Notes | Status |
|---|---|---|---|---|
| 1 | `ShutdownRitualPage` | 169 | First smoke test | pending |
| 2 | `FilesPage` | 286 | Bank LumotiaField + LumotiaNotice patterns | pending |
| 3 | `FirstRunPage` | 461 | Formsnap proof point; LumotiaTabs stepper | pending |
| 4 | `TasksPage` | 726 | Wrap `WipTaskList` / `TaskSidebar` / `MicroSteps`, don't rewrite | pending |
| 5 | `HistoryPage` | 1 225 | FTS5 search → LumotiaCombobox; row patterns standardised; row actions via LumotiaMenu | pending |
| 6 | `DictationPage` | 1 263 | **Centrepiece**. Recording state machine + post-capture card + hotkey wiring stay verbatim; only surrounding chrome migrates | pending |
| 7 | `SettingsPage` | 2 791 | **Last**. Section by section. Existing IA stays. Formsnap only where validation/error semantics matter | pending |
| 8 | `/float` window | — | TaskList in frameless window | pending |
| 9 | `/viewer` window | — | Bionic-reading action + font tokens load-bearing | pending |
| 10 | `/preview` window | — | Wayland-hardened; zero portaled primitives | pending |
After **each** page migration, run the full per-page gate:
```
npm run check
npm run test
npm run test:browser
npm run test:e2e
npm run test:rust:fast
```
Per-page commit shape: page end-to-end + dead components deleted (if no remaining consumers) + the gate above green + `design-system-v2` preview updated if the page exposed a new wrapper pattern.
## 8. Phase 8 verification matrix (before squash-merge)
```
cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo nextest run --workspace
npm run check
npm run test
npm run test:browser
npm run test:e2e
npm run analyze # emits reports/bundle-stats.html
scripts/dogfood-rebrand-drill.sh
npm run guard:no-skeleton
rg "from ['\"]\$lib/components" src/lib/pages src/routes # informational only
```
`guard:no-skeleton` fails loudly if `@skeletonlabs` appears in `package.json`, `package-lock.json`, or anywhere under `src/`. Component-import `rg` is informational only at this stage: a future small script can allowlist exact paths and make this guard failing once the bespoke list is fully settled.
Cross-platform CI green — `.github/workflows/check.yml` + `build.yml` pass on Linux/macOS/Windows.
Visual baseline approval is **deferred**: after the UI stabilises and Jake approves screenshots, a separate follow-up commit promotes the baselines and only then do Playwright visual regressions fail the build.
## 9. KI-05 resolution
Drop `settings.theme` writes; `preferences.theme` is canonical. Resolved in the same commit as Phase 3 semantic-alias token additions. Touches:
- `src/lib/stores/page.svelte.ts` — drop `theme` from `SettingsState`
- `src/lib/pages/SettingsPage.svelte:1118`, `:2360` — repoint bindings to `preferences.theme` with mapped options
- `src/routes/+layout.svelte:61-68` and the three `+layout@.svelte` — retire the migration `$effect`
- One-shot localStorage migration that copies any historical `settings.theme` into `preferences` on first run after the cleanup
## 10. Aesthetic direction — warm brutalist notebook cockpit
Calm, local, tactile, low-noise. Warm amber/copper accents; charcoal/cream surfaces; named shadow scale; bundled woff2 typography. Generous whitespace, deliberate corners, no SaaS gloss. Every page migration is reviewed against this vibe; if a page starts feeling SaaS-bland during a wrapper sweep, stop and re-grain.
## 11. Phase log
Filled during execution. Each entry: phase #, date, what landed, what's next.
| Phase | Status | Notes |
|---|---|---|
| 0 | ✅ complete | Doc written; tooling pins recorded |
| 1 | ✅ complete | Tooling baseline green: `npm run check` clean, `npm test` clean, `npm run test:e2e` 16/16, `npm run guard:no-skeleton` clean, 10 screenshot artefacts. Browser-preview OS detection fixed (see Regression diary). |
| 2 | ✅ complete | bits-ui 2.18.1, formsnap 2.0.1, sveltekit-superforms 2.30.1, zod 4.4.3, @internationalized/date 3.12.1. `npm audit signatures` 273 verified + 93 attestations. |
| 3 | ✅ complete | New tokens `--color-caution`, `--color-info`, `--color-accent-environment` (dark + light), `--color-warning` aliased to `var(--color-caution)`. KI-05 resolved: `theme` dropped from SettingsState type + defaults, all four route-layout migration `$effect`s deleted, two SettingsPage SegmentedButton bindings repointed to `preferences.theme` via Svelte 5 function bindings, one-shot legacy-theme migration on first mount strips the field after copying. Gate green (check 0/0, vitest 13/13, e2e 16/16). |
| 4 | pending | Wrapper alias layer |
| 5 | pending | New primitives + design-system-v2 preview |
| 6 | pending | Shell split |
| 7.1 ShutdownRitualPage | pending | |
| 7.2 FilesPage | pending | |
| 7.3 FirstRunPage | pending | |
| 7.4 TasksPage | pending | |
| 7.5 HistoryPage | pending | |
| 7.6 DictationPage | pending | |
| 7.7 SettingsPage | pending | |
| 7.8 /float | pending | |
| 7.9 /viewer | pending | |
| 7.10 /preview | pending | |
| 8 | pending | Full release gate |
## 12. Bundle-size delta
To be filled after Phase 8 from `reports/bundle-stats.html`. Compare against pre-overhaul baseline captured in Phase 1.
## 13. Regression diary
Anything that broke during a migration and how it was fixed. Appended chronologically.
### Phase 1 — browser-preview OS misdetection (Titlebar crash)
**Symptom.** Playwright smoke test `app loads without Tauri runtime` failed with `Cannot read properties of undefined (reading 'metadata') in $effect in Titlebar.svelte`.
**Root cause (two compounding bugs):**
1. `src/lib/utils/osInfo.ts` defined `FALLBACK_BROWSER_INFO` at module-load time. Under SvelteKit SSR, `navigator` is undefined, so `detectBrowserOs()` froze at `'unknown'` and `isLinux()` returned false even on Linux browsers.
2. Inside `detectBrowserOs()`, the UA was consulted before `navigator.platform`. Playwright's Chromium ships with a **Windows** UA on Linux runners (`Mozilla/5.0 (Windows NT 10.0; Win64; x64)`), so the UA check returned `'windows'` first, overriding the real `Linux x86_64` platform string.
**Fix.**
- `osInfo.ts` — replace the module-level constant with a lazy `buildBrowserFallback()` that runs at call-time. Re-order `detectBrowserOs()` to read `navigator.platform` first (the truthful OS surface, immune to UA spoofing) and only fall back to UA when platform is unknown.
- `Titlebar.svelte` — defensive `hasTauriRuntime()` guard on all four handlers and the `$effect`. `Titlebar` should never crash if a future code path mounts it without a Tauri runtime; the underlying `getCurrentWindow().metadata` is undefined in plain browsers.
**Why this lived in v0.1.** The browser-preview path (`npm run dev:frontend`, no Tauri) had never been exercised under headless Chromium with a spoofed UA — Lumotia's dogfood loop runs via `run.sh` which always has the Tauri runtime, so neither bug surfaced.
## 14. v0.2 release-notes excerpt
To be filled at Phase 8.
## 15. Known risks (live)
- **DictationPage migration before Settings.** Mitigation: only chrome around the record state machine moves to wrappers; state machine + hotkey integration stay verbatim. Per-page gate runs immediately after.
- **SettingsPage scale (2 791 LOC, ~95 controls).** Mitigation: section-by-section commits, existing IA preserved, Formsnap selectively.
- **Bits UI portals vs Tauri `/preview`.** Mitigation: `/preview` uses zero portaled primitives — only `LumotiaCard`, `LumotiaStatusPill`, `LumotiaButton`.
- **Token-name churn breaks mid-migration.** Mitigation: existing token names are stable API; Phase 3 is additive only.
- **Sensory-zone CSS combinatorics (3 zones × 2 themes = 6 surface sets).** Mitigation: Playwright tests cycle all 6 in Phase 1.
- **Skeleton temptation.** Mitigation: §16 below.
- **`@chenglou/pretext` dep in `package.json`** — verify still used; remove if not, during Phase 2.
- **KI-05 dual-theme fix.** Mitigation: small, contained edit; resolved in the same commit as Phase 3.
- **Identity drift.** Mitigation: every page migration reviewed against the "warm brutalist notebook cockpit" vibe.
- **Playwright on Wayland.** `npx playwright install --with-deps chromium` may need extra Linux libs on Fedora. Mitigation: `--with-deps` flag in Phase 1.
- **`@vitest/browser` Svelte 5 compat.** Mitigation: `vitest-browser-svelte@2.1.1` is the runes-aware bridge.
## 16. DO NOT add Skeleton
Future agents reading "frontend overhaul" may reach for Skeleton. **Do not.** The plan and this doc are explicit: Lumotia's token system and identity are non-negotiable. Bits UI + Formsnap + Superforms is the headless-primitive path. `npm run guard:no-skeleton` fails CI if `@skeletonlabs` appears anywhere in `package.json`, `package-lock.json`, or `src/`.