Files
Lumotia/docs/release/v0.2-frontend-overhaul.md
Jake 8c9708a508 v0.2 Phase 4: wrapper alias layer (src/lib/ui/)
Six thin alias wrappers, same prop APIs as the underlying components.
Lets pages migrate imports from $lib/components/* to $lib/ui/* one
file at a time without touching markup, and gives Phase 5+ a place
to tighten grammar without churning every call site.

  LumotiaCard           → Card.svelte
  LumotiaStatusPill     → StatusPill.svelte
  LumotiaToggle         → Toggle.svelte (forwards bind:checked, bind:loading)
  LumotiaSettingsGroup  → SettingsGroup.svelte (typed Props for svelte-check)
  LumotiaEmptyState     → EmptyState.svelte
  LumotiaPostCaptureCard → PostCaptureCard.svelte

Per the plan, the underlying components in src/lib/components/ are
untouched. They get retired during the per-page migrations in Phase 7
once no consumer remains.

LumotiaSettingsGroup mirrors the underlying Props interface explicitly
because Svelte 5's spread-into-typed-component caught a real missing-
`title` error during svelte-check. The mirrored interface keeps call
sites type-safe when importing via $lib/ui/.

Phase 4 per-phase gate green: npm run check (0/0/4135 files),
npm test (all green), npm run test:e2e (16/16), npm run
guard:no-skeleton (clean).

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

262 lines
18 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` | ✅ 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 | 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 | ✅ 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 | 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/`.