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>
This commit is contained in:
2026-05-15 08:28:51 +01:00
parent 3770815fbf
commit 100e04fa70
11 changed files with 1189 additions and 29 deletions

6
.gitignore vendored
View File

@@ -8,6 +8,12 @@ dist/
.cargo/
.lumotia-last-audit
# v0.2 frontend tooling artefacts
reports/
.playwright/
test-results/
playwright-report/
# Python bytecode (from release scripts under scripts/)
__pycache__/
*.pyc

View File

@@ -0,0 +1,261 @@
---
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` | 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/`.

630
package-lock.json generated
View File

@@ -20,18 +20,25 @@
"svelte-i18n": "^4.0.1"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.58.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.2.1",
"@tauri-apps/cli": "^2",
"@axe-core/playwright": "4.11.3",
"@playwright/test": "1.60.0",
"@sveltejs/adapter-static": "3.0.10",
"@sveltejs/kit": "2.58.0",
"@sveltejs/vite-plugin-svelte": "5.1.1",
"@tailwindcss/vite": "4.2.1",
"@tauri-apps/cli": "2.10.1",
"@vitest/browser": "4.1.6",
"@vitest/browser-playwright": "4.1.6",
"jsdom": "29.1.1",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.2.1",
"typescript": "~5.6.2",
"vite": "^6.4.2",
"vitest": "4.1.6"
"playwright": "1.60.0",
"rollup-plugin-visualizer": "7.0.1",
"svelte": "5.53.12",
"svelte-check": "4.4.5",
"tailwindcss": "4.2.1",
"typescript": "5.6.3",
"vite": "6.4.2",
"vitest": "4.1.6",
"vitest-browser-svelte": "2.1.1"
}
},
"node_modules/@asamuzakjp/css-color": {
@@ -85,6 +92,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/@axe-core/playwright": {
"version": "4.11.3",
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.3.tgz",
"integrity": "sha512-h/kfksv4F0cVIDlKpT4700OehdRgpvuVskuQ2nb7/JmtWUXpe9ftHAPtwyXGvVSsa6SJ64A9ER7Zrzc/sIvC4w==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"axe-core": "~4.11.4"
},
"peerDependencies": {
"playwright-core": ">= 1.0.0"
}
},
"node_modules/@blazediff/core": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz",
"integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==",
"dev": true,
"license": "MIT"
},
"node_modules/@bramus/specificity": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
@@ -800,6 +827,22 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@playwright/test": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
@@ -1883,6 +1926,19 @@
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@testing-library/svelte-core": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@testing-library/svelte-core/-/svelte-core-1.0.0.tgz",
"integrity": "sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0"
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -1933,6 +1989,53 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vitest/browser": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.6.tgz",
"integrity": "sha512-ynsspTubXGSpa58JFJ24xIQt4z4A25epSbugEyaTmmrV1//Wec9EgE/LtoaC6yxUrXi5P7erGHRrkdZIHaVQuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@blazediff/core": "1.9.1",
"@vitest/mocker": "4.1.6",
"@vitest/utils": "4.1.6",
"magic-string": "^0.30.21",
"pngjs": "^7.0.0",
"sirv": "^3.0.2",
"tinyrainbow": "^3.1.0",
"ws": "^8.19.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "4.1.6"
}
},
"node_modules/@vitest/browser-playwright": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.6.tgz",
"integrity": "sha512-4csoeyl/qwHyxU2zNL0++WaoDr8YJDXOQPwWPNJoTZ+QzcdO3INYKgF5Zfz730Io7zbkuv914aZmfQ+QE+1Hvw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/browser": "4.1.6",
"@vitest/mocker": "4.1.6",
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"playwright": "*",
"vitest": "4.1.6"
},
"peerDependenciesMeta": {
"playwright": {
"optional": false
}
}
},
"node_modules/@vitest/expect": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz",
@@ -2068,6 +2171,32 @@
"node": ">=0.4.0"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/aria-query": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
@@ -2087,6 +2216,16 @@
"node": ">=12"
}
},
"node_modules/axe-core": {
"version": "4.11.4",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz",
"integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==",
"dev": true,
"license": "MPL-2.0",
"engines": {
"node": ">=4"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -2106,6 +2245,22 @@
"require-from-string": "^2.0.2"
}
},
"node_modules/bundle-name": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"run-applescript": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -2148,6 +2303,21 @@
"node": ">=0.10"
}
},
"node_modules/cliui": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -2248,6 +2418,49 @@
"node": ">=0.10.0"
}
},
"node_modules/default-browser": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
"integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"bundle-name": "^4.1.0",
"default-browser-id": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/default-browser-id": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
"integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/define-lazy-prop": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2264,6 +2477,13 @@
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.20.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
@@ -2392,6 +2612,16 @@
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/esm-env": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
@@ -2491,6 +2721,29 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globalyzer": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
@@ -2535,6 +2788,54 @@
"tslib": "^2.8.0"
}
},
"node_modules/is-docker": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
"dev": true,
"license": "MIT",
"bin": {
"is-docker": "cli.js"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-in-ssh": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
"integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-docker": "^3.0.0"
},
"bin": {
"is-inside-container": "cli.js"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -2557,6 +2858,22 @@
"@types/estree": "^1.0.6"
}
},
"node_modules/is-wsl": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
"integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-inside-container": "^1.0.0"
},
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -3010,6 +3327,27 @@
],
"license": "MIT"
},
"node_modules/open": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz",
"integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==",
"dev": true,
"license": "MIT",
"dependencies": {
"default-browser": "^5.4.0",
"define-lazy-prop": "^3.0.0",
"is-in-ssh": "^1.0.0",
"is-inside-container": "^1.0.0",
"powershell-utils": "^0.1.0",
"wsl-utils": "^0.3.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse5": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
@@ -3050,6 +3388,63 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
@@ -3079,6 +3474,19 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/powershell-utils": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
"integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3158,6 +3566,50 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-visualizer": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-7.0.1.tgz",
"integrity": "sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"open": "^11.0.0",
"picomatch": "^4.0.2",
"source-map": "^0.7.4",
"yargs": "^18.0.0"
},
"bin": {
"rollup-plugin-visualizer": "dist/bin/cli.js"
},
"engines": {
"node": ">=22"
},
"peerDependencies": {
"rolldown": "1.x || ^1.0.0-beta || ^1.0.0-rc",
"rollup": "2.x || 3.x || 4.x"
},
"peerDependenciesMeta": {
"rolldown": {
"optional": true
},
"rollup": {
"optional": true
}
}
},
"node_modules/run-applescript": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
"integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -3212,6 +3664,16 @@
"node": ">=18"
}
},
"node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 12"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -3236,6 +3698,40 @@
"dev": true,
"license": "MIT"
},
"node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/svelte": {
"version": "5.53.12",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.12.tgz",
@@ -4089,6 +4585,23 @@
}
}
},
"node_modules/vitest-browser-svelte": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/vitest-browser-svelte/-/vitest-browser-svelte-2.1.1.tgz",
"integrity": "sha512-qbunYRSm+N92r9bfTkdDTpBZESLmp4QFz2SluV3n/x8U7ysosfeXYJZ4vXbJ0Y0LzoqqDnV5LHprmFgn4Eo+Ug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@testing-library/svelte-core": "^1.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0",
"vitest": "^4.0.0"
}
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
@@ -4154,6 +4667,63 @@
"node": ">=8"
}
},
"node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/wsl-utils": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz",
"integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-wsl": "^3.1.0",
"powershell-utils": "^0.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
@@ -4171,6 +4741,44 @@
"dev": true,
"license": "MIT"
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"string-width": "^7.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^22.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/yargs-parser": {
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/zimmerframe": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",

View File

@@ -13,6 +13,12 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:browser": "vitest --config vitest.browser.config.js --run",
"test:rust:fast": "cargo nextest run --workspace",
"analyze": "ANALYZE=1 vite build",
"guard:no-skeleton": "node scripts/guard-no-skeleton.mjs",
"tauri": "tauri"
},
"license": "MIT",
@@ -28,17 +34,24 @@
"svelte-i18n": "^4.0.1"
},
"devDependencies": {
"@axe-core/playwright": "4.11.3",
"@playwright/test": "1.60.0",
"@sveltejs/adapter-static": "3.0.10",
"@sveltejs/kit": "2.58.0",
"@sveltejs/vite-plugin-svelte": "5.1.1",
"@tailwindcss/vite": "4.2.1",
"@tauri-apps/cli": "2.10.1",
"@vitest/browser": "4.1.6",
"@vitest/browser-playwright": "4.1.6",
"jsdom": "29.1.1",
"playwright": "1.60.0",
"rollup-plugin-visualizer": "7.0.1",
"svelte": "5.53.12",
"svelte-check": "4.4.5",
"tailwindcss": "4.2.1",
"typescript": "5.6.3",
"vite": "6.4.2",
"vitest": "4.1.6"
"vitest": "4.1.6",
"vitest-browser-svelte": "2.1.1"
}
}

39
playwright.config.ts Normal file
View File

@@ -0,0 +1,39 @@
import { defineConfig, devices } from "@playwright/test";
// Frontend-only E2E. The dev server is SvelteKit (npm run dev:frontend) on
// localhost:1420 — Tauri IPC is not present. Tests must not depend on Tauri
// commands; mock the invoke boundary if you need a Tauri-flavoured surface,
// or skip the assertion with a reason and cover it via `cargo test` instead.
//
// Visual diffs are NOT failing the build yet; screenshots are captured as
// artifacts. After Phase 7 stabilises the UI, a follow-up commit promotes
// baselines and enables failing diffs.
export default defineConfig({
testDir: "tests/e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: [["list"], ["html", { open: "never", outputFolder: "playwright-report" }]],
outputDir: "test-results",
use: {
baseURL: "http://localhost:1420",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{
name: "chromium-900x700",
use: { ...devices["Desktop Chrome"], viewport: { width: 900, height: 700 } },
},
{
name: "chromium-1440x900",
use: { ...devices["Desktop Chrome"], viewport: { width: 1440, height: 900 } },
},
],
webServer: {
command: "npm run dev:frontend",
url: "http://localhost:1420",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env node
// Fails (exit 1) if any reference to @skeletonlabs appears in package.json,
// package-lock.json, or anywhere under src/. Prevents a future agent
// reading "frontend overhaul" from reaching for Skeleton — see
// docs/release/v0.2-frontend-overhaul.md §16.
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join, relative } from "node:path";
const ROOT = process.cwd();
const NEEDLE = "@skeletonlabs";
const hits = [];
function checkFile(path) {
try {
const content = readFileSync(path, "utf8");
if (content.includes(NEEDLE)) {
const rel = relative(ROOT, path);
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes(NEEDLE)) {
hits.push(`${rel}:${i + 1}: ${lines[i].trim()}`);
}
}
}
} catch {
// unreadable -> ignore
}
}
function walk(dir) {
let entries;
try {
entries = readdirSync(dir);
} catch {
return;
}
for (const name of entries) {
if (name === "node_modules" || name === ".svelte-kit" || name === ".git") continue;
const full = join(dir, name);
let st;
try {
st = statSync(full);
} catch {
continue;
}
if (st.isDirectory()) {
walk(full);
} else {
checkFile(full);
}
}
}
checkFile(join(ROOT, "package.json"));
checkFile(join(ROOT, "package-lock.json"));
walk(join(ROOT, "src"));
if (hits.length > 0) {
console.error(`guard-no-skeleton: found ${hits.length} reference(s) to ${NEEDLE}:`);
for (const h of hits) console.error(" " + h);
console.error("\nLumotia v0.2 frontend overhaul forbids Skeleton.");
console.error("See docs/release/v0.2-frontend-overhaul.md §16.");
process.exit(1);
}
console.log(`guard-no-skeleton: clean (no ${NEEDLE} references in package.json, package-lock.json, or src/)`);

View File

@@ -1,33 +1,48 @@
<script lang="ts">
import { getCurrentWindow } from "@tauri-apps/api/window";
import { settings } from "$lib/stores/page.svelte.js";
import { hasTauriRuntime } from "$lib/utils/runtime.js";
let { compact = false } = $props();
let maximised = $state(false);
// Titlebar is normally gated behind useCustomChrome in the route layouts,
// which is false on Linux (native KWin/Mutter decorations) and false in
// any browser-preview context that mis-detects the OS. The defensive
// guard here keeps the component from crashing in any future code path
// that mounts it without a Tauri runtime — `getCurrentWindow()` reads
// window.__TAURI_INTERNALS__.metadata which is undefined in plain browsers.
const tauriPresent = hasTauriRuntime();
async function handleMinimise() {
if (!tauriPresent) return;
await getCurrentWindow().minimize();
}
async function handleMaximise() {
if (!tauriPresent) return;
await getCurrentWindow().toggleMaximize();
maximised = await getCurrentWindow().isMaximized();
}
async function handleClose() {
if (!tauriPresent) return;
await getCurrentWindow().hide();
}
function handleDragStart(e: PointerEvent) {
if (!tauriPresent) return;
if (e.button !== 0) return;
if ((e.target as HTMLElement | null)?.closest("button")) return;
try { (e.currentTarget as HTMLElement | null)?.setPointerCapture?.(e.pointerId); } catch {}
getCurrentWindow().startDragging();
}
// Track maximise state
// Track maximise state. Skipped without Tauri; getCurrentWindow().onResized
// throws because the underlying Window object has no `metadata`.
$effect(() => {
if (!tauriPresent) return;
let unlisten: (() => void) | undefined;
getCurrentWindow().onResized(async () => {
maximised = await getCurrentWindow().isMaximized();

View File

@@ -30,23 +30,37 @@ interface OsInfo {
let cached: OsInfo | null = null;
const FALLBACK_BROWSER_INFO = {
os: detectBrowserOs(),
arch: 'unknown',
family: detectBrowserFamily(),
usesCmd: detectBrowserOs() === 'macos',
isWayland: false,
customHotkeyBackend: false,
primaryModifierLabel: detectBrowserOs() === 'macos' ? 'Cmd' : 'Ctrl',
};
// Lazy fallback builder. The previous module-level constant evaluated
// `navigator` during SvelteKit SSR — where it is undefined — so `os`
// got frozen at `'unknown'` and `isLinux()` returned false even when
// the page rendered in a real Linux browser. Building on demand reads
// `navigator` at call-time, which is always client-side.
function buildBrowserFallback(): OsInfo {
const browserOs = detectBrowserOs();
return {
os: browserOs,
arch: 'unknown',
family: detectBrowserFamily(),
usesCmd: browserOs === 'macos',
isWayland: false,
customHotkeyBackend: false,
primaryModifierLabel: browserOs === 'macos' ? 'Cmd' : 'Ctrl',
};
}
function detectBrowserOs() {
if (typeof navigator === 'undefined') return 'unknown';
const ua = (navigator.userAgent || '').toLowerCase();
// navigator.platform is the truthful OS surface and survives UA spoofing
// (e.g. Playwright's Chromium reports a Windows UA on Linux runners). UA
// is consulted only as a fallback when platform is empty or unrecognised.
const platform = (navigator.platform || '').toLowerCase();
if (platform.includes('mac') || ua.includes('mac os')) return 'macos';
if (platform.includes('win') || ua.includes('windows')) return 'windows';
if (platform.includes('linux') || ua.includes('linux')) return 'linux';
if (platform.includes('mac')) return 'macos';
if (platform.includes('win')) return 'windows';
if (platform.includes('linux')) return 'linux';
const ua = (navigator.userAgent || '').toLowerCase();
if (ua.includes('mac os')) return 'macos';
if (ua.includes('windows')) return 'windows';
if (ua.includes('linux')) return 'linux';
return 'unknown';
}
@@ -65,14 +79,14 @@ function detectBrowserFamily() {
export async function loadOsInfo(): Promise<OsInfo> {
if (cached) return cached;
if (!hasTauriRuntime()) {
cached = FALLBACK_BROWSER_INFO;
cached = buildBrowserFallback();
return cached;
}
try {
cached = await invoke<OsInfo>("get_os_info");
} catch (err) {
console.warn('loadOsInfo: get_os_info failed, using browser fallback', err);
cached = FALLBACK_BROWSER_INFO;
cached = buildBrowserFallback();
}
return cached;
}

91
tests/e2e/smoke.spec.ts Normal file
View File

@@ -0,0 +1,91 @@
// Phase 1 smoke tests. Frontend-only — no Tauri IPC.
// Visual screenshots are saved as artifacts; they do NOT fail the build at
// this stage (see playwright.config.ts header). Promotion to failing visual
// diffs happens after Phase 7 stabilises the UI.
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
const ZONES = ["cave", "energy", "reset"] as const;
const THEMES = ["light", "dark"] as const;
test.describe("Lumotia frontend smoke (no Tauri IPC)", () => {
test("app loads without Tauri runtime", async ({ page }) => {
// Suppress noisy invoke-rejected promises from the layout's onMount;
// they are expected without Tauri. We only fail on unhandled crashes.
const fatalErrors: string[] = [];
page.on("pageerror", (err) => fatalErrors.push(err.message));
await page.goto("/");
await expect(page.locator("main")).toBeVisible();
expect(fatalErrors).toEqual([]);
});
test("keyboard navigation reaches main + sidebar surface", async ({ page }) => {
await page.goto("/");
await page.locator("main").waitFor({ state: "visible" });
// Tab once and confirm focus moves to a focusable element somewhere in
// the document, not nothing. We don't assert the specific element to
// keep the test robust through chrome migration.
await page.keyboard.press("Tab");
const focused = await page.evaluate(() =>
document.activeElement && document.activeElement !== document.body
? document.activeElement.tagName
: null,
);
expect(focused).not.toBeNull();
});
test("axe scan on / has no critical structural violations", async ({ page }) => {
await page.goto("/");
await page.locator("main").waitFor({ state: "visible" });
const results = await new AxeBuilder({ page })
.disableRules([
// svelte-i18n initialises asynchronously; <html lang> is set after
// first paint in browser-preview mode.
"html-has-lang",
// Colour-contrast is owned by docs/release/v0.1-contrast-audit.md
// and gets re-enabled in Phase 7 after the wrapper palette settles.
// Phase 1's smoke baseline focuses on structural/semantic a11y.
"color-contrast",
])
.analyze();
const criticals = results.violations.filter(
(v) => v.impact === "critical" || v.impact === "serious",
);
expect(criticals).toEqual([]);
});
for (const theme of THEMES) {
test(`theme smoke — ${theme}`, async ({ page }, testInfo) => {
await page.goto("/");
await page.locator("main").waitFor({ state: "visible" });
// Apply theme without depending on the preferences store, which
// needs Tauri persistence. The data-theme attribute is what the CSS
// tokens key off; the preferences store also writes this attribute
// in production.
await page.evaluate((t) => {
document.documentElement.dataset.theme = t;
}, theme);
await page.waitForTimeout(50);
await page.screenshot({
path: `test-results/theme-${theme}-${testInfo.project.name}.png`,
fullPage: false,
});
});
}
for (const zone of ZONES) {
test(`sensory zone smoke — ${zone}`, async ({ page }, testInfo) => {
await page.goto("/");
await page.locator("main").waitFor({ state: "visible" });
await page.evaluate((z) => {
document.documentElement.dataset.zone = z;
}, zone);
await page.waitForTimeout(50);
await page.screenshot({
path: `test-results/zone-${zone}-${testInfo.project.name}.png`,
fullPage: false,
});
});
}
});

View File

@@ -4,13 +4,30 @@
import { defineConfig } from "vitest/config";
import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";
import { visualizer } from "rollup-plugin-visualizer";
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// @ts-expect-error process is a nodejs global
const analyze = process.env.ANALYZE === "1";
// https://vite.dev/config/
export default defineConfig(async () => ({
plugins: [sveltekit(), tailwindcss()],
plugins: [
sveltekit(),
tailwindcss(),
...(analyze
? [
visualizer({
filename: "reports/bundle-stats.html",
template: "treemap",
gzipSize: true,
brotliSize: true,
open: false,
}),
]
: []),
],
clearScreen: false,
server: {
port: 1420,

29
vitest.browser.config.js Normal file
View File

@@ -0,0 +1,29 @@
// Browser-mode vitest config. Separate from the jsdom suite in vite.config.js
// so we never accidentally cross-pollute environments. Includes only
// component-level tests under src/lib/ui/** and src/lib/shell/**, the
// new homes for v0.2 primitives and the split shell.
import { defineConfig } from "vitest/config";
import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";
import { playwright } from "@vitest/browser-playwright";
export default defineConfig({
plugins: [sveltekit(), tailwindcss()],
test: {
include: [
"src/lib/ui/**/*.browser.{test,spec}.{ts,js,svelte}",
"src/lib/shell/**/*.browser.{test,spec}.{ts,js,svelte}",
"src/lib/utils/**/*.browser.{test,spec}.{ts,js}",
],
exclude: ["src-tauri/**", "node_modules/**", "build/**", ".svelte-kit/**"],
browser: {
enabled: true,
provider: playwright(),
headless: true,
instances: [{ browser: "chromium" }],
},
restoreMocks: true,
clearMocks: true,
unstubAllGlobals: true,
},
});