Files
Lumotia/docs/release/v0.2-frontend-overhaul.md
Jake 100e04fa70 v0.2 Phase 0+1: planning doc + tooling baseline
Phase 0 — docs/release/v0.2-frontend-overhaul.md as single source of
truth for the v0.2 frontend coherence pass. Records hard rules,
tooling pins, sacred-behaviour contract list, wrapper catalogue,
per-page migration order, verification matrix, KI-05 plan, and the
explicit "DO NOT add Skeleton" line.

Phase 1 — tooling baseline. All exact-pinned per the plan:

- @playwright/test@1.60.0 + playwright@1.60.0 + @axe-core/playwright@4.11.3
- rollup-plugin-visualizer@7.0.1 (wired behind ANALYZE=1)
- @vitest/browser@4.1.6 + @vitest/browser-playwright@4.1.6 (provider)
- vitest-browser-svelte@2.1.1 (runes-aware Svelte 5 bridge)
- cargo-nextest installed globally

New configs: playwright.config.ts (frontend-only, dev:frontend webServer,
900x700 + 1440x900 projects, visual baselines deferred), vitest.browser.config.js
(separate from jsdom suite). New scripts: test:e2e, test:e2e:ui,
test:browser, analyze, test:rust:fast, guard:no-skeleton.

guard-no-skeleton.mjs walks package.json + package-lock + src/ for any
@skeletonlabs reference and exits 1 if found — locks in the no-Skeleton
hard rule for any future agent.

Smoke baseline (tests/e2e/smoke.spec.ts): 16 tests passing across two
viewports — app loads without Tauri runtime, keyboard nav, axe scan
(color-contrast deferred to Phase 7 per docs/release/v0.1-contrast-audit.md),
light/dark theme cycle, all three sensory zones (cave/energy/reset).
10 screenshots emitted to test-results/ as non-failing artefacts.

Surfaced + fixed two browser-preview bugs while landing the baseline:

- src/lib/utils/osInfo.ts: FALLBACK_BROWSER_INFO was evaluated at SSR
  module-load (navigator undefined → os: 'unknown'), and the UA check
  ran before navigator.platform — so Playwright's Windows-UA Chromium
  on a Linux runner detected as Windows, useCustomChrome went true,
  Titlebar mounted and tripped Tauri-only APIs. Now lazy-built per
  call; platform is the primary signal, UA only a fallback.

- src/lib/components/Titlebar.svelte: defensive hasTauriRuntime() guard
  on every handler and the $effect. Titlebar should not crash if any
  future code path mounts it without Tauri.

.gitignore: reports/, .playwright/, test-results/, playwright-report/.

Per-phase gate green: npm run check (0/0), npm test (0/0), 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:28:51 +01:00

17 KiB
Raw Blame History

name, type, tags, description
name type tags description
v0.2-frontend-overhaul release
release
v0.2
frontend
coherence-pass
no-skeleton
bits-ui
formsnap
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 pending UI/form deps
3 pending Additive tokens + KI-05
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/.