---
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 ~85–90% 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 4–5. 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` | ✅ Phase 4 |
| `LumotiaStatusPill` | `StatusPill.svelte` | ✅ Phase 4 |
| `LumotiaToggle` | `Toggle.svelte` | ✅ Phase 4 |
| `LumotiaSettingsGroup` | `SettingsGroup.svelte` | ✅ Phase 4 |
| `LumotiaEmptyState` | `EmptyState.svelte` | ✅ Phase 4 |
| `LumotiaPostCaptureCard` | `PostCaptureCard.svelte` | ✅ Phase 4 |

### 6.2 Phase 5 new primitives

| Wrapper | Implementation | Status |
|---|---|---|
| `LumotiaButton` | native + `.btn-*` class system; primary / secondary / tertiary / destructive | ✅ Phase 5 |
| `LumotiaIconButton` | native + lucide-svelte; standard sizing + tooltip slot | ✅ Phase 5 |
| `LumotiaNotice` | custom; info / caution / danger / success inline notices | ✅ Phase 5 |
| `LumotiaProgress` | native `<progress>` + tokens; fall back to `role="progressbar"` only if native styling proves inconsistent | ✅ Phase 5 (native `<progress>` held up; no fallback needed) |
| `LumotiaField` | native + Formsnap Field integration; text + textarea + label + error wrapper | ✅ Phase 5 |
| `LumotiaSelect` | Bits UI Select | ✅ Phase 5 |
| `LumotiaCombobox` | Bits UI Combobox | ✅ Phase 5 |
| `LumotiaDialog` | Bits UI Dialog | ✅ Phase 5 |
| `LumotiaTabs` | Bits UI Tabs | ✅ Phase 5 |
| `LumotiaTooltip` | Bits UI Tooltip | ✅ Phase 5 |
| `LumotiaMenu` (DropdownMenu) | Bits UI DropdownMenu | ✅ Phase 5 (Popover deferred — DropdownMenu covers the v0.2 use cases) |

### 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 | ✅ complete | Six wrapper aliases under `src/lib/ui/`: Lumotia{Card,StatusPill,Toggle,SettingsGroup,EmptyState,PostCaptureCard}. Same prop APIs, $bindable forwarded for Toggle. Underlying `src/lib/components/*.svelte` untouched. |
| 5 | ✅ complete | 11 primitives shipped under `src/lib/ui/`. `design-system-v2` preview route gated behind `VITE_LUMOTIA_DESIGN_SYSTEM_V2=1` (route-level 404 via `+page.ts` load, not nav-hidden). Browser-mode component test (LumotiaButton): 3/3 passing in Chromium. Gate green (check 0/0/5700 files, vitest 0/0, test:browser 3/3, e2e 16/16). |
| 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/`.
