From 100e04fa7005e7995b00d872febdd0410b9c3aff Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 15 May 2026 08:28:51 +0100 Subject: [PATCH] v0.2 Phase 0+1: planning doc + tooling baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 6 + docs/release/v0.2-frontend-overhaul.md | 261 ++++++++++ package-lock.json | 630 ++++++++++++++++++++++++- package.json | 15 +- playwright.config.ts | 39 ++ scripts/guard-no-skeleton.mjs | 67 +++ src/lib/components/Titlebar.svelte | 17 +- src/lib/utils/osInfo.ts | 44 +- tests/e2e/smoke.spec.ts | 91 ++++ vite.config.js | 19 +- vitest.browser.config.js | 29 ++ 11 files changed, 1189 insertions(+), 29 deletions(-) create mode 100644 docs/release/v0.2-frontend-overhaul.md create mode 100644 playwright.config.ts create mode 100644 scripts/guard-no-skeleton.mjs create mode 100644 tests/e2e/smoke.spec.ts create mode 100644 vitest.browser.config.js diff --git a/.gitignore b/.gitignore index 15f7812..d51bc0e 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/docs/release/v0.2-frontend-overhaul.md b/docs/release/v0.2-frontend-overhaul.md new file mode 100644 index 0000000..45c25f0 --- /dev/null +++ b/docs/release/v0.2-frontend-overhaul.md @@ -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 ~85–90% cohesive: token-driven warm-amber dark system, named shadow scale, 5 bundled fonts, sensory-zone CSS, per-region accessibility controls. The gap is **grammar inconsistency** — one button style here, another there; one notice pattern in Files, a different one in History; ~95 ad-hoc form controls in `SettingsPage`. The fix is to land one button grammar, one status grammar, one notice/error grammar, one settings-row grammar — across every page — **without disturbing the brand or the bespoke identity surfaces.** + +**Aesthetic target.** Warm brutalist notebook cockpit. Calm, local, tactile, low-noise. Not SaaS, not garden-game, not generic Tailwind, not a wholesale sage rebrand. + +**Tooling is Phase 1, not an afterthought.** Verification and performance tooling are installed before any UI work. If a tool is needed, install/configure it instead of simulating the check manually. + +## 2. Hard rules + +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 4–5. Wrappers live in `src/lib/ui/`; bespoke identity surfaces stay in `src/lib/components/`. + +### 6.1 Phase 4 alias wrappers (thin, same props) + +| Wrapper | Wraps | Status | +|---|---|---| +| `LumotiaCard` | `Card.svelte` | 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 `` + 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/`. diff --git a/package-lock.json b/package-lock.json index ef1dd3f..c395051 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 913b4b7..d1b861d 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..cb68b8b --- /dev/null +++ b/playwright.config.ts @@ -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, + }, +}); diff --git a/scripts/guard-no-skeleton.mjs b/scripts/guard-no-skeleton.mjs new file mode 100644 index 0000000..8abf224 --- /dev/null +++ b/scripts/guard-no-skeleton.mjs @@ -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/)`); diff --git a/src/lib/components/Titlebar.svelte b/src/lib/components/Titlebar.svelte index e760d1f..287db7b 100644 --- a/src/lib/components/Titlebar.svelte +++ b/src/lib/components/Titlebar.svelte @@ -1,33 +1,48 @@