commit 9f97d8407a17019656c52f735a396b36a422316a Author: Florian Heinz Date: Wed Oct 21 14:50:31 2026 +0200 Initial release 0.1.0 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}'] + } + } + ] + } +});