--- 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 `` + tokens; fall back to `role="progressbar"` only if native styling proves inconsistent | ✅ Phase 5 (native `` 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 | ✅ Phase 7 | | 2 | `FilesPage` | 286 | Bank LumotiaField + LumotiaNotice patterns | ✅ Phase 7 | | 3 | `FirstRunPage` | 461 | Formsnap proof point; LumotiaTabs stepper | ✅ Phase 7 | | 4 | `TasksPage` | 726 | Wrap `WipTaskList` / `TaskSidebar` / `MicroSteps`, don't rewrite | ✅ Phase 7 | | 5 | `HistoryPage` | 1 225 | FTS5 search → LumotiaCombobox; row patterns standardised; row actions via LumotiaMenu | ✅ Phase 7 | | 6 | `DictationPage` | 1 263 | **Centrepiece**. Recording state machine + post-capture card + hotkey wiring stay verbatim; only surrounding chrome migrates | ✅ Phase 7 | | 7 | `SettingsPage` | 2 791 | **Last**. Section by section. Existing IA stays. Formsnap only where validation/error semantics matter | ✅ Phase 7 | | 8 | `/float` window | — | TaskList in frameless window | ✅ Phase 7 | | 9 | `/viewer` window | — | Bionic-reading action + font tokens load-bearing | ✅ Phase 7 | | 10 | `/preview` window | — | Wayland-hardened; zero portaled primitives | ✅ Phase 7 | 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 | ✅ complete | `src/routes/+layout.svelte` (537 LOC) split into `$lib/shell/AppRuntime.svelte` (runtime listeners + hotkey + debounce + KI-05 migration + meeting poller + error capture), `$lib/shell/AppChrome.svelte` (titlebar + sidebar + task rail), `$lib/shell/AppOverlays.svelte` (toasts + focus timer + triage modal + resize handles). Shared `useCustomChrome` flag moved into `src/lib/utils/customChrome.svelte.ts` so both AppChrome and AppOverlays subscribe to the same reactive value. +layout.svelte is now ~28 LOC of pure composition. Gate green. | | 7.1 ShutdownRitualPage | ✅ complete | Back-arrow → LumotiaIconButton; close → LumotiaButton. | | 7.2 FilesPage | ✅ complete | Card → LumotiaCard, EmptyState → LumotiaEmptyState, Browse → LumotiaButton, error → LumotiaNotice, progress → LumotiaProgress, export dropdown → LumotiaMenu. | | 7.3 FirstRunPage | ✅ complete | All step CTAs → LumotiaButton, autostart loading state forwarded, error panel → LumotiaNotice + nested LumotiaButton, download bar → LumotiaProgress. | | 7.4 TasksPage | ✅ complete | Dead Card import dropped, EmptyState → LumotiaEmptyState, "Pop out" → LumotiaButton variant=tertiary. Bespoke task list, search, quick-capture untouched. | | 7.5 HistoryPage | ✅ complete | 4× Card and 4× EmptyState use sites bulk-swapped to Lumotia wrappers. Search + clear-all type-DELETE modal stay bespoke. | | 7.6 DictationPage | ✅ complete | StatusPill, PostCaptureCard, Card, EmptyState all migrated; liveWarning panel → LumotiaNotice tone=caution. Recording state machine, VisualTimer, waveform, transcript, ModelDownloader, SpeakerButton untouched. | | 7.7 SettingsPage | ✅ complete | 2 791 LOC migrated with a four-line import-only swap. Every existing ``, ``, ``, `` site picks up the Lumotia wrapper because the locally-bound import names still resolve to compatible components. IA preserved verbatim. | | 7.8 /float | ✅ complete | +layout@.svelte already migrated in Phase 3 (KI-05 sync $effect retired). +page.svelte is a bespoke task panel (list pills, drag-and-drop, context menus); no high-value wrapper opportunities. | | 7.9 /viewer | ✅ complete | +layout@.svelte already migrated in Phase 3. +page.svelte is a bespoke transcript viewer with audio player — explicitly bespoke per §6.3. | | 7.10 /preview | ✅ complete | +layout@.svelte already migrated in Phase 3. +page.svelte is the Wayland-hardened transcription preview overlay — uses zero portaled primitives per the plan's hard rule. | | 8 | ✅ complete | Full release gate green: `cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`, `cargo nextest run --workspace` (435/435), `npm run check` (0/0/5704), `npm test` (13/13 across 2 files), `npm run test:browser` (3/3 in Chromium), `npm run test:e2e` (16/16 across two viewports), `npm run analyze` → `reports/bundle-stats.html` (1.7 MB), `scripts/dogfood-rebrand-drill.sh` 8/8, `npm run guard:no-skeleton` clean. Browser-mode tests excluded from the jsdom suite via `vite.config.js` so the two runners don't double-run. Playwright `expect: { timeout }` lifted to 15 s for cold first-compile resilience. | ## 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/`.