From 9f97d8407a17019656c52f735a396b36a422316a Mon Sep 17 00:00:00 2001 From: Florian Heinz Date: Wed, 21 Oct 2026 14:50:31 +0200 Subject: [PATCH] Initial release 0.1.0 --- .dockerignore | 8 + .env.example | 3 + .gitignore | 19 + .npmrc | 1 + .prettierignore | 7 + CloudronManifest.json | 17 + Dockerfile | 16 + LICENSE | 21 + README.md | 105 + docs/ACCEPTANCE.md | 38 + docs/ARCHITECTURE.md | 84 + docs/DECISIONS.md | 83 + docs/STATUS.md | 43 + eslint.config.js | 32 + migrations/0001_editorial_core.sql | 96 + package-lock.json | 3230 +++++++++++++++++++++++ package.json | 54 + playwright.config.ts | 30 + prettier.config.js | 9 + scripts/config.mjs | 12 + scripts/migrate.mjs | 57 + scripts/seed.mjs | 286 ++ src/app.css | 1350 ++++++++++ src/app.d.ts | 5 + src/app.html | 30 + src/lib/components/NavIcon.svelte | 32 + src/lib/components/PreviewNotice.svelte | 13 + src/lib/components/ThemeToggle.svelte | 49 + src/lib/format.ts | 12 + src/lib/index.ts | 1 + src/lib/server/config.ts | 12 + src/lib/server/database.spec.ts | 77 + src/lib/server/database.ts | 21 + src/lib/server/previews.ts | 228 ++ src/routes/+layout.svelte | 82 + src/routes/+page.server.ts | 5 + src/routes/archiv/+page.svelte | 20 + src/routes/eingang/+page.svelte | 21 + src/routes/fundus/+page.server.ts | 5 + src/routes/fundus/+page.svelte | 101 + src/routes/healthz/+server.ts | 12 + src/routes/live/+page.server.ts | 15 + src/routes/live/+page.svelte | 92 + src/routes/sendungen/+page.server.ts | 5 + src/routes/sendungen/+page.svelte | 141 + src/routes/verwaltung/+page.server.ts | 5 + src/routes/verwaltung/+page.svelte | 50 + start.sh | 14 + static/robots.txt | 2 + tests/ui.e2e.ts | 86 + tsconfig.json | 8 + vite.config.ts | 29 + 52 files changed, 6774 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 CloudronManifest.json create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/ACCEPTANCE.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/DECISIONS.md create mode 100644 docs/STATUS.md create mode 100644 eslint.config.js create mode 100644 migrations/0001_editorial_core.sql create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 prettier.config.js create mode 100644 scripts/config.mjs create mode 100644 scripts/migrate.mjs create mode 100644 scripts/seed.mjs create mode 100644 src/app.css create mode 100644 src/app.d.ts create mode 100644 src/app.html create mode 100644 src/lib/components/NavIcon.svelte create mode 100644 src/lib/components/PreviewNotice.svelte create mode 100644 src/lib/components/ThemeToggle.svelte create mode 100644 src/lib/format.ts create mode 100644 src/lib/index.ts create mode 100644 src/lib/server/config.ts create mode 100644 src/lib/server/database.spec.ts create mode 100644 src/lib/server/database.ts create mode 100644 src/lib/server/previews.ts create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+page.server.ts create mode 100644 src/routes/archiv/+page.svelte create mode 100644 src/routes/eingang/+page.svelte create mode 100644 src/routes/fundus/+page.server.ts create mode 100644 src/routes/fundus/+page.svelte create mode 100644 src/routes/healthz/+server.ts create mode 100644 src/routes/live/+page.server.ts create mode 100644 src/routes/live/+page.svelte create mode 100644 src/routes/sendungen/+page.server.ts create mode 100644 src/routes/sendungen/+page.svelte create mode 100644 src/routes/verwaltung/+page.server.ts create mode 100644 src/routes/verwaltung/+page.svelte create mode 100755 start.sh create mode 100644 static/robots.txt create mode 100644 tests/ui.e2e.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9400d82 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.data +.env +.svelte-kit +build +node_modules +playwright-report +test-results diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8d8e95a --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Relative paths are resolved from the project directory. +SENDEWERK_DB_PATH=.data/sendewerk.sqlite +SENDEWERK_UPLOAD_DIR=.data/uploads diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79fceeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +node_modules +/.svelte-kit +/build +/.data +/uploads +/test-results +/playwright-report +/coverage +/.env +/.env.* +!/.env.example +*.sqlite +*.sqlite-* +*.log +.idea +.vscode +.DS_Store +AGENTS.md +Sendewerk-OpenCode-Bauplan.md diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b1971a0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules +build +.svelte-kit +package-lock.json +test-results +playwright-report +Sendewerk-OpenCode-Bauplan.md diff --git a/CloudronManifest.json b/CloudronManifest.json new file mode 100644 index 0000000..d9a8cd8 --- /dev/null +++ b/CloudronManifest.json @@ -0,0 +1,17 @@ +{ + "id": "io.hnz.sendewerk", + "title": "Sendewerk", + "description": "Redaktionelle Planung und Durchführung von heinzlive-Sendungen.", + "tagline": "Vom Fundstück bis zur Livesendung", + "version": "0.1.0", + "healthCheckPath": "/healthz", + "httpPort": 3000, + "addons": { + "localstorage": { + "sqlite": { + "paths": ["/app/data/sendewerk.sqlite"] + } + } + }, + "manifestVersion": 2 +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e05294b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM cloudron/node-base:24-20260920@sha256:d984683ec59bf2379130bf41cf3c2b6bc0453f327297d7183525cb05424e7b34 + +WORKDIR /app/code + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . ./ +RUN npm run build \ + && npm prune --omit=dev \ + && chmod +x /app/code/start.sh + +ENV NODE_ENV=production +EXPOSE 3000 + +CMD ["/app/code/start.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aefe7fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Sendewerk contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc37690 --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# Sendewerk + +Sendewerk ist die redaktionelle Webanwendung für den Weg vom vorbereiteten +Inhalt bis zur moderierten und archivierten Sendung. Der aktuelle Stand umfasst +ausschließlich Etappe 1: technisches Grundgerüst, persistente Beispieldaten +und gekennzeichnete Gestaltungsvorschauen. Anmeldung, Bearbeitung und echte +Live-Steuerung folgen in späteren Etappen. + +## Voraussetzungen + +- Linux +- Node.js 22.17 oder neuer (Node 24 LTS ist für das Cloudron-Image vorgesehen) +- npm 10 oder neuer + +Cloudron, Docker, eine Domain und externe Dienste sind für den lokalen Betrieb +nicht erforderlich. + +## Frischer lokaler Start + +```sh +npm ci +cp .env.example .env +npm run setup +npm run dev +``` + +Danach ist die Anwendung unter erreichbar. `setup` +erstellt die konfigurierten Verzeichnisse, führt ausstehende Migrationen aus +und lädt den reproduzierbaren Beispieldatensatz. Der Seed darf beliebig oft +ausgeführt werden und ersetzt nur die von ihm verwalteten Beispieldaten. + +## Konfiguration + +| Variable | Vorgabe | Bedeutung | +| ---------------------- | ------------------------ | ----------------------------------- | +| `SENDEWERK_DB_PATH` | `.data/sendewerk.sqlite` | Pfad zur persistenten SQLite-Datei | +| `SENDEWERK_UPLOAD_DIR` | `.data/uploads` | Persistentes Upload-Verzeichnis | +| `HOST` | adapterabhängig | Bind-Adresse des Produktionsservers | +| `PORT` | `3000` | Port des Produktionsservers | + +Relative Sendewerk-Pfade werden vom Projektverzeichnis aus aufgelöst. In +Cloudron liegen Datenbank und Uploads unter `/app/data`. + +## Befehle + +| Befehl | Zweck | +| -------------------- | ---------------------------------------------------------- | +| `npm run setup` | Verzeichnisse, Migrationen und Seed für lokale Entwicklung | +| `npm run db:migrate` | Ausstehende Migrationen atomar ausführen | +| `npm run db:seed` | Reproduzierbare Beispieldaten laden | +| `npm run dev` | Entwicklungsserver starten | +| `npm run check` | Svelte- und TypeScript-Prüfung | +| `npm run lint` | Formatierung und ESLint prüfen | +| `npm test` | Verhaltenstests mit temporärer, getrennter Datenbank | +| `npm run test:e2e` | Browserprüfungen gegen eine getrennte E2E-Datenbank | +| `npm run build` | Produktionsartefakt in `build/` erstellen | +| `npm start` | gebautes Artefakt mit `node build` starten | + +## Lokaler Produktionslauf + +```sh +npm ci +npm run setup +npm run build +HOST=127.0.0.1 PORT=3000 npm start +``` + +Der Healthcheck liegt unter und prüft auch den +Datenbankzugriff. + +Vor der ersten Browserprüfung wird der Testbrowser einmalig installiert: + +```sh +npx playwright install chromium +npm run test:e2e +``` + +## Container und Cloudron + +Das frühe Cloudron-Paket besteht aus `Dockerfile`, `CloudronManifest.json` und +`start.sh`. Ein lokaler Container-Smoke-Test kann nach dem Build so erfolgen: + +```sh +docker build -t sendewerk:local . +docker run --rm -p 3000:3000 -v sendewerk-data:/app/data sendewerk:local +``` + +Das ersetzt keinen Test auf einem echten Cloudron. Insbesondere Addon-Backup, +Restore, Proxy-Origin und das read-only Laufzeitdateisystem können erst dort +abschließend geprüft werden. + +## Dokumentation + +- `docs/ARCHITECTURE.md`: Systemgrenzen und geplanter Gesamtdatenbestand +- `docs/DECISIONS.md`: technische Entscheidungen und offizielle Quellen +- `docs/ACCEPTANCE.md`: Abnahmekriterien und konkrete Nachweise +- `docs/STATUS.md`: aktueller Projektstand und bekannte Grenzen + +Der Anwendungscode steht unter der MIT-Lizenz. Rechte an später eingestellten +Medien und redaktionellen Inhalten werden dadurch nicht verändert. + +Die direkten Laufzeit- und Werkzeugabhängigkeiten verwenden MIT- oder +Apache-2.0-Lizenzen: Svelte/SvelteKit, Vite, Vitest, ESLint und +`better-sqlite3` sind MIT-lizenziert; TypeScript und Playwright stehen unter +Apache-2.0. Die Lockdatei fixiert den tatsächlich installierten Baum. diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md new file mode 100644 index 0000000..82c34f6 --- /dev/null +++ b/docs/ACCEPTANCE.md @@ -0,0 +1,38 @@ +# Abnahme + +## Etappe 1 + +| Kriterium | Nachweis | Stand | +| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| Frischer Checkout startet ohne Cloudron | `npm ci` erfolgreich; README-Setup und lokaler Produktionsstart ausgeführt | erfüllt am 21.10.2026 | +| Typprüfung und Build erfolgreich | `npm run check`, `npm run build` | erfüllt am 21.10.2026 | +| Theme bleibt nach Neuladen erhalten | Playwright: System auf Hell wechseln, Reload, DOM und Schalterzustand prüfen | erfüllt am 21.10.2026 | +| Testdaten überleben Neustart | Gebauten Server zweimal gegen `.data/sendewerk.sqlite` gestartet; Fundusdaten und `/healthz` jeweils vorhanden | erfüllt am 21.10.2026 | +| Migration wiederholt ausführbar | lokal mehrfach ausgeführt; Verhaltenstest führt Migration zweimal in temporärer DB aus | erfüllt am 21.10.2026 | +| Seed reproduzierbar | lokal und im Verhaltenstest zweimal ausgeführt; danach 4 Pieces, 1 Sendung und 3 Verwendungen | erfüllt am 21.10.2026 | +| Tests verwenden getrennte Daten | Vitest und Playwright erzeugen je eigene Datenbank im Betriebssystem-Tempdir | erfüllt am 21.10.2026 | +| Desktop und Smartphone ohne horizontales Scrollen | 4 Playwright-Abläufe; 1440x1000, 1280x900 und 390x844; Screenshots visuell kontrolliert | erfüllt am 21.10.2026 | +| Cloudron-Pfade und Healthcheck vorbereitet | Manifestpfad `/app/data/sendewerk.sqlite`, `/healthz`, `start.sh`; JSON-Kernfelder und Bash-Syntax geprüft | vorbereitet; Container offen | +| Echte Cloudron-Prüfung | Installation, Backup/Restore, Proxy und read-only Laufzeit | offen, kein Serverzugang | + +## Ausgeführte Prüfungen + +- `npm ci`: erfolgreich, 190 Pakete geprüft, 0 gemeldete Schwachstellen +- `npm run setup`, anschließend erneute Migration und erneuter Seed: erfolgreich +- `npm test`: 2 Verhaltenstests erfolgreich +- `npm run lint`: erfolgreich +- `npm run check`: 0 Fehler und 0 Warnungen +- `npm run build`: erfolgreich mit `@sveltejs/adapter-node` +- `npm run test:e2e`: 4 von 4 Browsertests erfolgreich +- Zwei getrennte `npm start`-Läufe: Healthcheck und persistente Daten erfolgreich +- `bash -n start.sh` und programmatische Manifest-Kernfeldprüfung: erfolgreich + +## Nicht erfolgreich oder nicht ausführbar + +- `docker build -t sendewerk:local .`: nicht ausgeführt, weil der installierte + Docker-Client keinen laufenden Daemon unter `/var/run/docker.sock` erreichen + konnte. +- Echte Cloudron-Installation: mangels Serverzugang nicht ausgeführt. +- Ein zusätzlicher Python-Playwright-Lauf: nicht gestartet, weil das optionale + Python-Modul `playwright` nicht installiert war. Die projektgebundene + TypeScript-Playwright-Suite lief stattdessen vollständig erfolgreich. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..f8bb81c --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,84 @@ +# Architektur + +## Systemgrenze + +Sendewerk ist ein einzelner SvelteKit-Prozess. Browserseiten, serverseitige +Actions, spätere REST-Endpunkte und Hintergrundaufgaben verwenden dieselbe +Geschäftslogik. SQLite und ein lokales Upload-Verzeichnis sind die einzigen +persistenten Laufzeitabhängigkeiten. Externe Mail-, RSS- oder KI-Dienste sind +nicht Voraussetzung für Kernfunktionen. + +```text +Browser -> SvelteKit-Routen -> Anwendungsdienste -> SQLite + | -> Upload-Verzeichnis + +-> später: Mail/RSS über Adapter +``` + +Servercode lebt unter `src/lib/server` und darf nicht in Browser-Bundles +gelangen. Routen liefern nur die für ihre Ansicht erlaubten Daten. Fachlogik +wird nicht an Svelte-Komponenten gebunden, damit Browseraktionen und die +spätere API dieselben Regeln verwenden. + +## Laufzeit und Speicher + +- Lokal: `.data/sendewerk.sqlite` und `.data/uploads`, jeweils konfigurierbar. +- Tests: pro Lauf ein temporäres Verzeichnis unter dem Betriebssystem-Tempdir. +- Cloudron: `/app/data/sendewerk.sqlite` und `/app/data/uploads`. +- Migrationen liegen nummeriert unter `migrations/`, laufen in Transaktionen + und werden in `schema_migrations` protokolliert. +- Der Seed nutzt stabile IDs und Transaktionen. Wiederholung liefert denselben + fachlichen Ausgangsbestand, ohne Migrationstabellen anzutasten. +- Zeitpunkte werden als ISO-8601-UTC-Text gespeichert. Dauern sind nichtnegative + ganze Sekunden. Zeitzonendarstellung ist `Europe/Berlin`. + +## Geplantes Gesamtdatenmodell + +Etappe 1 materialisiert nur den für echte Vorschau- und Seed-Daten benötigten +Kern. Folgende Grenzen sind für spätere Migrationen bereits festgelegt. + +| Bereich | Geplante Entitäten und Beziehungen | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------- | +| Identität | `users`, Rollen, Sessions, Einladungen, Reset-Tokens, Sperren und technische Zugänge; Tokens nur gehasht | +| Taxonomie | getrennte `section_categories`, `formats`, `topics`; Pieces und Vorschläge über Zuordnungstabellen mehrfach verknüpft | +| Inhalt | `pieces` mit Status und Dauer; Quellen, Medien, private Recherche, Moderation, Präsentation und Vorlagen als getrennte Daten | +| Sendungsbau | `shows`, geordnete `show_sections`, kopierte `piece_usages`, Reservebank und Sendungsvorlagen; Original-ID bleibt Referenz | +| Live | expliziter Live-Stand, aktuelles Element, Status je Verwendung, tatsächliche Zeiten und getrennte Präsentationsausgabe | +| Historie | unveränderliche Abschluss-Snapshots und Verwendungshistorie; Übersprungenes zählt nicht als gesendet | +| Freigaben | widerrufbare, gehashte Lesetokens mit Sendung, Umfang, Passwortoption und Ablaufzeit | +| RSS | Feedquellen, Abrufläufe und externe Einträge mit stabiler Herkunfts-ID sowie Status neu/übernommen/verworfen | +| API/Audit | technische Berechtigungen, Idempotenzschlüssel und Audit-Ereignisse mit Akteur, Aktion und Ziel | +| Betrieb | Anwendungseinstellungen und spätere Job-Zustände, keine Geheimnisse im Klartext ohne begründete Schutzstrategie | + +### Historien- und Kopierregel + +Beim Einplanen entsteht eine bearbeitbare `piece_usage`-Kopie der +sendungsrelevanten Texte und Dauer. Das Original bleibt verknüpft, wird aber +nicht live gespiegelt. Beim Abschluss wird der damalige Sendungsinhalt stabil +eingefroren. Eine historische Sendung wird dupliziert, nie weiterbearbeitet. + +### Dauerregel + +Eine Abschnittsdauer ist entweder die Summe ihrer eingeplanten Verwendungen +oder eine explizite Abschnittsdauer. Beides wird nie addiert. Reserveelemente +zählen erst nach dem Einplanen. Geplante und tatsächliche Dauer bleiben +getrennt. + +## Oberflächenstruktur + +Die Hauptnavigation umfasst Eingang, Fundus, Sendungen, Archiv und Verwaltung. +Auf breiten Ansichten stehen Fundus und Ablauf nebeneinander. Auf schmalen +Ansichten werden sie über eine gut erreichbare Umschaltung einzeln gezeigt. +Der Live-Modus ist eine eigene, fokussierte Ansicht; die spätere +Präsentationsausgabe erhält eine separate, strikt reduzierte Datenansicht. + +Etappe 1 zeigt Fundus, Sendungsbau und Live-Modus als sichtbare +`Gestaltungsvorschau`. Die Daten kommen bereits aus SQLite, mutierende +Fachabläufe werden jedoch nicht vorgetäuscht. + +## Sicherheitsgrenzen ab den Folgeetappen + +Authentifizierung und Autorisierung werden serverseitig an jeder Route und +Action geprüft. Markdown und fremdes HTML werden vor Ausgabe bereinigt. +Uploadzugriff folgt denselben Rechten. Netzwerkabrufe erhalten Schutz gegen +interne Ziele, Größen-/Zeitlimits und kontrollierte Redirects. Diese Regeln +sind geplant, aber nicht Bestandteil der Etappe-1-Vorschauen. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md new file mode 100644 index 0000000..7fce92a --- /dev/null +++ b/docs/DECISIONS.md @@ -0,0 +1,83 @@ +# Entscheidungen + +Stand der Quellenprüfung: 21. Oktober 2026. Verwendet wurden offizielle +Dokumentationen und die npm-Registry für konkrete Paket-Metadaten. + +## D-001: SvelteKit 3 und Node-Unterstützung + +Die Anwendung verwendet SvelteKit 3, Svelte 5, TypeScript und Vite 8. Das +Projekt wurde gemäß aktueller Empfehlung mit `npx sv create` angelegt. Die +Kit-Konfiguration liegt bei SvelteKit 3 in `vite.config.ts`, nicht in einer +`svelte.config.js`. + +Die lokale Mindestversion ist Node 22.17. Das Cloudron-Image nutzt Node 24 LTS. +Der vorhandene lokale Lauf wurde mit Node 22.23.1 geplant. + +Quellen: + +- +- +- +- npm-Metadaten für `@sveltejs/kit` 3.1.0 am 21.10.2026 + +## D-002: Offizieller Node-Adapter + +`@sveltejs/adapter-node` erzeugt den Produktionsserver in `build/`. Gebaut wird +mit `npm run build`, gestartet mit `node build`. `HOST` und `PORT` konfigurieren +den Server. Der Adapter behandelt SIGTERM/SIGINT und kontrolliertes Herunter- +fahren; ein eigener Server ist für Etappe 1 nicht erforderlich. + +Quelle: + +## D-003: SQLite mit better-sqlite3 + +`better-sqlite3` 13.x wird als gekapselter synchroner Treiber verwendet. Das +Paket ist MIT-lizenziert und verlangt Node 22 oder neuer. Gegenüber +`node:sqlite` wird damit in Etappe 1 keine API verwendet, die in der offiziellen +Node-24-Dokumentation noch als Release Candidate gekennzeichnet ist. Die +Datenbankgrenze bleibt in `src/lib/server` gekapselt. + +Migrationen sind eigene nummerierte SQL-Dateien und laufen transaktional. WAL +wird in Etappe 1 nicht aktiviert; für die kleine Redaktion ist das +Rollback-Journal zunächst ausreichend und vereinfacht die frühe +Betriebsprüfung. + +Quellen: + +- +- +- npm-Metadaten für `better-sqlite3` 13.0.3 am 21.10.2026 + +## D-004: Cloudron-Paket + +Das Paket nutzt `manifestVersion: 2`, internen HTTP-Port 3000 und `/healthz`. +`localstorage.sqlite.paths` nennt `/app/data/sendewerk.sqlite`, damit Cloudron +eine konsistente Sicherung der aktiven SQLite-Datei erstellen kann. Das +Startskript stellt Verzeichnisse und Besitzrechte her, migriert als Benutzer +`cloudron` und ersetzt sich zuletzt per `exec` durch den Node-Prozess. + +Das Image ist auf die am 21.10.2026 offiziell dokumentierte datierte Node-24- +Basis samt Digest gepinnt. Außer `/app/data`, `/run` und `/tmp` wird das +Laufzeitdateisystem als read-only behandelt. + +Quellen: + +- +- +- +- + +## D-005: Theme ohne serverseitiges Nutzerkonto + +Etappe 1 speichert `light`, `dark` oder `system` in `localStorage`. Ein kurzes +Inline-Skript in `app.html` setzt das resultierende Theme vor dem ersten Paint +auf dem Wurzelelement. Dies vermeidet einen falschen Theme-Blitz und benötigt +vor Etappe 2 weder Cookie noch Konto. Die Einstellung bleibt bei Neuladen im +gleichen Browser erhalten. + +## D-006: Etappe-1-Vorschauen statt Attrappen + +Vorschauseiten lesen echte persistente SQLite-Seed-Daten, bieten aber keine +funktionslosen Speichern- oder Live-Schaltflächen. Ein sichtbarer Hinweis +grenzt Gestaltungsvorschauen von den späteren Fachfunktionen ab. Damit wird +keine noch nicht implementierte Funktion als fertig dargestellt. diff --git a/docs/STATUS.md b/docs/STATUS.md new file mode 100644 index 0000000..35c52a2 --- /dev/null +++ b/docs/STATUS.md @@ -0,0 +1,43 @@ +# Projektstatus + +## Aktueller Umfang + +Etappe 1 ist abgeschlossen. Architektur, Gesamtdatenmodell, Entscheidungen und +lokaler Betriebsweg sind dokumentiert. Etappe 2 wurde nicht begonnen. + +## Umgesetzter Stand Etappe 1 + +- SvelteKit-/TypeScript-Monolith mit Node-Adapter +- konfigurierbare lokale SQLite- und Uploadpfade +- wiederholbare Migrationen und realistischer Seed +- persistentes Light-/Dark-/System-Theme +- responsive Navigation und gekennzeichnete Vorschauen für Fundus, + Sendungsbau und Live-Modus +- Healthcheck, Produktionsstart und frühes Cloudron-Paket +- getrennte Unit-/Integrations- und Browser-Testdaten + +Die Vorschauen lesen echte Daten aus SQLite. Suche und mobile Pane-Umschaltung +funktionieren lokal; redaktionelle Schreiboperationen bleiben bewusst den +späteren Etappen vorbehalten. + +## Nicht Bestandteil dieser Etappe + +Anmeldung, Rollen, echte Inhaltsbearbeitung, Autospeichern, Sendungsplanung, +Live-Steuerung, Präsentationsfenster, Archivabschluss, Freigaben, RSS und API. +Die Vorschauen dürfen nicht als Implementierung dieser Funktionen verstanden +werden. + +## Bekannte offene Betriebsprüfung + +Eine echte Cloudron-Installation samt Backup/Restore, Reverse-Proxy-Verhalten +und read-only Laufzeit konnte ohne bereitgestellten Cloudron-Server nicht +durchgeführt werden. Auch der lokale Container-Smoke-Test blieb offen, weil +auf der Prüfmaschine kein Docker-Daemon lief. Manifestkernfelder und +Startskript-Syntax wurden lokal geprüft. + +## Verifizierte Umgebung + +- Node.js 22.23.1, npm 10.9.8 +- SvelteKit 3.1.0, Svelte 5.57.2, Vite 8.3.5 +- SQLite-Treiber `better-sqlite3` 13.0.3 +- Playwright 1.64.1 mit Chromium 156.0.8078.4 (Fallback-Build für Ubuntu 24.04) diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..6227642 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,32 @@ +import prettier from 'eslint-config-prettier'; +import path from 'node:path'; +import js from '@eslint/js'; +import svelte from 'eslint-plugin-svelte'; +import { defineConfig, includeIgnoreFile } from 'eslint/config'; +import globals from 'globals'; +import ts from 'typescript-eslint'; + +const gitignorePath = path.resolve(import.meta.dirname, '.gitignore'); + +export default defineConfig( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ts.configs.recommended, + svelte.configs.recommended, + prettier, + svelte.configs.prettier, + { + languageOptions: { globals: { ...globals.browser, ...globals.node } }, + rules: { 'no-undef': 'off' } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser + } + } + } +); diff --git a/migrations/0001_editorial_core.sql b/migrations/0001_editorial_core.sql new file mode 100644 index 0000000..06b2b61 --- /dev/null +++ b/migrations/0001_editorial_core.sql @@ -0,0 +1,96 @@ +CREATE TABLE section_categories ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + color TEXT NOT NULL, + sort_order INTEGER NOT NULL CHECK (sort_order >= 0) +); + +CREATE TABLE formats ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE topics ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE pieces ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + summary TEXT NOT NULL DEFAULT '', + format_id TEXT REFERENCES formats(id) ON DELETE SET NULL, + status TEXT NOT NULL CHECK (status IN ('inbox', 'preparing', 'ready', 'archived')), + duration_seconds INTEGER NOT NULL DEFAULT 0 CHECK (duration_seconds >= 0), + intro TEXT NOT NULL DEFAULT '', + moderation_notes TEXT NOT NULL DEFAULT '', + transition_text TEXT NOT NULL DEFAULT '', + presentation_text TEXT NOT NULL DEFAULT '', + created_at_utc TEXT NOT NULL, + updated_at_utc TEXT NOT NULL +); + +CREATE TABLE piece_topics ( + piece_id TEXT NOT NULL REFERENCES pieces(id) ON DELETE CASCADE, + topic_id TEXT NOT NULL REFERENCES topics(id) ON DELETE CASCADE, + PRIMARY KEY (piece_id, topic_id) +); + +CREATE TABLE piece_category_suggestions ( + piece_id TEXT NOT NULL REFERENCES pieces(id) ON DELETE CASCADE, + category_id TEXT NOT NULL REFERENCES section_categories(id) ON DELETE CASCADE, + PRIMARY KEY (piece_id, category_id) +); + +CREATE TABLE piece_sources ( + id TEXT PRIMARY KEY, + piece_id TEXT NOT NULL REFERENCES pieces(id) ON DELETE CASCADE, + label TEXT NOT NULL, + url TEXT NOT NULL, + timecode_seconds INTEGER CHECK (timecode_seconds IS NULL OR timecode_seconds >= 0), + position INTEGER NOT NULL CHECK (position >= 0) +); + +CREATE TABLE shows ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + starts_at_utc TEXT NOT NULL, + twitch_title TEXT NOT NULL DEFAULT '', + announcement_text TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL CHECK (status IN ('draft', 'ready', 'live', 'completed')), + created_at_utc TEXT NOT NULL, + updated_at_utc TEXT NOT NULL +); + +CREATE TABLE show_sections ( + id TEXT PRIMARY KEY, + show_id TEXT NOT NULL REFERENCES shows(id) ON DELETE CASCADE, + name TEXT NOT NULL, + category_id TEXT REFERENCES section_categories(id) ON DELETE SET NULL, + position INTEGER NOT NULL CHECK (position >= 0), + explicit_duration_seconds INTEGER CHECK ( + explicit_duration_seconds IS NULL OR explicit_duration_seconds >= 0 + ), + UNIQUE (show_id, position) +); + +CREATE TABLE piece_usages ( + id TEXT PRIMARY KEY, + show_section_id TEXT NOT NULL REFERENCES show_sections(id) ON DELETE CASCADE, + original_piece_id TEXT REFERENCES pieces(id) ON DELETE SET NULL, + position INTEGER NOT NULL CHECK (position >= 0), + status TEXT NOT NULL CHECK (status IN ('open', 'current', 'done', 'skipped')), + duration_seconds INTEGER NOT NULL CHECK (duration_seconds >= 0), + title_snapshot TEXT NOT NULL, + intro_snapshot TEXT NOT NULL DEFAULT '', + moderation_snapshot TEXT NOT NULL DEFAULT '', + transition_snapshot TEXT NOT NULL DEFAULT '', + presentation_snapshot TEXT NOT NULL DEFAULT '', + is_reserve INTEGER NOT NULL DEFAULT 0 CHECK (is_reserve IN (0, 1)), + UNIQUE (show_section_id, position) +); + +CREATE INDEX idx_pieces_status ON pieces(status); +CREATE INDEX idx_shows_status_start ON shows(status, starts_at_utc); +CREATE INDEX idx_sections_show ON show_sections(show_id, position); +CREATE INDEX idx_usages_section ON piece_usages(show_section_id, position); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a022a60 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3230 @@ +{ + "name": "sendewerk", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sendewerk", + "version": "0.1.0", + "dependencies": { + "better-sqlite3": "^13.0.3" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.64.1", + "@sveltejs/adapter-node": "^6.0.0", + "@sveltejs/kit": "^3.1.0", + "@sveltejs/vite-plugin-svelte": "^7.2.0", + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^22.18.13", + "eslint": "^10.4.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.19.0", + "globals": "^17.6.0", + "prettier": "^3.8.3", + "prettier-plugin-svelte": "^4.1.0", + "svelte": "^5.57.2", + "svelte-check": "^4.6.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1", + "vite": "^8.3.5", + "vitest": "^5.0.5" + }, + "engines": { + "node": ">=22.17 <25" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@playwright/test": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz", + "integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.13.tgz", + "integrity": "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-6.0.0.tgz", + "integrity": "sha512-s7muM4JmJ75OZ8o0G4VRhrEKdi8twuEm3t2o/qQsdlm12hKaX7S/CuUBuw3n983wB/IFBSpzG2ugX0xdGzbKwg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^3.0.0-next.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-3.1.0.tgz", + "integrity": "sha512-JTWlq2HQlkqx6vIoxhC0R6sc4WVqbQoRyEmr5YlqKmiYbzY1U3awtSWTmjMLlS056a49AmkQR6rnNANXIE8B0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1", + "@standard-schema/spec": "^1.1.0", + "cookie": "^2.0.1", + "devalue": "^5.9.4", + "esm-env": "^1.2.2", + "magic-string": "^1.2.3", + "mrmime": "^2.0.1", + "sirv": "^3.0.2" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=22.17" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "svelte": "^5.57.1", + "typescript": "^6.0.0", + "vite": "^8.0.12" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.3.tgz", + "integrity": "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.3.1.tgz", + "integrity": "sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^1.0.0", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz", + "integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/type-utils": "8.72.0", + "@typescript-eslint/utils": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.72.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz", + "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz", + "integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz", + "integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.0", + "@typescript-eslint/types": "^8.72.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz", + "integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz", + "integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz", + "integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz", + "integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz", + "integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.0", + "@typescript-eslint/tsconfig-utils": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz", + "integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz", + "integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.5", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/better-sqlite3": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", + "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz", + "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.23.1.tgz", + "integrity": "sha512-+w86Jlx4IRUuuvtqD6j4njql+akx+81JnpvbN5de3AV5iq2SexvST/Jr51CiTlKeY0i+vhoKjPO/ywFKIP75sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.6.1", + "@jridgewell/sourcemap-codec": "^1.5.0", + "esutils": "^2.0.3", + "globals": "^16.0.0", + "known-css-properties": "^0.37.0", + "postcss": "^8.4.49", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.7.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-svelte/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.4.0.tgz", + "integrity": "sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", + "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", + "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-safe-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.1.0.tgz", + "integrity": "sha512-1WzZxRLaAFwEh6Do+zyGpjWV3nGJNxxhuh7Ubu/q1ICImMgZJnLwhoaEpRbG4pJppp2Y1ncL9ffA2f+LhrefQg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.7.tgz", + "integrity": "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-4.1.1.tgz", + "integrity": "sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^5.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rolldown": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.154.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/svelte": { + "version": "5.57.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.2.tgz", + "integrity": "sha512-1YiPCAXT0Ct6g1vOMmSLa0Nsc3QRMvtDF32ihgglGVrugIuIK38RyDiFNXVDhtQ81TJjSO57NdMw6DyYLqSGDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.6.0", + "@sveltejs/acorn-typescript": "^1.0.13", + "@types/estree": "^1.0.9", + "acorn": "^8.18.0", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.9.2", + "esm-env": "^1.2.1", + "esrap": "^2.3.6", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.7.tgz", + "integrity": "sha512-Ge2IMdeTpbJuzNFGRiiMg0RWgbScysKQu7xwcyFJ22+DU9F5nPX51+Q3gHmGqRxYQMx2knreVWt5oCtfe0vf2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.3", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.9.0.tgz", + "integrity": "sha512-aKdKQAZP7MobfVcQNyy5Yk0QjlC9m5T1lTS0qA7+0EIsFzFX3WgbGHfb8267QK+4hI8Az9+2f+hk5sp39op8sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0", + "pnpm": "10.34.6" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.0.tgz", + "integrity": "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.0", + "@typescript-eslint/parser": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.5.tgz", + "integrity": "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..99bc18e --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "sendewerk", + "private": true, + "version": "0.1.0", + "type": "module", + "engines": { + "node": ">=22.17 <25" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "start": "npm run db:migrate && node --env-file-if-exists=.env build", + "setup": "npm run db:migrate && npm run db:seed", + "db:migrate": "node --env-file-if-exists=.env scripts/migrate.mjs", + "db:seed": "node --env-file-if-exists=.env scripts/seed.mjs", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check . && eslint .", + "format": "prettier --write .", + "test:unit": "vitest", + "test": "npm run test:unit -- --run", + "test:e2e": "playwright test" + }, + "dependencies": { + "better-sqlite3": "^13.0.3" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.64.1", + "@sveltejs/adapter-node": "^6.0.0", + "@sveltejs/kit": "^3.1.0", + "@sveltejs/vite-plugin-svelte": "^7.2.0", + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^22.18.13", + "eslint": "^10.4.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.19.0", + "globals": "^17.6.0", + "prettier": "^3.8.3", + "prettier-plugin-svelte": "^4.1.0", + "svelte": "^5.57.2", + "svelte-check": "^4.6.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1", + "vite": "^8.3.5", + "vitest": "^5.0.5" + }, + "imports": { + "#lib": "./src/lib/index.js", + "#lib/*": "./src/lib/*" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..aa9802b --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from '@playwright/test'; +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; + +const dataDirectory = mkdtempSync(path.join(tmpdir(), 'sendewerk-e2e-')); + +export default defineConfig({ + testDir: 'tests', + testMatch: '**/*.e2e.ts', + fullyParallel: false, + workers: 1, + webServer: { + command: 'npm run setup && npm run build && npm start', + url: 'http://127.0.0.1:4173/healthz', + timeout: 120_000, + reuseExistingServer: false, + env: { + SENDEWERK_DB_PATH: path.join(dataDirectory, 'e2e.sqlite'), + SENDEWERK_UPLOAD_DIR: path.join(dataDirectory, 'uploads'), + HOST: '127.0.0.1', + PORT: '4173' + } + }, + use: { + baseURL: 'http://127.0.0.1:4173', + browserName: 'chromium', + trace: 'retain-on-failure' + } +}); diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..dcf8389 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,9 @@ +/** @type {import('prettier').Config} */ +export default { + useTabs: true, + singleQuote: true, + trailingComma: 'none', + printWidth: 100, + plugins: ['prettier-plugin-svelte'], + overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }] +}; diff --git a/scripts/config.mjs b/scripts/config.mjs new file mode 100644 index 0000000..8f3433f --- /dev/null +++ b/scripts/config.mjs @@ -0,0 +1,12 @@ +import path from 'node:path'; + +function configuredPath(value, fallback) { + return path.resolve(process.cwd(), value || fallback); +} + +export function getPaths() { + return { + database: configuredPath(process.env.SENDEWERK_DB_PATH, '.data/sendewerk.sqlite'), + uploads: configuredPath(process.env.SENDEWERK_UPLOAD_DIR, '.data/uploads') + }; +} diff --git a/scripts/migrate.mjs b/scripts/migrate.mjs new file mode 100644 index 0000000..6b3befb --- /dev/null +++ b/scripts/migrate.mjs @@ -0,0 +1,57 @@ +import Database from 'better-sqlite3'; +import { mkdirSync, readdirSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getPaths } from './config.mjs'; + +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +export function migrate(databasePath = getPaths().database) { + mkdirSync(path.dirname(databasePath), { recursive: true }); + mkdirSync(getPaths().uploads, { recursive: true }); + + const database = new Database(databasePath); + database.pragma('foreign_keys = ON'); + database.exec(` + CREATE TABLE IF NOT EXISTS schema_migrations ( + name TEXT PRIMARY KEY, + applied_at_utc TEXT NOT NULL + ) + `); + + const applied = new Set( + database + .prepare('SELECT name FROM schema_migrations') + .all() + .map((row) => row.name) + ); + const migrations = readdirSync(path.join(projectRoot, 'migrations')) + .filter((name) => /^\d+.+\.sql$/.test(name)) + .sort(); + + for (const name of migrations) { + if (applied.has(name)) continue; + + const sql = readFileSync(path.join(projectRoot, 'migrations', name), 'utf8'); + database.exec('BEGIN IMMEDIATE'); + try { + database.exec(sql); + database + .prepare('INSERT INTO schema_migrations (name, applied_at_utc) VALUES (?, ?)') + .run(name, new Date().toISOString()); + database.exec('COMMIT'); + console.log(`Migration applied: ${name}`); + } catch (error) { + database.exec('ROLLBACK'); + database.close(); + throw error; + } + } + + database.close(); + console.log(`Database ready: ${databasePath}`); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + migrate(); +} diff --git a/scripts/seed.mjs b/scripts/seed.mjs new file mode 100644 index 0000000..3ac2eaf --- /dev/null +++ b/scripts/seed.mjs @@ -0,0 +1,286 @@ +import Database from 'better-sqlite3'; +import { mkdirSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getPaths } from './config.mjs'; + +export function seed(databasePath = getPaths().database) { + mkdirSync(path.dirname(databasePath), { recursive: true }); + const database = new Database(databasePath); + database.pragma('foreign_keys = ON'); + + const seedDatabase = database.transaction(() => { + const replace = (table, columns, rows) => { + const placeholders = columns.map(() => '?').join(', '); + const update = columns + .filter((column) => column !== 'id') + .map((column) => `${column} = excluded.${column}`) + .join(', '); + const statement = database.prepare( + `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders}) + ON CONFLICT(id) DO UPDATE SET ${update}` + ); + for (const row of rows) statement.run(...row); + }; + + replace( + 'section_categories', + ['id', 'name', 'color', 'sort_order'], + [ + ['cat-jc', 'JC', '#67DDE8', 0], + ['cat-reaction', 'Reaction', '#E7B957', 1], + ['cat-game', 'Spiel', '#9B8AFB', 2] + ] + ); + replace( + 'formats', + ['id', 'name'], + [ + ['format-find', 'Fundstück'], + ['format-talk', 'Gespräch'], + ['format-quiz', 'Quiz'] + ] + ); + replace( + 'topics', + ['id', 'name'], + [ + ['topic-gaming', 'Gaming'], + ['topic-film', 'Film & Serien'], + ['topic-internet', 'Netzkultur'] + ] + ); + + const timestamp = '2026-10-21T10:00:00.000Z'; + replace( + 'pieces', + [ + 'id', + 'title', + 'summary', + 'format_id', + 'status', + 'duration_seconds', + 'intro', + 'moderation_notes', + 'transition_text', + 'presentation_text', + 'created_at_utc', + 'updated_at_utc' + ], + [ + [ + 'piece-console', + 'Die kleine Konsole mit dem großen Dreh', + 'Ein ungewöhnliches Handheld-Konzept sorgt für Diskussionsstoff.', + 'format-find', + 'ready', + 480, + 'Eine Kurbel, ein gelber Knopf und sehr viel Charme: Was macht kleine Hardware spannend?', + 'Erst Formfaktor zeigen, dann über bewusst gesetzte Grenzen sprechen.', + 'Von der kleinen Konsole geht es zur ganz großen Leinwand.', + 'Weniger Leistung, mehr Idee.', + timestamp, + timestamp + ], + [ + 'piece-effects', + 'Warum praktische Effekte wieder auffallen', + 'Neue Produktionen mischen Miniaturen, Masken und digitale Nacharbeit.', + 'format-talk', + 'ready', + 720, + 'Man sieht manchmal gerade deshalb weniger Trick, weil wieder mehr vor der Kamera passiert.', + 'Beispiele vergleichen, nicht analog gegen digital ausspielen.', + 'Nach so viel Handarbeit wird es spielerisch.', + 'Wenn das Licht wirklich auf ein Modell fällt.', + timestamp, + timestamp + ], + [ + 'piece-quiz', + 'Drei Sounds, ein Spiel', + 'Kurzes Audioquiz mit bekannten Inventar- und Menüklängen.', + 'format-quiz', + 'preparing', + 600, + 'Augen zu: Erkennt ihr das Spiel nur an seinem Inventar?', + 'Drei Runden, Auflösung jeweils erst nach den Tipps.', + 'Damit sind die Ohren warm für unser erstes Spiel.', + 'Welches Spiel steckt hinter diesem Klang?', + timestamp, + timestamp + ], + [ + 'piece-pixels', + 'Pixelart, die nicht nostalgisch sein will', + 'Wie moderne Spiele Pixel als Material statt als Rückblick einsetzen.', + 'format-find', + 'ready', + 540, + 'Pixel sind längst keine technische Notlösung mehr.', + 'Großaufnahmen vorbereiten, drei Stilmittel benennen.', + 'Genug Theorie, jetzt spielen wir selbst.', + 'Pixel als bewusste Entscheidung.', + timestamp, + timestamp + ] + ] + ); + + database.prepare('DELETE FROM piece_topics WHERE piece_id LIKE ?').run('piece-%'); + const addTopic = database.prepare( + 'INSERT INTO piece_topics (piece_id, topic_id) VALUES (?, ?)' + ); + for (const values of [ + ['piece-console', 'topic-gaming'], + ['piece-effects', 'topic-film'], + ['piece-quiz', 'topic-gaming'], + ['piece-pixels', 'topic-gaming'], + ['piece-pixels', 'topic-internet'] + ]) + addTopic.run(...values); + + database.prepare('DELETE FROM piece_category_suggestions WHERE piece_id LIKE ?').run('piece-%'); + const addCategory = database.prepare( + 'INSERT INTO piece_category_suggestions (piece_id, category_id) VALUES (?, ?)' + ); + for (const values of [ + ['piece-console', 'cat-jc'], + ['piece-effects', 'cat-reaction'], + ['piece-quiz', 'cat-jc'], + ['piece-pixels', 'cat-reaction'] + ]) + addCategory.run(...values); + + replace( + 'piece_sources', + ['id', 'piece_id', 'label', 'url', 'timecode_seconds', 'position'], + [ + [ + 'source-console', + 'piece-console', + 'Herstellerseite', + 'https://example.com/console', + null, + 0 + ], + [ + 'source-effects', + 'piece-effects', + 'Werkstattbericht', + 'https://example.com/effects', + 95, + 0 + ], + ['source-pixels', 'piece-pixels', 'Galerie', 'https://example.com/pixels', null, 0] + ] + ); + + replace( + 'shows', + [ + 'id', + 'title', + 'starts_at_utc', + 'twitch_title', + 'announcement_text', + 'status', + 'created_at_utc', + 'updated_at_utc' + ], + [ + [ + 'show-october', + 'Herbstsendung: Kleine Welten', + '2026-10-23T18:00:00.000Z', + 'Kleine Welten, große Ideen | heinzlive', + 'Heute schauen wir auf Hardware mit Kurbel, handgemachte Filmtricks und Pixel mit Haltung.', + 'ready', + timestamp, + timestamp + ] + ] + ); + + replace( + 'show_sections', + ['id', 'show_id', 'name', 'category_id', 'position', 'explicit_duration_seconds'], + [ + ['section-start', 'show-october', 'Ankommen & Warmwerden', 'cat-jc', 0, 300], + ['section-stories', 'show-october', 'Kleine Welten', 'cat-reaction', 1, null], + ['section-game', 'show-october', 'Spiel 1', 'cat-game', 2, 3600] + ] + ); + + replace( + 'piece_usages', + [ + 'id', + 'show_section_id', + 'original_piece_id', + 'position', + 'status', + 'duration_seconds', + 'title_snapshot', + 'intro_snapshot', + 'moderation_snapshot', + 'transition_snapshot', + 'presentation_snapshot', + 'is_reserve' + ], + [ + [ + 'usage-console', + 'section-stories', + 'piece-console', + 0, + 'current', + 480, + 'Die kleine Konsole mit dem großen Dreh', + 'Eine Kurbel, ein gelber Knopf und sehr viel Charme: Was macht kleine Hardware spannend?', + 'Erst Formfaktor zeigen, dann über bewusst gesetzte Grenzen sprechen.', + 'Von der kleinen Konsole geht es zur ganz großen Leinwand.', + 'Weniger Leistung, mehr Idee.', + 0 + ], + [ + 'usage-effects', + 'section-stories', + 'piece-effects', + 1, + 'open', + 720, + 'Warum praktische Effekte wieder auffallen', + 'Man sieht manchmal gerade deshalb weniger Trick, weil wieder mehr vor der Kamera passiert.', + 'Beispiele vergleichen, nicht analog gegen digital ausspielen.', + 'Nach so viel Handarbeit wird es spielerisch.', + 'Wenn das Licht wirklich auf ein Modell fällt.', + 0 + ], + [ + 'usage-reserve', + 'section-stories', + 'piece-pixels', + 2, + 'open', + 540, + 'Pixelart, die nicht nostalgisch sein will', + 'Pixel sind längst keine technische Notlösung mehr.', + 'Großaufnahmen vorbereiten, drei Stilmittel benennen.', + 'Genug Theorie, jetzt spielen wir selbst.', + 'Pixel als bewusste Entscheidung.', + 1 + ] + ] + ); + }); + + seedDatabase(); + database.close(); + console.log(`Example data ready: ${databasePath}`); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + seed(); +} diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..0cf4695 --- /dev/null +++ b/src/app.css @@ -0,0 +1,1350 @@ +:root { + font-family: + Inter, + ui-sans-serif, + -apple-system, + BlinkMacSystemFont, + 'SF Pro Display', + 'Segoe UI', + sans-serif; + font-synthesis: none; + color-scheme: light; + --bg: #f3f1ec; + --bg-elevated: rgba(255, 255, 255, 0.78); + --panel: #fffefa; + --panel-strong: #ffffff; + --panel-soft: #ebe9e3; + --text: #20211f; + --text-muted: #6e706b; + --text-faint: #969891; + --line: rgba(35, 36, 33, 0.1); + --line-strong: rgba(35, 36, 33, 0.16); + --accent: #225a4e; + --accent-soft: #dfece7; + --accent-text: #173e36; + --gold: #c99530; + --danger: #a94a43; + --shadow: 0 18px 45px rgba(56, 52, 44, 0.08); + --sidebar-width: 15rem; + --radius-large: 1.5rem; + --radius-medium: 1rem; + --radius-small: 0.7rem; +} + +:root[data-theme='dark'] { + color-scheme: dark; + --bg: #151714; + --bg-elevated: rgba(31, 34, 30, 0.82); + --panel: #20231f; + --panel-strong: #272a26; + --panel-soft: #2b2e29; + --text: #f1f0ea; + --text-muted: #b1b4ab; + --text-faint: #858980; + --line: rgba(244, 243, 236, 0.1); + --line-strong: rgba(244, 243, 236, 0.18); + --accent: #74b9a7; + --accent-soft: #203d36; + --accent-text: #d9f3eb; + --gold: #e7b957; + --danger: #e48a81; + --shadow: 0 20px 55px rgba(0, 0, 0, 0.24); +} + +* { + box-sizing: border-box; +} + +html { + min-width: 20rem; + background: var(--bg); + scrollbar-gutter: stable; +} + +body { + margin: 0; + min-width: 20rem; + min-height: 100vh; + background: + radial-gradient( + circle at 94% 4%, + color-mix(in srgb, var(--accent) 7%, transparent), + transparent 24rem + ), + var(--bg); + color: var(--text); + font-size: 0.95rem; + line-height: 1.5; +} + +button, +input { + font: inherit; +} + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +button { + color: inherit; +} + +a { + color: inherit; + text-decoration: none; +} + +:focus-visible { + outline: 3px solid color-mix(in srgb, var(--accent) 72%, white); + outline-offset: 3px; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.app-shell { + min-height: 100vh; +} + +.sidebar { + position: fixed; + inset: 0 auto 0 0; + z-index: 20; + display: flex; + width: var(--sidebar-width); + flex-direction: column; + padding: 1.5rem 1rem 1.1rem; + border-right: 1px solid var(--line); + background: color-mix(in srgb, var(--bg-elevated) 90%, transparent); + backdrop-filter: blur(24px) saturate(1.3); +} + +.brand { + display: flex; + align-items: center; + gap: 0.7rem; + min-height: 2.7rem; + padding: 0 0.65rem; + font-size: 1.12rem; + font-weight: 720; + letter-spacing: -0.025em; +} + +.brand-mark { + display: flex; + width: 1.65rem; + height: 1.65rem; + align-items: flex-end; + justify-content: center; + gap: 0.14rem; + padding: 0.37rem; + border-radius: 0.53rem; + background: var(--accent); + box-shadow: 0 6px 16px color-mix(in srgb, var(--accent) 28%, transparent); +} + +.brand-mark i { + display: block; + width: 0.16rem; + border-radius: 1rem; + background: var(--panel-strong); +} + +.brand-mark i:nth-child(1) { + height: 0.46rem; +} +.brand-mark i:nth-child(2) { + height: 0.78rem; +} +.brand-mark i:nth-child(3) { + height: 0.6rem; +} + +.main-navigation { + display: grid; + gap: 0.28rem; + margin-top: 2.25rem; +} + +.main-navigation a { + display: flex; + align-items: center; + gap: 0.82rem; + min-height: 2.85rem; + padding: 0.65rem 0.8rem; + border-radius: 0.8rem; + color: var(--text-muted); + font-weight: 590; + transition: + background-color 140ms ease, + color 140ms ease, + transform 140ms ease; +} + +.main-navigation a:hover { + background: color-mix(in srgb, var(--panel-strong) 66%, transparent); + color: var(--text); +} + +.main-navigation a.active { + background: var(--panel-strong); + box-shadow: 0 7px 20px rgba(48, 48, 43, 0.07); + color: var(--accent); +} + +.sidebar-footer { + display: grid; + gap: 0.85rem; + margin-top: auto; +} + +.theme-toggle { + display: flex; + width: 100%; + align-items: center; + gap: 0.65rem; + padding: 0.65rem 0.72rem; + border: 1px solid var(--line); + border-radius: 0.75rem; + background: transparent; + color: var(--text-muted); + cursor: pointer; + font-size: 0.82rem; + font-weight: 620; +} + +.theme-toggle:hover { + background: var(--panel); + color: var(--text); +} + +.theme-symbol { + display: grid; + width: 1.4rem; + height: 1.4rem; + place-items: center; + border-radius: 50%; + color: var(--gold); + font-size: 1.12rem; + line-height: 1; +} + +.stage-indicator { + display: flex; + align-items: center; + gap: 0.65rem; + padding: 0.72rem; + color: var(--text-muted); +} + +.stage-indicator > span { + width: 0.48rem; + height: 0.48rem; + border-radius: 50%; + background: #58a66b; + box-shadow: 0 0 0 0.25rem color-mix(in srgb, #58a66b 14%, transparent); +} + +.stage-indicator div { + display: grid; +} +.stage-indicator strong { + color: var(--text); + font-size: 0.75rem; +} +.stage-indicator small { + font-size: 0.69rem; +} + +.mobile-header, +.mobile-navigation { + display: none; +} + +#main-content { + min-height: 100vh; + margin-left: var(--sidebar-width); +} + +.page-wrap { + width: min(100%, 92rem); + margin: 0 auto; + padding: 3.4rem clamp(2rem, 4vw, 4.6rem) 5rem; +} + +.narrow-page { + max-width: 74rem; +} + +.page-heading { + margin-bottom: 1.75rem; +} + +.page-heading h1, +.live-header h1 { + max-width: 18ch; + margin: 0.22rem 0 0; + font-size: clamp(2rem, 4vw, 3.65rem); + font-weight: 690; + letter-spacing: -0.055em; + line-height: 1.02; +} + +.eyebrow { + margin: 0; + color: var(--accent); + font-size: 0.72rem; + font-weight: 760; + letter-spacing: 0.115em; + text-transform: uppercase; +} + +.page-intro { + max-width: 42rem; + margin: 0.9rem 0 0; + color: var(--text-muted); + font-size: 1.04rem; +} + +.split-heading, +.show-heading, +.live-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 2rem; +} + +.heading-stat { + display: grid; + min-width: 6.5rem; + padding: 0.8rem 1rem; + border-left: 1px solid var(--line-strong); +} + +.heading-stat strong { + font-size: 1.7rem; + line-height: 1; +} +.heading-stat span { + margin-top: 0.25rem; + color: var(--text-muted); + font-size: 0.78rem; +} + +.preview-notice { + display: flex; + align-items: flex-start; + gap: 0.8rem; + margin: 0 0 1.3rem; + padding: 0.85rem 1rem; + border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--line)); + border-radius: var(--radius-small); + background: color-mix(in srgb, var(--accent-soft) 45%, transparent); + color: var(--accent-text); +} + +.preview-notice strong { + font-size: 0.79rem; +} +.preview-notice p { + margin: 0.08rem 0 0; + color: var(--text-muted); + font-size: 0.76rem; +} +.preview-dot { + flex: 0 0 auto; + width: 0.55rem; + height: 0.55rem; + margin-top: 0.38rem; + border: 2px solid var(--accent); + border-radius: 50%; +} + +.library-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.25rem; +} + +.search-field, +.compact-search { + display: flex; + align-items: center; + gap: 0.6rem; + border: 1px solid var(--line); + background: var(--panel); + color: var(--text-muted); +} + +.search-field { + width: min(100%, 28rem); + height: 2.85rem; + padding: 0 0.9rem; + border-radius: 0.88rem; + box-shadow: 0 6px 20px rgba(48, 48, 43, 0.035); +} + +.search-field span:first-child, +.compact-search span:first-child { + font-size: 1.35rem; + line-height: 1; + transform: rotate(-15deg); +} + +.search-field input, +.compact-search input { + width: 100%; + border: 0; + outline: 0; + background: transparent; + color: var(--text); +} + +.search-field input::placeholder, +.compact-search input::placeholder { + color: var(--text-faint); +} + +.filter-tabs, +.mobile-pane-switch { + display: flex; + padding: 0.22rem; + border-radius: 0.76rem; + background: var(--panel-soft); +} + +.filter-tabs button, +.mobile-pane-switch button { + padding: 0.48rem 0.82rem; + border: 0; + border-radius: 0.58rem; + background: transparent; + color: var(--text-muted); + cursor: pointer; + font-size: 0.78rem; + font-weight: 640; +} + +.filter-tabs button.active, +.mobile-pane-switch button.active { + background: var(--panel-strong); + box-shadow: 0 3px 10px rgba(40, 40, 36, 0.08); + color: var(--text); +} + +.piece-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.piece-card { + display: flex; + min-width: 0; + min-height: 17rem; + flex-direction: column; + padding: 1.25rem; + border: 1px solid var(--line); + border-radius: var(--radius-large); + background: var(--panel); + box-shadow: 0 8px 25px rgba(55, 52, 46, 0.035); + transition: + border-color 150ms ease, + transform 150ms ease, + box-shadow 150ms ease; +} + +.piece-card:hover { + transform: translateY(-2px); + border-color: var(--line-strong); + box-shadow: var(--shadow); +} + +.piece-card-topline, +.source-card > div, +.live-kicker { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.7rem; +} + +.category-label { + display: inline-flex; + align-items: center; + gap: 0.42rem; + color: var(--text-muted); + font-size: 0.68rem; + font-weight: 720; + letter-spacing: 0.075em; + text-transform: uppercase; +} + +.category-label i { + display: inline-block; + width: 0.48rem; + height: 0.48rem; + border-radius: 50%; + background: var(--category); + box-shadow: 0 0 0 0.18rem color-mix(in srgb, var(--category) 16%, transparent); +} + +.status-label { + padding: 0.24rem 0.5rem; + border-radius: 2rem; + background: var(--panel-soft); + color: var(--text-muted); + font-size: 0.66rem; + font-weight: 680; +} + +.status-label.ready { + background: var(--accent-soft); + color: var(--accent-text); +} + +.piece-card h2 { + margin: 1.35rem 0 0.55rem; + font-size: 1.35rem; + font-weight: 670; + letter-spacing: -0.025em; + line-height: 1.18; +} + +.piece-card > p { + margin: 0; + color: var(--text-muted); + font-size: 0.88rem; +} + +.piece-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 1.1rem; +} + +.piece-tags span { + padding: 0.28rem 0.52rem; + border: 1px solid var(--line); + border-radius: 2rem; + color: var(--text-muted); + font-size: 0.67rem; +} + +.piece-card footer { + display: flex; + justify-content: space-between; + gap: 1rem; + margin-top: auto; + padding-top: 1.1rem; + border-top: 1px solid var(--line); + color: var(--text-faint); + font-size: 0.72rem; +} + +.piece-card footer b { + color: var(--text); + font-weight: 680; +} + +.empty-state, +.planned-panel { + padding: clamp(2rem, 7vw, 5rem); + text-align: center; +} + +.empty-state { + border: 1px dashed var(--line-strong); + border-radius: var(--radius-large); +} + +.empty-state > span { + color: var(--text-faint); + font-size: 2rem; +} +.empty-state h2 { + margin-bottom: 0.3rem; +} +.empty-state p { + color: var(--text-muted); +} +.empty-state button { + padding: 0.65rem 0.9rem; + border: 0; + border-radius: 0.65rem; + background: var(--accent); + color: var(--panel-strong); + cursor: pointer; + font-weight: 650; +} + +.show-summary { + display: flex; + align-items: stretch; + gap: 0.65rem; +} + +.show-summary > div { + display: grid; + min-width: 5.8rem; + padding: 0.6rem 0.85rem; + border-left: 1px solid var(--line-strong); +} + +.show-summary strong { + font-size: 1rem; +} +.show-summary span { + color: var(--text-muted); + font-size: 0.68rem; +} + +.live-link { + display: flex; + align-items: center; + gap: 0.55rem; + padding: 0.7rem 0.9rem; + border-radius: 0.72rem; + background: var(--text); + color: var(--bg); + font-size: 0.78rem; + font-weight: 680; +} + +.live-link span { + color: inherit; + font-size: 1rem; +} + +.mobile-pane-switch { + display: none; +} + +.builder-workspace { + display: grid; + grid-template-columns: minmax(18rem, 0.78fr) minmax(28rem, 1.35fr); + gap: 1rem; + align-items: start; +} + +.builder-pane { + min-width: 0; + padding: 1.15rem; + border: 1px solid var(--line); + border-radius: var(--radius-large); + background: var(--panel); +} + +.pane-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +.pane-heading h2 { + margin: 0.12rem 0 0; + font-size: 1.12rem; + letter-spacing: -0.02em; +} +.pane-heading > span { + padding: 0.3rem 0.55rem; + border-radius: 2rem; + background: var(--panel-soft); + color: var(--text-muted); + font-size: 0.7rem; + font-weight: 680; +} + +.compact-search { + height: 2.5rem; + padding: 0 0.75rem; + border-radius: 0.72rem; +} + +.compact-search:has(input:disabled) { + opacity: 0.72; +} + +.source-list { + display: grid; + gap: 0.55rem; + margin-top: 0.8rem; +} + +.source-card { + padding: 0.9rem; + border: 1px solid var(--line); + border-radius: 0.9rem; + background: var(--panel-strong); +} + +.source-card > div > span:last-child { + color: var(--text-faint); + font-size: 0.68rem; +} +.source-card h3 { + margin: 0.72rem 0 0.2rem; + font-size: 0.88rem; + line-height: 1.25; +} +.source-card p { + margin: 0; + color: var(--text-muted); + font-size: 0.7rem; +} + +.reserve-box { + margin-top: 0.8rem; + padding: 0.9rem; + border: 1px dashed color-mix(in srgb, var(--gold) 48%, var(--line)); + border-radius: 0.9rem; + background: color-mix(in srgb, var(--gold) 7%, transparent); +} + +.reserve-box > div { + display: flex; + align-items: center; + gap: 0.65rem; +} +.reserve-box > div > span { + color: var(--gold); + font-size: 1.2rem; +} +.reserve-box div div { + display: grid; +} +.reserve-box strong { + font-size: 0.78rem; +} +.reserve-box small { + color: var(--text-muted); + font-size: 0.67rem; +} +.reserve-box p { + margin: 0.65rem 0 0; + color: var(--text-muted); + font-size: 0.7rem; +} + +.run-list { + display: grid; + gap: 0.7rem; +} + +.run-section { + padding: 0.95rem; + border: 1px solid var(--line); + border-radius: 1rem; + background: var(--panel-strong); +} + +.run-section > header { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 0.8rem; +} + +.run-section > header > strong { + color: var(--text-muted); + font-size: 0.72rem; +} +.run-section h3 { + margin: 0.13rem 0 0; + font-size: 0.95rem; + letter-spacing: -0.012em; +} + +.section-number { + display: grid; + width: 2rem; + height: 2rem; + place-items: center; + border-radius: 0.65rem; + background: color-mix(in srgb, var(--category) 16%, var(--panel-soft)); + color: var(--text-muted); + font-size: 0.67rem; + font-weight: 750; +} + +.usage-list { + display: grid; + gap: 0.42rem; + margin: 0.8rem 0 0 2.8rem; +} + +.usage-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 0.65rem; + padding: 0.68rem; + border-radius: 0.72rem; + background: var(--panel-soft); +} + +.drag-hint { + color: var(--text-faint); + font-size: 1.05rem; +} +.usage-row div { + display: grid; + min-width: 0; +} +.usage-row strong { + overflow: hidden; + font-size: 0.78rem; + text-overflow: ellipsis; + white-space: nowrap; +} +.usage-row small { + color: var(--text-muted); + font-size: 0.65rem; +} +.usage-status { + padding: 0.2rem 0.4rem; + border-radius: 2rem; + color: var(--text-muted); + font-size: 0.61rem; + font-weight: 690; +} +.usage-status.current { + background: var(--accent-soft); + color: var(--accent-text); +} +.section-placeholder { + margin: 0.75rem 0 0 2.8rem; + padding: 0.65rem; + border: 1px dashed var(--line-strong); + border-radius: 0.7rem; + color: var(--text-muted); + font-size: 0.7rem; +} + +.live-page { + max-width: 88rem; +} +.live-header { + margin-bottom: 1.5rem; +} +.live-header h1 { + max-width: 24ch; + font-size: clamp(1.65rem, 3vw, 2.65rem); +} + +.live-pulse { + display: inline-block; + width: 0.48rem; + height: 0.48rem; + margin-right: 0.35rem; + border-radius: 50%; + background: #d65c55; + box-shadow: 0 0 0 0.25rem color-mix(in srgb, #d65c55 15%, transparent); +} + +.live-header-actions { + display: flex; + align-items: center; + gap: 0.7rem; +} +.live-header-actions > a { + color: var(--text-muted); + font-size: 0.75rem; + font-weight: 650; +} +.font-control { + display: flex; + padding: 0.18rem; + border: 1px solid var(--line); + border-radius: 0.65rem; + background: var(--panel); +} +.font-control button { + width: 2.25rem; + height: 2rem; + border: 0; + border-radius: 0.5rem; + background: transparent; + cursor: pointer; + font-size: 0.75rem; + font-weight: 700; +} +.font-control button:hover { + background: var(--panel-soft); +} + +.live-grid { + display: grid; + grid-template-columns: minmax(0, 1.65fr) minmax(16rem, 0.7fr); + gap: 1rem; + align-items: start; +} + +.live-current { + min-width: 0; + padding: clamp(1.4rem, 4vw, 3rem); + border: 1px solid var(--line); + border-radius: 1.8rem; + background: var(--panel); + box-shadow: var(--shadow); +} + +.live-kicker { + color: var(--accent); + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.055em; + text-transform: uppercase; +} +.live-kicker strong { + color: var(--text-muted); +} +.live-current h2 { + max-width: 20ch; + margin: 1.4rem 0 2rem; + font-size: clamp(2rem, 4vw, 3.4rem); + font-weight: 670; + letter-spacing: -0.05em; + line-height: 1.03; +} +.prompter-copy { + max-width: 34ch; + font-family: ui-serif, Georgia, serif; + font-size: var(--prompt-size); + line-height: 1.47; +} + +.moderation-card, +.transition-card { + margin-top: 2rem; + padding: 1rem 1.1rem; + border-radius: 0.9rem; + background: var(--panel-soft); +} + +.moderation-card span, +.transition-card span { + color: var(--accent); + font-size: 0.67rem; + font-weight: 750; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.moderation-card p, +.transition-card p { + margin: 0.4rem 0 0; +} + +.research-details { + margin-top: 0.8rem; + padding: 0.82rem 1rem; + border: 1px solid var(--line); + border-radius: 0.85rem; + color: var(--text-muted); +} +.research-details summary { + cursor: pointer; + color: var(--text); + font-size: 0.78rem; + font-weight: 680; +} +.research-details p { + margin-bottom: 0; + font-size: 0.78rem; +} +.transition-card { + border-left: 3px solid var(--gold); + background: color-mix(in srgb, var(--gold) 8%, var(--panel)); +} + +.live-rail { + display: grid; + gap: 0.75rem; +} +.next-card, +.live-progress, +.readonly-actions { + padding: 1rem; + border: 1px solid var(--line); + border-radius: 1rem; + background: var(--panel); +} +.next-card > span { + color: var(--accent); + font-size: 0.66rem; + font-weight: 750; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.next-card h3 { + margin: 0.7rem 0 0.28rem; + font-size: 1rem; + line-height: 1.25; +} +.next-card p { + margin: 0; + color: var(--text-muted); + font-size: 0.7rem; +} +.live-progress header { + display: flex; + justify-content: space-between; + color: var(--text-muted); + font-size: 0.7rem; + font-weight: 680; +} +.progress-track { + height: 0.24rem; + margin: 0.7rem 0 1rem; + overflow: hidden; + border-radius: 1rem; + background: var(--panel-soft); +} +.progress-track span { + display: block; + width: 45%; + height: 100%; + border-radius: inherit; + background: var(--accent); +} +.rail-section { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.6rem; + padding: 0.55rem; + border-radius: 0.6rem; + color: var(--text-muted); +} +.rail-section > span { + font-size: 0.63rem; + font-weight: 720; +} +.rail-section div { + display: flex; + justify-content: space-between; + gap: 0.6rem; +} +.rail-section strong { + font-size: 0.7rem; +} +.rail-section small { + font-size: 0.64rem; +} +.rail-section.active { + background: var(--accent-soft); + color: var(--accent-text); +} +.readonly-actions { + display: flex; + justify-content: space-between; + gap: 0.5rem; + color: var(--text-faint); + font-size: 0.63rem; +} +.readonly-actions strong { + color: var(--text-muted); + font-size: 0.61rem; +} + +.quiet-panel { + border: 1px solid var(--line); + border-radius: var(--radius-large); + background: var(--panel); +} + +.planned-panel { + margin-top: 2.5rem; +} +.planned-panel h2 { + margin: 0.55rem 0; + font-size: 1.55rem; + letter-spacing: -0.03em; +} +.planned-panel > p:last-child { + max-width: 40rem; + margin: 0 auto; + color: var(--text-muted); +} +.planned-icon { + display: grid; + width: 3.2rem; + height: 3.2rem; + margin: 0 auto 1rem; + place-items: center; + border-radius: 1rem; + background: var(--accent-soft); + color: var(--accent); + font-size: 1.6rem; +} + +.settings-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} +.taxonomy-panel, +.planned-settings { + padding: 1.3rem; +} +.taxonomy-panel h2, +.planned-settings h2 { + margin: 0.25rem 0 1.1rem; + font-size: 1.08rem; +} +.taxonomy-list { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} +.taxonomy-list span { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.42rem 0.68rem; + border: 1px solid var(--line); + border-radius: 2rem; + color: var(--text-muted); + font-size: 0.75rem; +} +.taxonomy-list i { + width: 0.48rem; + height: 0.48rem; + border-radius: 50%; + background: var(--tax-color); +} +.planned-settings { + background: color-mix(in srgb, var(--panel) 72%, var(--accent-soft)); +} +.planned-settings p:last-child { + color: var(--text-muted); + font-size: 0.82rem; +} + +@media (min-width: 84rem) { + .piece-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 70rem) { + .builder-workspace { + grid-template-columns: minmax(16rem, 0.72fr) minmax(24rem, 1.2fr); + } + .page-wrap { + padding-inline: 2rem; + } +} + +@media (max-width: 52rem) { + :root { + --mobile-header: 4rem; + --mobile-nav: 4.6rem; + } + body { + background: var(--bg); + } + .sidebar { + display: none; + } + .mobile-header { + position: fixed; + inset: 0 0 auto; + z-index: 30; + display: flex; + height: var(--mobile-header); + align-items: center; + justify-content: space-between; + padding: 0 0.85rem 0 0.45rem; + border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--bg-elevated) 90%, transparent); + backdrop-filter: blur(20px) saturate(1.25); + } + .mobile-header .theme-toggle { + width: auto; + } + .mobile-header .theme-toggle span:last-child { + display: none; + } + .mobile-header .theme-toggle { + padding: 0.48rem; + border: 0; + } + .mobile-navigation { + position: fixed; + inset: auto 0 0; + z-index: 30; + display: grid; + height: calc(var(--mobile-nav) + env(safe-area-inset-bottom)); + grid-template-columns: repeat(5, 1fr); + padding: 0.45rem 0.35rem env(safe-area-inset-bottom); + border-top: 1px solid var(--line); + background: color-mix(in srgb, var(--bg-elevated) 92%, transparent); + backdrop-filter: blur(22px) saturate(1.3); + } + .mobile-navigation a { + display: flex; + min-width: 0; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.18rem; + border-radius: 0.65rem; + color: var(--text-faint); + font-size: clamp(0.54rem, 2.2vw, 0.65rem); + font-weight: 620; + } + .mobile-navigation a.active { + color: var(--accent); + } + .mobile-navigation a svg { + width: 1.25rem; + height: 1.25rem; + } + #main-content { + margin-left: 0; + padding: var(--mobile-header) 0 var(--mobile-nav); + } + .page-wrap { + width: 100%; + padding: 1.6rem 1rem 3rem; + } + .page-heading h1 { + font-size: clamp(2rem, 10vw, 3rem); + } + .split-heading, + .show-heading, + .live-header { + align-items: flex-start; + flex-direction: column; + gap: 1.15rem; + } + .heading-stat { + display: none; + } + .library-toolbar { + align-items: stretch; + flex-direction: column; + } + .search-field { + width: 100%; + } + .filter-tabs { + width: 100%; + overflow-x: auto; + } + .filter-tabs button { + flex: 1 0 auto; + } + .piece-grid { + grid-template-columns: 1fr; + } + .piece-card { + min-height: 15.5rem; + } + .show-summary { + width: 100%; + flex-wrap: wrap; + } + .show-summary > div { + flex: 1; + } + .live-link { + flex: 1 0 100%; + justify-content: center; + } + .mobile-pane-switch { + display: flex; + width: 100%; + margin-bottom: 0.8rem; + } + .mobile-pane-switch button { + flex: 1; + } + .builder-workspace { + display: block; + } + .builder-pane.mobile-hidden { + display: none; + } + .builder-pane { + padding: 0.85rem; + } + .usage-list, + .section-placeholder { + margin-left: 0; + } + .usage-row { + grid-template-columns: minmax(0, 1fr) auto; + } + .drag-hint { + display: none; + } + .live-header-actions { + width: 100%; + justify-content: space-between; + } + .live-grid { + grid-template-columns: 1fr; + } + .live-current { + padding: 1.3rem; + border-radius: 1.3rem; + } + .live-current h2 { + margin: 1.1rem 0 1.5rem; + } + .live-rail { + grid-template-columns: 1fr; + } + .settings-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 24rem) { + .page-wrap { + padding-inline: 0.75rem; + } + .preview-notice { + padding: 0.75rem; + } + .piece-card footer { + align-items: flex-start; + flex-direction: column; + gap: 0.25rem; + } + .run-section > header { + grid-template-columns: auto minmax(0, 1fr); + } + .run-section > header > strong { + grid-column: 2; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..6285566 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,5 @@ +declare global { + namespace App {} +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..4320f5e --- /dev/null +++ b/src/app.html @@ -0,0 +1,30 @@ + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/lib/components/NavIcon.svelte b/src/lib/components/NavIcon.svelte new file mode 100644 index 0000000..7d9d4fa --- /dev/null +++ b/src/lib/components/NavIcon.svelte @@ -0,0 +1,32 @@ + + + + + diff --git a/src/lib/components/PreviewNotice.svelte b/src/lib/components/PreviewNotice.svelte new file mode 100644 index 0000000..e0720b3 --- /dev/null +++ b/src/lib/components/PreviewNotice.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/lib/components/ThemeToggle.svelte b/src/lib/components/ThemeToggle.svelte new file mode 100644 index 0000000..33a2da1 --- /dev/null +++ b/src/lib/components/ThemeToggle.svelte @@ -0,0 +1,49 @@ + + + diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..d6c9c4f --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,12 @@ +export function formatDuration(totalSeconds: number): string { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + if (hours > 0) return `${hours} Std. ${minutes.toString().padStart(2, '0')} Min.`; + return `${minutes} Min.`; +} + +export function formatClock(totalSeconds: number): string { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + return hours > 0 ? `${hours}:${minutes.toString().padStart(2, '0')} h` : `${minutes} min`; +} diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/src/lib/server/config.ts b/src/lib/server/config.ts new file mode 100644 index 0000000..d348095 --- /dev/null +++ b/src/lib/server/config.ts @@ -0,0 +1,12 @@ +import path from 'node:path'; + +function configuredPath(value: string | undefined, fallback: string): string { + return path.resolve(process.cwd(), value || fallback); +} + +export function getServerPaths() { + return { + database: configuredPath(process.env.SENDEWERK_DB_PATH, '.data/sendewerk.sqlite'), + uploads: configuredPath(process.env.SENDEWERK_UPLOAD_DIR, '.data/uploads') + }; +} diff --git a/src/lib/server/database.spec.ts b/src/lib/server/database.spec.ts new file mode 100644 index 0000000..2c8d792 --- /dev/null +++ b/src/lib/server/database.spec.ts @@ -0,0 +1,77 @@ +import Database from 'better-sqlite3'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const temporaryDirectories: string[] = []; + +afterEach(async () => { + const { closeDatabase } = await import('./database.js'); + closeDatabase(); + delete process.env.SENDEWERK_DB_PATH; + delete process.env.SENDEWERK_UPLOAD_DIR; + for (const directory of temporaryDirectories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } + vi.resetModules(); +}); + +describe('database setup', () => { + it('runs migrations and the seed repeatedly in an isolated database', () => { + const directory = mkdtempSync(path.join(tmpdir(), 'sendewerk-test-')); + temporaryDirectories.push(directory); + const databasePath = path.join(directory, 'test.sqlite'); + const environment = { + ...process.env, + SENDEWERK_DB_PATH: databasePath, + SENDEWERK_UPLOAD_DIR: path.join(directory, 'uploads') + }; + + for (let run = 0; run < 2; run += 1) { + execFileSync(process.execPath, ['scripts/migrate.mjs'], { + cwd: process.cwd(), + env: environment + }); + execFileSync(process.execPath, ['scripts/seed.mjs'], { + cwd: process.cwd(), + env: environment + }); + } + + const database = new Database(databasePath, { readonly: true }); + const counts = database + .prepare( + `SELECT + (SELECT count(*) FROM schema_migrations) AS migrations, + (SELECT count(*) FROM pieces) AS pieces, + (SELECT count(*) FROM shows) AS shows, + (SELECT count(*) FROM piece_usages) AS usages` + ) + .get() as { migrations: number; pieces: number; shows: number; usages: number }; + database.close(); + + expect(counts).toEqual({ migrations: 1, pieces: 4, shows: 1, usages: 3 }); + expect(databasePath).not.toBe(path.resolve('.data/sendewerk.sqlite')); + }); + + it('does not count reserve pieces or explicit sections twice', async () => { + const directory = mkdtempSync(path.join(tmpdir(), 'sendewerk-duration-test-')); + temporaryDirectories.push(directory); + process.env.SENDEWERK_DB_PATH = path.join(directory, 'test.sqlite'); + process.env.SENDEWERK_UPLOAD_DIR = path.join(directory, 'uploads'); + + execFileSync(process.execPath, ['scripts/migrate.mjs'], { + cwd: process.cwd(), + env: process.env + }); + execFileSync(process.execPath, ['scripts/seed.mjs'], { cwd: process.cwd(), env: process.env }); + const { getShow } = await import('./previews.js'); + const show = getShow(); + + expect(show.sections.map((section) => section.durationSeconds)).toEqual([300, 1200, 3600]); + expect(show.totalDurationSeconds).toBe(5100); + expect(show.sections[1].usages.find((usage) => usage.isReserve)?.durationSeconds).toBe(540); + }); +}); diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts new file mode 100644 index 0000000..7b3d02d --- /dev/null +++ b/src/lib/server/database.ts @@ -0,0 +1,21 @@ +import Database from 'better-sqlite3'; +import { getServerPaths } from './config.js'; + +let database: Database.Database | undefined; + +export function getDatabase(): Database.Database { + if (!database) { + database = new Database(getServerPaths().database, { fileMustExist: true }); + database.pragma('foreign_keys = ON'); + } + return database; +} + +export function closeDatabase(): void { + database?.close(); + database = undefined; +} + +process.on('sveltekit:shutdown', () => { + closeDatabase(); +}); diff --git a/src/lib/server/previews.ts b/src/lib/server/previews.ts new file mode 100644 index 0000000..f8d97e4 --- /dev/null +++ b/src/lib/server/previews.ts @@ -0,0 +1,228 @@ +import { getDatabase } from './database.js'; + +export type PreviewPiece = { + id: string; + title: string; + summary: string; + status: 'inbox' | 'preparing' | 'ready' | 'archived'; + durationSeconds: number; + format: string; + category: string; + categoryColor: string; + topics: string[]; + sourceCount: number; +}; + +export type PreviewUsage = { + id: string; + title: string; + durationSeconds: number; + status: 'open' | 'current' | 'done' | 'skipped'; + intro: string; + moderation: string; + transition: string; + isReserve: boolean; +}; + +export type PreviewSection = { + id: string; + name: string; + category: string; + categoryColor: string; + durationSeconds: number; + usages: PreviewUsage[]; +}; + +export type PreviewShow = { + id: string; + title: string; + startsAtUtc: string; + startsAtBerlin: string; + twitchTitle: string; + announcement: string; + status: string; + sections: PreviewSection[]; + totalDurationSeconds: number; +}; + +type PieceRow = { + id: string; + title: string; + summary: string; + status: PreviewPiece['status']; + duration_seconds: number; + format: string; + category: string | null; + category_color: string | null; + topics: string | null; + source_count: number; +}; + +export function getPieces(): PreviewPiece[] { + const rows = getDatabase() + .prepare( + `SELECT + p.id, + p.title, + p.summary, + p.status, + p.duration_seconds, + COALESCE(f.name, 'Ohne Format') AS format, + ( + SELECT c.name FROM piece_category_suggestions pcs + JOIN section_categories c ON c.id = pcs.category_id + WHERE pcs.piece_id = p.id ORDER BY c.sort_order LIMIT 1 + ) AS category, + ( + SELECT c.color FROM piece_category_suggestions pcs + JOIN section_categories c ON c.id = pcs.category_id + WHERE pcs.piece_id = p.id ORDER BY c.sort_order LIMIT 1 + ) AS category_color, + ( + SELECT group_concat(t.name, '||') FROM piece_topics pt + JOIN topics t ON t.id = pt.topic_id WHERE pt.piece_id = p.id + ) AS topics, + (SELECT count(*) FROM piece_sources ps WHERE ps.piece_id = p.id) AS source_count + FROM pieces p + LEFT JOIN formats f ON f.id = p.format_id + WHERE p.status != 'archived' + ORDER BY CASE p.status WHEN 'ready' THEN 0 ELSE 1 END, p.title` + ) + .all() as PieceRow[]; + + return rows.map((row) => ({ + id: row.id, + title: row.title, + summary: row.summary, + status: row.status, + durationSeconds: row.duration_seconds, + format: row.format, + category: row.category ?? 'Offen', + categoryColor: row.category_color ?? '#8b8f98', + topics: row.topics?.split('||') ?? [], + sourceCount: row.source_count + })); +} + +type ShowRow = { + id: string; + title: string; + starts_at_utc: string; + twitch_title: string; + announcement_text: string; + status: string; +}; + +type SectionRow = { + id: string; + name: string; + category: string | null; + category_color: string | null; + explicit_duration_seconds: number | null; +}; + +type UsageRow = { + id: string; + show_section_id: string; + title_snapshot: string; + duration_seconds: number; + status: PreviewUsage['status']; + intro_snapshot: string; + moderation_snapshot: string; + transition_snapshot: string; + is_reserve: number; +}; + +export function getShow(): PreviewShow { + const database = getDatabase(); + const show = database + .prepare( + `SELECT id, title, starts_at_utc, twitch_title, announcement_text, status + FROM shows ORDER BY starts_at_utc DESC LIMIT 1` + ) + .get() as ShowRow | undefined; + + if (!show) throw new Error('No preview show found. Run npm run db:seed.'); + + const sectionRows = database + .prepare( + `SELECT s.id, s.name, c.name AS category, c.color AS category_color, + s.explicit_duration_seconds + FROM show_sections s + LEFT JOIN section_categories c ON c.id = s.category_id + WHERE s.show_id = ? ORDER BY s.position` + ) + .all(show.id) as SectionRow[]; + const usageRows = database + .prepare( + `SELECT id, show_section_id, title_snapshot, duration_seconds, status, + intro_snapshot, moderation_snapshot, transition_snapshot, is_reserve + FROM piece_usages WHERE show_section_id IN ( + SELECT id FROM show_sections WHERE show_id = ? + ) ORDER BY position` + ) + .all(show.id) as UsageRow[]; + + const sections = sectionRows.map((section) => { + const usages = usageRows + .filter((usage) => usage.show_section_id === section.id) + .map((usage) => ({ + id: usage.id, + title: usage.title_snapshot, + durationSeconds: usage.duration_seconds, + status: usage.status, + intro: usage.intro_snapshot, + moderation: usage.moderation_snapshot, + transition: usage.transition_snapshot, + isReserve: usage.is_reserve === 1 + })); + const plannedUsages = usages.filter((usage) => !usage.isReserve); + return { + id: section.id, + name: section.name, + category: section.category ?? 'Offen', + categoryColor: section.category_color ?? '#8b8f98', + durationSeconds: + section.explicit_duration_seconds ?? + plannedUsages.reduce((total, usage) => total + usage.durationSeconds, 0), + usages + }; + }); + + return { + id: show.id, + title: show.title, + startsAtUtc: show.starts_at_utc, + startsAtBerlin: new Intl.DateTimeFormat('de-DE', { + timeZone: 'Europe/Berlin', + weekday: 'short', + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }).format(new Date(show.starts_at_utc)), + twitchTitle: show.twitch_title, + announcement: show.announcement_text, + status: show.status, + sections, + totalDurationSeconds: sections.reduce((total, section) => total + section.durationSeconds, 0) + }; +} + +export function getTaxonomy() { + const database = getDatabase(); + return { + categories: database + .prepare('SELECT id, name, color FROM section_categories ORDER BY sort_order') + .all() as { id: string; name: string; color: string }[], + formats: database.prepare('SELECT id, name FROM formats ORDER BY name').all() as { + id: string; + name: string; + }[], + topics: database.prepare('SELECT id, name FROM topics ORDER BY name').all() as { + id: string; + name: string; + }[] + }; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..35444a3 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,82 @@ + + + + + + +
+ + +
+ + + Sendewerk + + +
+ +
{@render children()}
+ + +
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..e68757a --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit'; + +export function load() { + redirect(307, '/fundus'); +} diff --git a/src/routes/archiv/+page.svelte b/src/routes/archiv/+page.svelte new file mode 100644 index 0000000..ed598b7 --- /dev/null +++ b/src/routes/archiv/+page.svelte @@ -0,0 +1,20 @@ +Archiv | Sendewerk + +
+
+

Bewahrte Sendungen

+

Archiv

+

+ Abgeschlossene Abläufe bleiben hier in ihrem damaligen Stand nachvollziehbar. +

+
+
+ +

Geplant für Etappe 6

+

Historie braucht klare Regeln.

+

+ Stabile Snapshots, Verwendungshistorie und bewusste Archivierung werden gemeinsam + implementiert, statt jetzt nur eine leere Liste vorzutäuschen. +

+
+
diff --git a/src/routes/eingang/+page.svelte b/src/routes/eingang/+page.svelte new file mode 100644 index 0000000..77da0a0 --- /dev/null +++ b/src/routes/eingang/+page.svelte @@ -0,0 +1,21 @@ +Eingang | Sendewerk + +
+
+

Sammelstelle

+

Eingang

+

+ Neue Gedanken und spätere RSS-Fundstücke landen hier, bevor sie in den Fundus übernommen + werden. +

+
+
+ +

Geplant für Etappe 3 und 7

+

Noch ist der Tresen leer.

+

+ Schnellaufnahme, Feed-Vorschau und bewusste Übernahme werden erst mit den dazugehörigen + Speicher- und Sicherheitsregeln umgesetzt. +

+
+
diff --git a/src/routes/fundus/+page.server.ts b/src/routes/fundus/+page.server.ts new file mode 100644 index 0000000..64ecc3e --- /dev/null +++ b/src/routes/fundus/+page.server.ts @@ -0,0 +1,5 @@ +import { getPieces } from '#lib/server/previews.js'; + +export function load() { + return { pieces: getPieces() }; +} diff --git a/src/routes/fundus/+page.svelte b/src/routes/fundus/+page.svelte new file mode 100644 index 0000000..a66cea6 --- /dev/null +++ b/src/routes/fundus/+page.svelte @@ -0,0 +1,101 @@ + + +Fundus | Sendewerk + +
+
+
+

Redaktioneller Fundus

+

Gute Stücke, sofort griffbereit.

+

+ Vorbereitete Ideen, Gespräche und Fundstücke für die nächste Sendung. +

+
+
+ {data.pieces.length}Inhalte +
+
+ + + +
+ +
+ + + +
+
+ + {#if filteredPieces.length > 0} +
+ {#each filteredPieces as piece (piece.id)} +
+
+ {piece.category} + {statusLabel[piece.status]} +
+

{piece.title}

+

{piece.summary}

+
+ {piece.format} + {#each piece.topics as topic (topic)}{topic}{/each} +
+
+ {formatClock(piece.durationSeconds)} geplant + {piece.sourceCount} {piece.sourceCount === 1 ? 'Quelle' : 'Quellen'} +
+
+ {/each} +
+ {:else} +
+ +

Kein passendes Stück

+

Suche oder Filter zurücksetzen, um wieder alle Beispieldaten zu sehen.

+ +
+ {/if} +
diff --git a/src/routes/healthz/+server.ts b/src/routes/healthz/+server.ts new file mode 100644 index 0000000..838d30a --- /dev/null +++ b/src/routes/healthz/+server.ts @@ -0,0 +1,12 @@ +import { getDatabase } from '#lib/server/database.js'; +import { json } from '@sveltejs/kit'; + +export function GET() { + try { + getDatabase().prepare('SELECT 1').get(); + return json({ status: 'ok' }); + } catch (error) { + console.error('Healthcheck failed', error); + return json({ status: 'error' }, { status: 503 }); + } +} diff --git a/src/routes/live/+page.server.ts b/src/routes/live/+page.server.ts new file mode 100644 index 0000000..e3ebca8 --- /dev/null +++ b/src/routes/live/+page.server.ts @@ -0,0 +1,15 @@ +import { getShow } from '#lib/server/previews.js'; + +export function load() { + const show = getShow(); + const usages = show.sections.flatMap((section) => + section.usages + .filter((usage) => !usage.isReserve) + .map((usage) => ({ ...usage, section: section.name })) + ); + const currentIndex = Math.max( + 0, + usages.findIndex((usage) => usage.status === 'current') + ); + return { show, current: usages[currentIndex], next: usages[currentIndex + 1] ?? null }; +} diff --git a/src/routes/live/+page.svelte b/src/routes/live/+page.svelte new file mode 100644 index 0000000..9a7c593 --- /dev/null +++ b/src/routes/live/+page.svelte @@ -0,0 +1,92 @@ + + +Live-Vorschau | Sendewerk + +
+
+
+

Live-Modus · Vorschau

+

{data.show.title}

+
+
+
+ + +
+ Planung schließen +
+
+ + + +
+
+
+ Aktuell · {data.current.section}{formatClock(data.current.durationSeconds)} +
+

{data.current.title}

+
{data.current.intro}
+
+ Moderationsstichpunkte +

{data.current.moderation}

+
+
+ Recherche öffnen +

+ Die Recherche bleibt im echten Live-Modus standardmäßig geschlossen und bewahrt beim + Öffnen die Leseposition. +

+
+
+ Überleitung +

{data.current.transition}

+
+
+ + +
+
diff --git a/src/routes/sendungen/+page.server.ts b/src/routes/sendungen/+page.server.ts new file mode 100644 index 0000000..e689e9e --- /dev/null +++ b/src/routes/sendungen/+page.server.ts @@ -0,0 +1,5 @@ +import { getPieces, getShow } from '#lib/server/previews.js'; + +export function load() { + return { pieces: getPieces(), show: getShow() }; +} diff --git a/src/routes/sendungen/+page.svelte b/src/routes/sendungen/+page.svelte new file mode 100644 index 0000000..33cc5d3 --- /dev/null +++ b/src/routes/sendungen/+page.svelte @@ -0,0 +1,141 @@ + + +Sendungen | Sendewerk + +
+
+
+

Nächste Sendung · {data.show.startsAtBerlin} Uhr

+

{data.show.title}

+
+
+
{plannedCount}Stücke
+
+ {formatDuration(data.show.totalDurationSeconds)}Planzeit +
+ Live-Vorschau ↗ +
+
+ + + +
+ + +
+ +
+
+
+
+

Quelle

+

Sendefertiger Fundus

+
+ {data.pieces.filter((piece) => piece.status === 'ready').length} +
+ +
+ {#each data.pieces.filter((piece) => piece.status === 'ready') as piece (piece.id)} +
+
+ {piece.category}{formatClock(piece.durationSeconds)} +
+

{piece.title}

+

{piece.format} · {piece.topics.join(', ')}

+
+ {/each} +
+
+
+ +
Reservebank1 Stück vorbereitet
+
+

„Pixelart, die nicht nostalgisch sein will“ wartet als Ersatz.

+
+
+ +
+
+
+

Sendungsplan

+

Ablauf

+
+ {formatDuration(data.show.totalDurationSeconds)} +
+ +
+ {#each data.show.sections as section, index (section.id)} +
+
+
{String(index + 1).padStart(2, '0')}
+
+ {section.category} +

{section.name}

+
+ {formatClock(section.durationSeconds)} +
+ {#if section.usages.filter((usage) => !usage.isReserve).length > 0} +
+ {#each section.usages.filter((usage) => !usage.isReserve) as usage (usage.id)} +
+ +
+ {usage.title}{formatClock(usage.durationSeconds)} · Eigene Sendungskopie +
+ {usage.status === 'current' ? 'Aktuell' : 'Offen'} +
+ {/each} +
+ {:else} +
+ Eigenständiger Spieleblock · keine Piece-Kopie nötig +
+ {/if} +
+ {/each} +
+
+
+
diff --git a/src/routes/verwaltung/+page.server.ts b/src/routes/verwaltung/+page.server.ts new file mode 100644 index 0000000..2ef8585 --- /dev/null +++ b/src/routes/verwaltung/+page.server.ts @@ -0,0 +1,5 @@ +import { getTaxonomy } from '#lib/server/previews.js'; + +export function load() { + return { taxonomy: getTaxonomy() }; +} diff --git a/src/routes/verwaltung/+page.svelte b/src/routes/verwaltung/+page.svelte new file mode 100644 index 0000000..8f0195c --- /dev/null +++ b/src/routes/verwaltung/+page.svelte @@ -0,0 +1,50 @@ + + +Verwaltung | Sendewerk + +
+
+

System & Struktur

+

Verwaltung

+

+ Die bereits angelegte Taxonomie ist sichtbar; Bearbeitung und Benutzerverwaltung folgen mit + serverseitigen Rechten. +

+
+
+
+

Abschnittskategorien

+

Struktur im Ablauf

+
+ {#each data.taxonomy.categories as category (category.id)}{category.name}{/each} +
+
+
+

Formate

+

Form des Stücks

+
+ {#each data.taxonomy.formats as format (format.id)}{format.name}{/each} +
+
+
+

Themen

+

Inhaltliche Zuordnung

+
+ {#each data.taxonomy.topics as topic (topic.id)}{topic.name}{/each} +
+
+
+

Etappe 2

+

Konten & Rollen

+

+ Barkeeper, Thekencrew und technische Zugänge werden erst mit vollständiger Authentifizierung + verwaltbar. +

+
+
+
diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..90342be --- /dev/null +++ b/start.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -eu + +mkdir -p /app/data/uploads +chown -R cloudron:cloudron /app/data + +export SENDEWERK_DB_PATH="${SENDEWERK_DB_PATH:-/app/data/sendewerk.sqlite}" +export SENDEWERK_UPLOAD_DIR="${SENDEWERK_UPLOAD_DIR:-/app/data/uploads}" +export HOST="${HOST:-0.0.0.0}" +export PORT="${PORT:-3000}" + +gosu cloudron:cloudron node /app/code/scripts/migrate.mjs +exec gosu cloudron:cloudron node /app/code/build diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/tests/ui.e2e.ts b/tests/ui.e2e.ts new file mode 100644 index 0000000..867ca7b --- /dev/null +++ b/tests/ui.e2e.ts @@ -0,0 +1,86 @@ +import { expect, test } from '@playwright/test'; + +test('desktop fundus is usable, filterable and keeps its theme', async ({ page }, testInfo) => { + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.goto('/fundus'); + + await expect( + page.getByRole('heading', { name: 'Gute Stücke, sofort griffbereit.' }) + ).toBeVisible(); + await expect(page.getByText('Gestaltungsvorschau: Fundus')).toBeVisible(); + await expect(page.locator('.piece-card')).toHaveCount(4); + + await page.getByRole('searchbox', { name: 'Fundus durchsuchen' }).fill('Pixelart'); + await expect(page.locator('.piece-card')).toHaveCount(1); + await expect( + page.getByRole('heading', { name: 'Pixelart, die nicht nostalgisch sein will' }) + ).toBeVisible(); + + await page.getByRole('button', { name: 'Darstellung: System' }).click(); + await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'light'); + await page.reload(); + await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'light'); + await expect(page.getByRole('button', { name: 'Darstellung: Hell' })).toBeVisible(); + + const dimensions = await page.evaluate(() => ({ + scrollWidth: document.documentElement.scrollWidth, + clientWidth: document.documentElement.clientWidth + })); + expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); + await page.screenshot({ path: testInfo.outputPath('fundus-desktop.png'), fullPage: true }); +}); + +test('desktop builder shows fundus and run side by side', async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.goto('/sendungen'); + + await expect(page.getByText('Gestaltungsvorschau: Sendungsbau')).toBeVisible(); + await expect(page.locator('.source-pane')).toBeVisible(); + await expect(page.locator('.run-pane')).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Herbstsendung: Kleine Welten' })).toBeVisible(); + await expect( + page.locator('.show-summary strong').filter({ hasText: '1 Std. 25 Min.' }) + ).toBeVisible(); + + const boxes = await Promise.all([ + page.locator('.source-pane').boundingBox(), + page.locator('.run-pane').boundingBox() + ]); + expect(boxes[0]?.x).toBeLessThan(boxes[1]?.x ?? 0); +}); + +test('mobile builder switches panes without horizontal scrolling', async ({ page }, testInfo) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto('/sendungen'); + + await expect(page.locator('.run-pane')).toBeVisible(); + await expect(page.locator('.source-pane')).toBeHidden(); + await page.getByRole('button', { name: 'Fundus', exact: true }).click(); + await expect(page.locator('.source-pane')).toBeVisible(); + await expect(page.locator('.run-pane')).toBeHidden(); + + const dimensions = await page.evaluate(() => ({ + scrollWidth: document.documentElement.scrollWidth, + clientWidth: document.documentElement.clientWidth + })); + expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); + await page.screenshot({ path: testInfo.outputPath('sendungen-mobile.png'), fullPage: true }); +}); + +test('live preview exposes readable prompts and working text controls', async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 900 }); + await page.goto('/live'); + + await expect(page.getByText('Gestaltungsvorschau: Live-Modus')).toBeVisible(); + await expect(page.getByText('Eine Kurbel, ein gelber Knopf und sehr viel Charme')).toBeVisible(); + const before = await page + .locator('.live-current') + .evaluate((element) => getComputedStyle(element).getPropertyValue('--prompt-size')); + await page.getByRole('button', { name: 'Schrift vergrößern' }).click(); + const after = await page + .locator('.live-current') + .evaluate((element) => getComputedStyle(element).getPropertyValue('--prompt-size')); + expect(after).not.toBe(before); + await page.getByText('Recherche öffnen').click(); + await expect(page.locator('.research-details')).toHaveAttribute('open', ''); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..eb99c02 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "$app/tsconfig", + "compilerOptions": { + "strict": true, + "types": ["$app/types", "node"] + }, + "include": ["src", "vite.config.ts", "playwright.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..befc935 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from 'vitest/config'; +import adapter from '@sveltejs/adapter-node'; +import { sveltekit } from '@sveltejs/kit/vite'; + +export default defineConfig({ + plugins: [ + sveltekit({ + compilerOptions: { + runes: ({ filename }) => + filename.split(/[/\\]/).includes('node_modules') ? undefined : true + }, + adapter: adapter() + }) + ], + test: { + expect: { requireAssertions: true }, + projects: [ + { + extends: './vite.config.ts', + test: { + name: 'server', + environment: 'node', + include: ['src/**/*.{test,spec}.{js,ts}'], + exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'] + } + } + ] + } +});