Files
Lumotia/docs/release/v0.2-frontend-overhaul.md
Jake a9733544c0 v0.2 Phase 6: shell split — AppRuntime / AppChrome / AppOverlays
src/routes/+layout.svelte was 537 LOC of mixed runtime, chrome and
overlay concerns. Split into three single-purpose shells under
src/lib/shell/, with +layout.svelte reduced to ~28 LOC of pure
composition.

AppRuntime (no DOM beyond <svelte:window>):
  - Global hotkey dual backend (evdev / tauri-plugin-global-shortcut)
  - 120ms hotkey debounce (sacred behaviour §5 #2)
  - PREFERENCES_CHANGED_EVENT listener (sacred §5 #4)
  - KI-05 one-shot legacy-theme migration
  - Sidebar hotkeys: [ toggle, Ctrl+K, Ctrl+, (sacred §5 #10)
  - Wind-down tray listener
  - Meeting auto-capture poller
  - Global frontend error capture
  - Nudge bus + implementation intentions lifecycle
  - Font-size CSS var $effect
  - Window resize → sidebar auto-collapse
  - Onboarding/first-run check + update check + LLM status warm-up

AppChrome (the visual shell):
  - Titlebar (OS-aware via customChrome helper)
  - Sidebar (recording-state-aware — sacred §5 #1 stays in
    Sidebar.svelte verbatim)
  - Main slot
  - TaskSidebar conditional rail

AppOverlays (mounted-once globals):
  - ToastViewport
  - FocusTimer
  - MorningTriageModal
  - ResizeHandles (OS-gated)

src/lib/utils/customChrome.svelte.ts holds the single source of truth
for useCustomChrome, a module-level $state both AppChrome and
AppOverlays subscribe to. Each only ever sees one loadOsInfo() call
between them.

Secondary windows still escape via their own +layout@.svelte; the
defensive isSecondaryWindow check in +layout.svelte stays so a
direct /float, /viewer, /preview navigation through the root layout
also drops the chrome.

Phase 6 per-page gate green: npm run check (0/0/5704 files),
npm test, npm run test:browser (3/3), npm run test:e2e (16/16).
No regressions in the Phase 1 smoke baseline.

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

19 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 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 / 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 $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 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/.