Files
Lumotia/docs/release/v0.2-frontend-overhaul.md
Jake f03f8a01b0 v0.2 Phase 8: full release gate — all checks 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 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>
2026-05-15 09:07:10 +01:00

262 lines
21 KiB
Markdown
Raw Permalink 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 | ✅ 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 | ✅ 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` — 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 | ✅ 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):**
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/`.