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>
18 KiB
name, type, tags, description
| name | type | tags | description | |||||||
|---|---|---|---|---|---|---|---|---|---|---|
| v0.2-frontend-overhaul | release |
|
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
- Do not install Skeleton.
- 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. - Do not rewrite bespoke identity surfaces: recording controls, waveform/timer, bionic transcript surfaces, FocusTimer, MicroSteps, TaskSidebar, ModelDownloader, HotkeyRecorder, ZonePicker.
- Use exact-pinned packages.
- 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 |
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 /
VisualTimerSVG ring - Bionic transcript surfaces (
use:bionicaction consumers) FocusTimerMicroStepsTaskSidebarModelDownloaderHotkeyRecorderZonePickerCompletionSparklineEnergyChip,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— dropthemefromSettingsStatesrc/lib/pages/SettingsPage.svelte:1118,:2360— repoint bindings topreferences.themewith mapped optionssrc/routes/+layout.svelte:61-68and the three+layout@.svelte— retire the migration$effect- One-shot localStorage migration that copies any historical
settings.themeintopreferenceson 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 $effects 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):
src/lib/utils/osInfo.tsdefinedFALLBACK_BROWSER_INFOat module-load time. Under SvelteKit SSR,navigatoris undefined, sodetectBrowserOs()froze at'unknown'andisLinux()returned false even on Linux browsers.- Inside
detectBrowserOs(), the UA was consulted beforenavigator.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 realLinux x86_64platform string.
Fix.
osInfo.ts— replace the module-level constant with a lazybuildBrowserFallback()that runs at call-time. Re-orderdetectBrowserOs()to readnavigator.platformfirst (the truthful OS surface, immune to UA spoofing) and only fall back to UA when platform is unknown.Titlebar.svelte— defensivehasTauriRuntime()guard on all four handlers and the$effect.Titlebarshould never crash if a future code path mounts it without a Tauri runtime; the underlyinggetCurrentWindow().metadatais 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:/previewuses zero portaled primitives — onlyLumotiaCard,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/pretextdep inpackage.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 chromiummay need extra Linux libs on Fedora. Mitigation:--with-depsflag in Phase 1. @vitest/browserSvelte 5 compat. Mitigation:vitest-browser-svelte@2.1.1is 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/.