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 files npm test ✓ 13/13 (2 files) npm run test:browser ✓ 3/3 in Chromium npm run test:e2e ✓ 16/16 × 2 viewports npm run analyze ✓ reports/bundle-stats.html (1.7 MB) scripts/dogfood-rebrand-drill.sh ✓ 8/8 (sandbox) npm run guard:no-skeleton ✓ clean Two small Phase 8 corrections landed alongside the gate: vite.config.js: the jsdom suite was picking up `src/lib/ui/*.browser.test.ts`, which only works under the @vitest/browser-playwright provider. Added the browser-suffix glob to the jsdom suite's exclude list so the two runners stop double-running the same files. playwright.config.ts: bumped the global `expect.timeout` to 15 s. The cold first-compile of the Vite SPA tree was exceeding Playwright's default 5 s `toBeVisible` timeout on the 900x700 project on dogfood runs. The 1440x900 project (which runs second after a warm cache) never hit it. Phase 8 closes Phase-7 page migrations. Branch is ready for the finishing-a-development-branch handoff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
21 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 |
✅ 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 /
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 | ✅ 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— 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 | ✅ 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 <Card>, <Toggle>, <SettingsGroup>, <StatusPill> 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):
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/.