commit 1061a8b9ad289e6a7504dece09f960b27cd6e20e Author: Daniel Michelberger Date: Thu Aug 6 13:16:43 2026 +0200 chore: Repo-Struktur mit frontend/ und backend/ aufsetzen Die SvelteKit-App liegt unter frontend/, das Backend unter backend/ als eigenständige PocketBase-Instanz mit Dockerfile, docker-compose und versioniertem Schema. - PocketBase-URL über PUBLIC_PB_URL konfigurierbar, Default bleibt die produktive Instanz https://api.stammtisch-hersbruck.de - Schema als Snapshot-Migration der sechs fachlichen Collections (users, teams, events, runs, riders, times), abgezogen von der produktiven Instanz. Verifiziert: ein Erststart gegen leere pb_data legt alle sechs an, Felder und API-Rules stimmen überein. - pb_data und pb_migrations als Bind-Mounts, damit Daten persistieren und im Admin-UI erzeugte Migrationen im Repo landen - Veraltete Dokumentation entfernt: pocketbase_schema.json nannte Collections (stages, results, organizers), die es nicht gibt Co-Authored-By: Claude Opus 5 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f9e841 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Dependencies +node_modules + +# Build-Output +.output +.vercel +.netlify +.wrangler +.svelte-kit +build + +# OS +.DS_Store +Thumbs.db + +# Env — enthält Tokens, gehört nie ins Repo +.env +.env.* +!.env.example + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# PocketBase-Laufzeitdaten +backend/pb_data/ +*.db +# Auto-Backups, die PocketBase beim Start anlegt +backups/ + +# IDE +.idea/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dea97d9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,136 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +stammtisch-hersbruck.de is a SvelteKit 5 application using Svelte 5's new runes syntax ($state, $props, etc.) with PocketBase as the backend. The app manages events, runs, riders, times and teams for a motorsport/cycling event organization. + +The repository is a monorepo: the SvelteKit app lives in `frontend/`, the PocketBase instance in `backend/`. + +## Development Commands + +Alle Frontend-Befehle laufen aus `frontend/`: + +```bash +cd frontend + +# Start development server (runs on http://stammtisch-hersbruck.de.localhost:31337) +npm run dev + +# Build for production +npm run build + +# Preview production build +npm run preview + +# Type-check Svelte files +npm run check + +# Type-check with watch mode +npm run check:watch + +# Generate TypeScript types from PocketBase schema +npm run generate-pocketbase-types +``` + +Backend (PocketBase) aus `backend/`: + +```bash +cd backend + +# PocketBase lokal starten (Admin-UI auf http://127.0.0.1:8090/_/) +docker compose up -d --build + +# Stoppen +docker compose down +``` + +## Architecture + +### Backend Integration (PocketBase) + +- **API Base URL**: konfigurierbar über `PUBLIC_PB_URL` in `frontend/.env`. + Default ist die produktive Instanz `https://api.stammtisch-hersbruck.de`; + für das lokale Backend aus `backend/` auf `http://127.0.0.1:8090` umstellen. +- **Schema**: versioniert als Migration in `backend/pb_migrations/`. Änderungen + im Admin-UI erzeugen dort neue Dateien, die committet werden müssen. +- **Type-safe client**: The PocketBase client is typed using auto-generated types in `frontend/src/lib/types.d.ts` +- **Collections**: users, teams, events, runs, riders, times +- **Authentication**: Handled through `src/lib/stores/pocketbase.svelte.ts` with the `AuthStore` class +- **File handling**: Use `getFileURL(record, file, options)` helper for PocketBase file URLs + +### State Management Pattern + +The app uses Svelte 5 runes for state management with a custom store pattern: + +1. **Global stores** in `src/lib/stores/`: + - `pocketbase.svelte.ts`: PocketBase client, auth, and collection operations + - `app.svelte.ts`: Global app state (theme, navigation, confirm dialogs, hotkeys) + - `teams.svelte.ts`: Example of collection-specific store pattern + +2. **Collection store pattern**: + - Each collection has a context-based store (see `teams.svelte.ts` as template) + - Use `setTeamContext()` in parent and `getTeamContext()` in children + - Stores provide: `records`, `refresh()`, `create()`, `edit()`, `remove()` + - The `collections` helper in `pocketbase.svelte.ts` provides reusable CRUD operations + +3. **Auth flow**: + - Auth state lives in `auth` store from `pocketbase.svelte.ts` + - Cookie-based persistence available via `auth.cookie` flag + - Auth store syncs with PocketBase `authStore.onChange()` + +### UI Components + +- **UI Library**: Using shadcn-svelte components (bits-ui based) +- **Component path alias**: `@/components/ui/*` maps to `$lib/components/ui/*` +- **Styling**: Tailwind CSS 4.x with custom configuration +- **Dark mode**: Handled by `mode-watcher` package, toggle with `toggleMode()` +- **Tooltips**: Global `tooltip` action available from `app.svelte.ts` using tippy.js + +### Important Patterns + +**Svelte 5 Runes**: This project uses Svelte 5 syntax exclusively: +- `$state()` for reactive state (not `let` with `$:`) +- `$props()` for component props +- `$derived()` for computed values +- `{@render children?.()}` for slot content + +**Type Generation**: After modifying PocketBase schema, run `npm run generate-pocketbase-types` to update TypeScript types. + +**Async Data Loading**: Root layout (`src/routes/+layout.svelte`) shows pattern: +```svelte +{#await load()} + +{:then _} + +{/await} +``` + +**Confirm Dialogs**: Use `app.confirm.request()` for user confirmations (see `events.svelte.ts` remove pattern). + +**Hotkeys**: Use `app.hotkey(event, condition, callback)` which auto-ignores input fields and contenteditable elements. + +### Project Configuration + +- **Repo-Struktur**: Monorepo mit `frontend/` (SvelteKit) und `backend/` + (PocketBase). Ein einziges Git-Repo im Root. +- **Dev server port**: 31337 (strict mode, custom domain: `stammtisch-hersbruck.de.localhost`) +- **Path alias**: `@/*` resolves to `./src/lib/*` (configured in frontend/svelte.config.js) +- **Adapter**: @sveltejs/adapter-auto (auto-detects deployment platform) + +### File Structure Notes + +- Routes are in `src/routes/` following SvelteKit conventions +- Reusable components in `src/lib/components/` +- Stores use `.svelte.ts` extension for Svelte 5 runes +- Static assets in `static/` +- `components.json` configures shadcn-svelte CLI + +## When Working with This Codebase + +- Always use Svelte 5 runes syntax, never legacy Svelte syntax +- Use the official Svelte MCP server to validate Svelte code +- Collection stores should follow the pattern in `teams.svelte.ts` +- PocketBase operations go through the `collections` helper for consistency +- UI components should use the shadcn-svelte imports from `@/components/ui/` \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..15baa44 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# stammtisch-hersbruck.de + +Zeitmessung und Verwaltung für Läufe des Stammtisch Hersbruck. + +Das Repository enthält beide Teile der Anwendung: + +| Verzeichnis | Inhalt | +|---|---| +| [`frontend/`](frontend/) | SvelteKit-5-Anwendung (Svelte 5 Runes, Tailwind 4) | +| [`backend/`](backend/) | PocketBase-Instanz — Dockerfile, Schema-Migrationen | + +## Schnellstart + +Frontend gegen die produktive Instanz: + + cd frontend + cp .env.example .env # Werte eintragen + npm ci + npm run dev + +Die App läuft dann auf http://stammtisch-hersbruck.de.localhost:31337 + +Backend lokal (optional — der Default zeigt auf die produktive Instanz): + + cd backend + cp .env.example .env # Werte eintragen + docker compose up -d --build + +Danach in `frontend/.env` `PUBLIC_PB_URL=http://127.0.0.1:8090` setzen. + +Details stehen in [`backend/README.md`](backend/README.md). + +## PocketBase + +Produktiv: https://api.stammtisch-hersbruck.de + +Welche Instanz das Frontend anspricht, entscheidet `PUBLIC_PB_URL` in +`frontend/.env`. Das Schema ist als Migration in `backend/pb_migrations/` +versioniert. diff --git a/UML_basic.png b/UML_basic.png new file mode 100644 index 0000000..b7f3ed4 Binary files /dev/null and b/UML_basic.png differ diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..b1a4776 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,17 @@ +# Vorlage für die lokale .env — kopieren und ausfüllen: +# cp .env.example .env +# +# Diese Datei enthält nur Platzhalter und ist versioniert. Die echte .env steht +# in .gitignore und darf nicht committet werden. + +# Admin-Zugang, wird nur beim allerersten Start angelegt. Existiert der Account +# bereits, bleibt das Passwort unverändert — spätere Änderungen gehören ins +# Admin-UI, nicht in diese Datei. Mindestens 8 Zeichen. +# Beide Variablen weglassen = PocketBase gibt beim ersten Start einen +# Installer-Link im Log aus (30 Minuten gültig). +SUPERUSER_EMAIL=admin@example.de +SUPERUSER_PASSWORD=bitte-aendern-min-8-zeichen + +# Optional: überschreibt den in docker-compose.yaml gepinnten Default. +# Nur setzen, wenn bewusst eine andere PocketBase-Version gebaut werden soll. +#PB_VERSION=0.39.6 diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..8c7b57c --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,6 @@ +pb_data/ +*.db +.DS_Store +.env +.env.* +!.env.example diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..3c011f4 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,25 @@ +FROM alpine:3.21 + +# Version bewusst gepinnt: Der GitHub-Asset heißt +# pocketbase__linux_amd64.zip, ein "latest" gibt es nicht. Ohne Pin +# zöge jeder Redeploy unangekündigt eine neue Version inklusive möglicher +# DB-Schema-Migrationen. +ARG PB_VERSION=0.39.6 + +RUN apk add --no-cache ca-certificates unzip wget + +RUN wget -q https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip -O /tmp/pb.zip \ + && unzip /tmp/pb.zip -d /pb/ \ + && rm /tmp/pb.zip + +COPY ./pb_migrations /pb/pb_migrations +COPY ./pb_hooks /pb/pb_hooks + +COPY ./entrypoint.sh /pb/entrypoint.sh +RUN chmod +x /pb/entrypoint.sh + +EXPOSE 8090 + +ENTRYPOINT ["/pb/entrypoint.sh"] + +CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb/pb_data", "--migrationsDir=/pb/pb_migrations", "--hooksDir=/pb/pb_hooks"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..b1b5c66 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,90 @@ +# stammtisch-hersbruck.de — Backend (PocketBase) + +PocketBase-Instanz für Events, Läufe, Fahrer, Zeiten und Teams. + +## Lokal starten + +`.env` aus der Vorlage anlegen (die `.env` selbst steht in `.gitignore`): + + cp .env.example .env + +und die Werte eintragen — `.env.example` beschreibt jede Variable. Dann: + + docker compose up -d --build + +Admin-UI: http://127.0.0.1:8090/_/ — Login mit `SUPERUSER_EMAIL`/`SUPERUSER_PASSWORD`. + +Damit das Frontend gegen diese Instanz läuft, in `../frontend/.env` +`PUBLIC_PB_URL=http://127.0.0.1:8090` setzen. + +## Schema + +Das Schema liegt als versionierte Migration in `pb_migrations/` und wird beim +Start automatisch angewendet. Es enthält die sechs fachlichen Collections +`users`, `teams`, `events`, `runs`, `riders`, `times`. + +`1754400000_init_schema.js` ist ein Snapshot der produktiven Instanz. Er wurde +per `GET /api/collections` abgezogen und ruft `importCollections` mit +`deleteMissing = false` auf — die Migration legt an und aktualisiert, löscht +aber nichts, was nicht im Snapshot steht. + +Schema-Änderungen im Admin-UI erzeugen in `pb_migrations/` neue Dateien; diese +müssen committet werden. + +Ein frisch gestarteter Container hat damit dasselbe Schema wie die produktive +Instanz — aber **keine** Daten. + +## Datenpersistenz + +Sämtliche Laufzeitdaten liegen in der SQLite-DB unter `/pb/pb_data`: Datensätze, +Uploads, die SMTP-Einstellungen und der Superuser. Ohne gemountetes Volume auf +diesem Pfad sind sie nach jedem Redeploy verloren. + +Das Volume-Mapping `./pb_data:/pb/pb_data` steht in der `docker-compose.yaml`, +damit die Persistenz-Konfiguration versioniert im Repo liegt. + +**Trügerisches Signal:** Die Collections kommen aus der Migration und sind nach +einem Redeploy auch dann da, wenn gar kein Volume existiert. Sie taugen **nicht** +als Nachweis funktionierender Persistenz — das zeigt nur ein Datensatz, der in +keiner Migration steht. + +Mount prüfen: + + docker inspect --format '{{json .Mounts}}' | jq + +Erscheint dort kein Eintrag mit `"Destination": "/pb/pb_data"`, fehlt die Persistenz. + +`pb_data/` steht in `.gitignore` und gehört dort auch hin. + +## Superuser-Verhalten + +`entrypoint.sh` ruft `superuser create` auf — bewusst **nicht** `upsert`. +Existiert der Account bereits, scheitert `create` mit „Value must be unique" und +das Passwort bleibt unangetastet. Eine im Admin-UI vorgenommene +Passwortänderung überlebt damit jeden Redeploy. + +| Situation | Verhalten | +|---|---| +| Erster Start, Variablen gesetzt | Superuser wird angelegt | +| Neustart, Account existiert | „Superuser existiert bereits — Passwort bleibt unverändert" | +| Passwort im UI geändert, dann Neustart | UI-Passwort gilt weiter, Env wird ignoriert | +| Variablen nicht gesetzt | Übersprungen, PocketBase gibt Installer-Link im Log aus | +| Passwort zu kurz (< 8 Zeichen) | Container bricht mit Exitcode 1 ab | + +Passwort später ändern: **im Admin-UI**, nicht über die Env — eine Änderung der +Env-Variable hat keine Wirkung mehr, sobald der Account existiert. + +## PocketBase aktualisieren + +Die Version ist über das Build-Arg `PB_VERSION` gepinnt (Dockerfile und +`docker-compose.yaml`). Wert hochsetzen, committen, neu bauen. + +Ein `latest` gibt es bewusst nicht: Der GitHub-Asset heißt +`pocketbase__linux_amd64.zip`, enthält die Version also im Dateinamen. +Der Pin ist auch gewollt — sonst zieht jeder Rebuild unangekündigt eine neue +Version, inklusive möglicher DB-Schema-Migrationen. + +Vor einem Update: Changelog prüfen +(https://github.com/pocketbase/pocketbase/releases) und `pb_data` sichern — +PocketBase migriert die DB beim Start automatisch, ein Downgrade ist danach +nicht mehr ohne Weiteres möglich. diff --git a/backend/docker-compose.yaml b/backend/docker-compose.yaml new file mode 100644 index 0000000..0fab6e0 --- /dev/null +++ b/backend/docker-compose.yaml @@ -0,0 +1,32 @@ +services: + pocketbase: + build: + context: . + args: + # Version-Pin. Update: hier bzw. im Dockerfile hochsetzen und committen. + PB_VERSION: ${PB_VERSION:-0.39.6} + restart: unless-stopped + ports: + - "8090:8090" + volumes: + # Persistenz der SQLite-DB inklusive aller Laufzeitdaten, Uploads, + # Mail-Settings und des Superusers. Ohne dieses Mapping ist nach jedem + # Redeploy alles weg — das Schema käme zwar aus pb_migrations zurück, + # die Daten aber nicht. + - ./pb_data:/pb/pb_data + # Migrationen zurück auf den Host. Ohne dieses Mapping schreibt PocketBase + # die bei Schema-Änderungen im Admin-UI erzeugten Dateien nur ins + # Container-Dateisystem — sie wären beim nächsten Build verloren und + # könnten nie committet werden. Das COPY im Dockerfile bleibt trotzdem + # bestehen, damit das Image auch ohne Bind-Mount das Schema mitbringt. + - ./pb_migrations:/pb/pb_migrations + environment: + # Superuser wird nur beim allerersten Start angelegt (siehe entrypoint.sh) + SUPERUSER_EMAIL: ${SUPERUSER_EMAIL:-} + SUPERUSER_PASSWORD: ${SUPERUSER_PASSWORD:-} + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8090/api/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100644 index 0000000..f78a3af --- /dev/null +++ b/backend/entrypoint.sh @@ -0,0 +1,38 @@ +#!/bin/sh +set -e + +# Superuser nur beim allerersten Start anlegen. Existiert der Account bereits, +# scheitert "create" von selbst mit "email: Value must be unique." und das +# Passwort bleibt unangetastet. Damit überschreibt ein Redeploy keine +# Passwortänderung, die im Admin-UI gemacht wurde. +# +# Bewusst NICHT "superuser upsert": das würde bei jedem Start das Passwort aus +# der Env zurückschreiben und UI-seitige Änderungen still aushebeln. +# +# Ohne die beiden Variablen wird der Schritt übersprungen; PocketBase zeigt dann +# beim ersten Start einen Installer-Link im Log. +if [ -n "$SUPERUSER_EMAIL" ] && [ -n "$SUPERUSER_PASSWORD" ]; then + # PocketBase liefert auch im Fehlerfall Exitcode 0 und meldet den Fehler nur + # auf stdout — deshalb wird hier die Ausgabe ausgewertet, nicht der Status. + out=$(/pb/pocketbase superuser create "$SUPERUSER_EMAIL" "$SUPERUSER_PASSWORD" \ + --dir=/pb/pb_data 2>&1) || true + + case "$out" in + *"must be unique"*) + echo "Superuser existiert bereits — Passwort bleibt unverändert" + ;; + *"Successfully created"*) + echo "$out" + ;; + *) + # Unerwarteter Fehler (z. B. zu kurzes Passwort): sichtbar machen und + # abbrechen, statt mit unklarem Zustand weiterzulaufen. + echo "Superuser konnte nicht angelegt werden: $out" >&2 + exit 1 + ;; + esac +else + echo "SUPERUSER_EMAIL/SUPERUSER_PASSWORD nicht gesetzt — Superuser wird nicht angelegt" +fi + +exec "$@" diff --git a/backend/pb_hooks/.gitkeep b/backend/pb_hooks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/pb_migrations/1754400000_init_schema.js b/backend/pb_migrations/1754400000_init_schema.js new file mode 100644 index 0000000..84e2561 --- /dev/null +++ b/backend/pb_migrations/1754400000_init_schema.js @@ -0,0 +1,919 @@ +/// + +// Schema-Snapshot der sechs fachlichen Collections, abgezogen von der +// produktiven Instanz. System-Collections (_superusers, _mfas, ...) legt +// PocketBase selbst an und stehen deshalb nicht hier drin. +// +// importCollections wird mit deleteMissing = false aufgerufen: Die Migration +// legt an und aktualisiert, löscht aber nichts, was nicht im Snapshot steht. + +migrate((app) => { + const collections = [ + { + "id": "_pb_users_auth_", + "listRule": "id = @request.auth.id", + "viewRule": "id = @request.auth.id", + "createRule": null, + "updateRule": "id = @request.auth.id", + "deleteRule": "id = @request.auth.id", + "name": "users", + "type": "auth", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "cost": 10, + "hidden": true, + "id": "password901924565", + "max": 0, + "min": 8, + "name": "password", + "pattern": "", + "presentable": false, + "required": true, + "system": true, + "type": "password" + }, + { + "autogeneratePattern": "[a-zA-Z0-9_]{50}", + "hidden": true, + "id": "text2504183744", + "max": 60, + "min": 30, + "name": "tokenKey", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": true, + "system": true, + "type": "text" + }, + { + "exceptDomains": null, + "hidden": false, + "id": "email3885137012", + "name": "email", + "onlyDomains": null, + "presentable": false, + "required": false, + "system": true, + "type": "email" + }, + { + "hidden": false, + "id": "bool1547992806", + "name": "emailVisibility", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "hidden": false, + "id": "bool256245529", + "name": "verified", + "presentable": false, + "required": false, + "system": true, + "type": "bool" + }, + { + "autogeneratePattern": "users[0-9]{6}", + "hidden": false, + "id": "text4166911607", + "max": 150, + "min": 3, + "name": "username", + "pattern": "^[\\w][\\w\\.\\-]*$", + "presentable": false, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "users_name", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "users_avatar", + "maxSelect": 1, + "maxSize": 5242880, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": null, + "type": "file" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [ + "CREATE UNIQUE INDEX `__pb_users_auth__username_idx` ON `users` (username COLLATE NOCASE)", + "CREATE UNIQUE INDEX `__pb_users_auth__email_idx` ON `users` (`email`) WHERE `email` != ''", + "CREATE UNIQUE INDEX `__pb_users_auth__tokenKey_idx` ON `users` (`tokenKey`)" + ], + "created": "2023-10-28 21:35:49.020Z", + "updated": "2024-07-19 09:58:56.589Z", + "system": false, + "authRule": "", + "manageRule": null, + "authAlert": { + "enabled": true, + "emailTemplate": { + "subject": "Login from a new location", + "body": "

Hello,

\n

We noticed a login to your {APP_NAME} account from a new location.

\n

If this was you, you may disregard this email.

\n

If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "oauth2": { + "providers": [], + "mappedFields": { + "id": "", + "name": "", + "username": "username", + "avatarURL": "" + }, + "enabled": false + }, + "passwordAuth": { + "enabled": true, + "identityFields": [ + "email", + "username" + ] + }, + "mfa": { + "enabled": false, + "duration": 1800, + "rule": "" + }, + "otp": { + "enabled": false, + "duration": 180, + "length": 8, + "emailTemplate": { + "subject": "OTP for {APP_NAME}", + "body": "

Hello,

\n

Your one-time password is: {OTP}

\n

If you didn't ask for the one-time password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + "authToken": { + "duration": 1209600 + }, + "passwordResetToken": { + "duration": 1800 + }, + "emailChangeToken": { + "duration": 1800 + }, + "verificationToken": { + "duration": 604800 + }, + "fileToken": { + "duration": 120 + }, + "verificationTemplate": { + "subject": "Verify your {APP_NAME} email", + "body": "

Hello,

\n

Thank you for joining us at {APP_NAME}.

\n

Click on the button below to verify your email address.

\n

\n Verify\n

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "resetPasswordTemplate": { + "subject": "Reset your {APP_NAME} password", + "body": "

Hello,

\n

Click on the button below to reset your password.

\n

\n Reset password\n

\n

If you didn't ask to reset your password, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + }, + "confirmEmailChangeTemplate": { + "subject": "Confirm your {APP_NAME} new email address", + "body": "

Hello,

\n

Click on the button below to confirm your new email address.

\n

\n Confirm new email\n

\n

If you didn't ask to change your email address, you can ignore this email.

\n

\n Thanks,
\n {APP_NAME} team\n

" + } + }, + { + "id": "pbc_1568971955", + "listRule": "users.id ?= @request.auth.id", + "viewRule": "users.id ?= @request.auth.id", + "createRule": "@request.auth.id != \"\"", + "updateRule": "owner.id ?= @request.auth.id || admins.id ?= @request.auth.id", + "deleteRule": "owner.id ?= @request.auth.id", + "name": "teams", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 100, + "min": 1, + "name": "name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": true, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation3479234172", + "maxSelect": 1, + "minSelect": 1, + "name": "owner", + "presentable": false, + "required": true, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation2732594447", + "maxSelect": 999, + "minSelect": 0, + "name": "admins", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "relation344172009", + "maxSelect": 999, + "minSelect": 0, + "name": "users", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "indexes": [], + "created": "2026-05-15 19:54:02.885Z", + "updated": "2026-05-15 19:54:02.885Z", + "system": false + }, + { + "id": "xiqlzggqxg1e2y7", + "listRule": "team.users.id ?= @request.auth.id", + "viewRule": "team.users.id ?= @request.auth.id", + "createRule": "team.users.id ?= @request.auth.id", + "updateRule": "team.users.id ?= @request.auth.id", + "deleteRule": "team.users.id ?= @request.auth.id", + "name": "events", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "g7nkasyb", + "max": 0, + "min": 0, + "name": "description", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "7n5qjiax", + "max": 0, + "min": 0, + "name": "location", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "hdp9vbeh", + "max": 0, + "min": 0, + "name": "gps", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "select2063623452", + "maxSelect": 1, + "name": "status", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "draft", + "active", + "finished" + ] + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1568971955", + "hidden": false, + "id": "relation3303056927", + "maxSelect": 1, + "minSelect": 0, + "name": "team", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + } + ], + "indexes": [], + "created": "2023-10-28 21:38:00.122Z", + "updated": "2026-05-15 19:54:27.453Z", + "system": false + }, + { + "id": "jo49lycyfs2sz2c", + "listRule": "team.users.id ?= @request.auth.id", + "viewRule": "team.users.id ?= @request.auth.id", + "createRule": "team.users.id ?= @request.auth.id", + "updateRule": "team.users.id ?= @request.auth.id", + "deleteRule": "team.users.id ?= @request.auth.id", + "name": "runs", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "lnfys3xe", + "max": 0, + "min": 0, + "name": "description", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "xiqlzggqxg1e2y7", + "hidden": false, + "id": "fqqevsrf", + "maxSelect": 1, + "minSelect": 0, + "name": "event", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1568971955", + "hidden": false, + "id": "relation3303056927", + "maxSelect": 1, + "minSelect": 0, + "name": "team", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + } + ], + "indexes": [], + "created": "2023-10-30 20:50:18.294Z", + "updated": "2026-05-15 19:54:27.393Z", + "system": false + }, + { + "id": "spgs7e4anufen9u", + "listRule": "team.users.id ?= @request.auth.id", + "viewRule": "team.users.id ?= @request.auth.id", + "createRule": "team.users.id ?= @request.auth.id", + "updateRule": "team.users.id ?= @request.auth.id", + "deleteRule": "team.users.id ?= @request.auth.id", + "name": "riders", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "hcc93nbj", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "hc38dfyc", + "maxSelect": 1, + "maxSize": 5242880, + "mimeTypes": [], + "name": "avatar", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": [], + "type": "file" + }, + { + "cascadeDelete": false, + "collectionId": "xiqlzggqxg1e2y7", + "hidden": false, + "id": "anbugepo", + "maxSelect": 1, + "minSelect": 0, + "name": "event", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2526027604", + "max": 0, + "min": 0, + "name": "number", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text2208304744", + "max": 0, + "min": 0, + "name": "firstname", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text824489398", + "max": 0, + "min": 0, + "name": "lastname", + "pattern": "", + "presentable": true, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1568971955", + "hidden": false, + "id": "relation3303056927", + "maxSelect": 1, + "minSelect": 0, + "name": "team", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + } + ], + "indexes": [], + "created": "2023-10-28 21:37:09.275Z", + "updated": "2026-05-15 19:54:27.372Z", + "system": false + }, + { + "id": "iomx9mcd1ko22ma", + "listRule": "team.users.id ?= @request.auth.id", + "viewRule": "team.users.id ?= @request.auth.id", + "createRule": "team.users.id ?= @request.auth.id", + "updateRule": "team.users.id ?= @request.auth.id", + "deleteRule": "team.users.id ?= @request.auth.id", + "name": "times", + "type": "base", + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "hidden": false, + "id": "3k55v0ld", + "maxSelect": 1, + "name": "status", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "active", + "finished", + "dnf", + "dns", + "dsq" + ] + }, + { + "cascadeDelete": false, + "collectionId": "spgs7e4anufen9u", + "hidden": false, + "id": "r3ixummt", + "maxSelect": 1, + "minSelect": 0, + "name": "rider", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "mfhbdhhe", + "max": "", + "min": "", + "name": "start", + "presentable": false, + "required": false, + "system": false, + "type": "date" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "ybjm5n0k", + "maxSelect": 1, + "minSelect": 0, + "name": "startedBy", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "nkmjfeqt", + "max": "", + "min": "", + "name": "stop", + "presentable": false, + "required": false, + "system": false, + "type": "date" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "hidden": false, + "id": "o2fp9hvv", + "maxSelect": 1, + "minSelect": 0, + "name": "stoppedBy", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "dky8o1vw", + "max": 0, + "min": 0, + "name": "comment", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "cascadeDelete": false, + "collectionId": "jo49lycyfs2sz2c", + "hidden": false, + "id": "ltfephwe", + "maxSelect": 1, + "minSelect": 0, + "name": "run", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "number2728239544", + "max": null, + "min": null, + "name": "correction", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "cascadeDelete": false, + "collectionId": "pbc_1568971955", + "hidden": false, + "id": "relation3303056927", + "maxSelect": 1, + "minSelect": 0, + "name": "team", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + } + ], + "indexes": [], + "created": "2023-10-28 21:37:47.259Z", + "updated": "2026-05-15 19:54:27.415Z", + "system": false + } +] + + // importCollections erwartet das Array selbst, keinen JSON-String — die + // String-Variante heißt importCollectionsByMarshaledJSON. + app.importCollections(collections, false) +}, (app) => { + // Rückwärts: die angelegten Collections wieder entfernen, in umgekehrter + // Reihenfolge, damit keine Relation ins Leere zeigt. + // + // "users" bleibt bewusst stehen: PocketBase legt die Auth-Collection selbst + // an, ein Löschen wäre kein Zurückrollen dieser Migration. + const ids = [ + "_pb_users_auth_", + "pbc_1568971955", + "xiqlzggqxg1e2y7", + "jo49lycyfs2sz2c", + "spgs7e4anufen9u", + "iomx9mcd1ko22ma" +] + + for (const id of ids.slice().reverse()) { + if (id === '_pb_users_auth_') continue + + try { + app.delete(app.findCollectionByNameOrId(id)) + } catch { + // Bereits entfernt — nichts zu tun. + } + } +}) diff --git a/docs/superpowers/plans/2026-08-06-repo-frontend-backend.md b/docs/superpowers/plans/2026-08-06-repo-frontend-backend.md new file mode 100644 index 0000000..941ded8 --- /dev/null +++ b/docs/superpowers/plans/2026-08-06-repo-frontend-backend.md @@ -0,0 +1,972 @@ +# Repo-Umbau auf `frontend/` und `backend/` — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Die SvelteKit-App nach `frontend/` verschieben und unter `backend/` eine eigenständige, lokal per Docker lauffähige PocketBase-Instanz mit versioniertem Schema anlegen. + +**Architecture:** Monorepo — das vorhandene `.git` bleibt im Root, `frontend/` und `backend/` sind Unterverzeichnisse ohne eigene Repos. Das Frontend spricht PocketBase über die konfigurierbare Variable `PUBLIC_PB_URL` an (Default: Remote-Instanz). Das Backend baut PocketBase in einem Alpine-Container, das Schema liegt als Snapshot-Migration in `backend/pb_migrations/`. + +**Tech Stack:** SvelteKit 5 / Svelte 5 Runes, TypeScript, Vite 7, Tailwind 4, PocketBase 0.26 (Client) / 0.39.6 (Container), Docker Compose. + +## Global Constraints + +- Spec: `docs/superpowers/specs/2026-08-06-repo-frontend-backend-design.md` +- **Monorepo:** Nur ein `.git`, im Root. In `frontend/` und `backend/` wird **kein** `git init` ausgeführt. +- **Die Live-Instanz `https://api.stammtisch-hersbruck.de` wird nicht verändert.** Kein Schreibzugriff, keine Migration dagegen anwenden. Nur lesende Aufrufe (`GET /api/collections`). +- **PocketBase-Version im Container:** exakt `0.39.6`, gepinnt über Build-Arg `PB_VERSION`. +- **Fachliche Collections:** genau `users, teams, events, runs, riders, times`. System-Collections (`_superusers`, `_externalAuths`, `_mfas`, `_otps`, `_authOrigins`) gehören **nicht** in die Migration. +- **`frontend/.env` enthält `PB_SUPERUSER_TOKEN` und darf niemals committet werden.** Vor jedem Commit mit `git status` prüfen. +- **Kein `git push`** — nur auf ausdrückliche Anweisung des Nutzers. +- Sprache aller neuen Kommentare, READMEs und Commit-Messages: **Deutsch**, mit korrekten Umlauten. +- Das Repo hat zu Beginn **keinen Commit**. Task 8 legt den initialen Commit an; die Tasks davor committen nicht. + +## Hinweis zur Task-Abfolge + +Dieser Plan committet erst am Ende (Task 8), weil das Repo bis dahin keinen Commit hat und die Spec einen einzigen initialen Commit mit fertiger Struktur vorsieht. Die Tasks 1–7 verändern nur den Arbeitsbaum. Jede Task endet stattdessen mit einer eigenen Verifikation. + +## Dateiübersicht + +| Datei | Verantwortung | +|---|---| +| `frontend/**` | Komplette SvelteKit-App (verschoben aus dem Root) | +| `frontend/.env` | Laufzeit- und Token-Konfiguration, **nicht** versioniert | +| `frontend/.env.example` | Dokumentierte Vorlage, versioniert | +| `frontend/src/lib/stores/pocketbase.svelte.ts` | PocketBase-Client; liest die URL neu aus `PUBLIC_PB_URL` | +| `backend/Dockerfile` | Baut das PocketBase-Image, Version gepinnt | +| `backend/docker-compose.yaml` | Service-Definition inkl. Volume und Healthcheck | +| `backend/entrypoint.sh` | Superuser-Bootstrap beim ersten Start | +| `backend/pb_migrations/1754400000_init_schema.js` | Schema-Snapshot der sechs Collections | +| `backend/pb_hooks/.gitkeep` | Platzhalter, damit das Verzeichnis existiert | +| `backend/.env.example` | Vorlage der Backend-Variablen | +| `backend/.gitignore` | Schließt `pb_data/` und `.env` aus | +| `backend/README.md` | Lokaler Start, Schema, Persistenz, Superuser | +| `.gitignore` (Root) | Ignoriert für beide Teilprojekte | +| `README.md` (Root) | Übersicht über beide Teile | +| `CLAUDE.md` | Angepasste Pfade und Befehle | + +--- + +### Task 1: Frontend nach `frontend/` verschieben + +**Files:** +- Verschieben nach `frontend/`: `src/`, `static/`, `scripts/`, `package.json`, `package-lock.json`, `.npmrc`, `svelte.config.js`, `vite.config.ts`, `tsconfig.json`, `components.json`, `.env` +- Löschen: `node_modules/`, `.svelte-kit/` (im Root) + +**Interfaces:** +- Consumes: nichts +- Produces: Ein vollständiges SvelteKit-Projekt unter `frontend/`, in dem alle folgenden Tasks arbeiten. + +- [ ] **Step 1: Ist-Zustand von `npm run check` festhalten** + +Damit später beurteilt werden kann, was eine Regression ist und was schon vorher kaputt war. + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +npm run check 2>&1 | tail -20 > /tmp/check-vorher.txt +cat /tmp/check-vorher.txt +``` + +Die letzte Zeile nennt die Anzahl Fehler und Warnungen. Diese Zahl notieren — sie ist der Vergleichsmaßstab in Task 3. + +- [ ] **Step 2: Zielverzeichnis anlegen und Dateien verschieben** + +`git mv` funktioniert hier nicht, weil noch nichts getrackt ist — daher normales `mv`. + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +mkdir -p frontend +mv src static scripts package.json package-lock.json .npmrc \ + svelte.config.js vite.config.ts tsconfig.json components.json .env \ + frontend/ +``` + +- [ ] **Step 3: Alte Build-Artefakte im Root entfernen** + +`node_modules` wird nicht mitverschoben — installierte Binaries und Caches enthalten absolute Pfade und wären nach dem Umzug teils unbrauchbar. `.svelte-kit` ist generiert. + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +rm -rf node_modules .svelte-kit +``` + +- [ ] **Step 4: Verschiebung prüfen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +ls -a frontend +ls -a +``` + +Erwartet: In `frontend/` liegen `src`, `static`, `scripts`, `package.json`, `.env` usw. Im Root liegen nur noch `CLAUDE.md`, `docs`, die Markdown-Altdateien, `UML_basic.png`, die drei zu löschenden Schema-Dateien und `.git`/`.gitignore`/`.idea`. + +- [ ] **Step 5: Abhängigkeiten neu installieren** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de/frontend +npm ci +``` + +Erwartet: Läuft ohne Fehler durch. Bei `EBADENGINE`-Abbruch (`.npmrc` setzt `engine-strict=true`) die Node-Version prüfen (`node -v`) und melden statt `engine-strict` zu entfernen. + +--- + +### Task 2: PocketBase-URL über `PUBLIC_PB_URL` konfigurierbar machen + +**Files:** +- Modify: `frontend/src/lib/stores/pocketbase.svelte.ts:4` +- Modify: `frontend/.env` +- Create: `frontend/.env.example` + +**Interfaces:** +- Consumes: das verschobene Frontend aus Task 1 +- Produces: `export const api` unverändert als `TypedPocketBase`; die URL kommt nun aus `PUBLIC_PB_URL`. Signatur und Name bleiben gleich, alle bestehenden Importe funktionieren weiter. + +- [ ] **Step 1: `PUBLIC_PB_URL` in `.env` ergänzen** + +Die bestehenden Variablen bleiben unangetastet. Ans Ende von `frontend/.env` anfügen: + +``` +# Basis-URL der PocketBase-Instanz, die das Frontend anspricht. +# Default ist die produktive Remote-Instanz. Für Arbeit gegen das lokale +# Backend aus ../backend auf http://127.0.0.1:8090 umstellen. +PUBLIC_PB_URL=https://api.stammtisch-hersbruck.de +``` + +- [ ] **Step 2: `frontend/.env.example` anlegen** + +Diese Datei ist versioniert und enthält **keine** echten Tokens. + +``` +# Vorlage für die lokale .env — kopieren und ausfüllen: +# cp .env.example .env +# +# Die echte .env steht in .gitignore und darf nicht committet werden, +# sie enthält den Superuser-Token. + +# Basis-URL der PocketBase-Instanz, die das Frontend anspricht. +# Produktiv: https://api.stammtisch-hersbruck.de +# Lokales Backend aus ../backend: http://127.0.0.1:8090 +PUBLIC_PB_URL=https://api.stammtisch-hersbruck.de + +# Ziel für die Typgenerierung (npm run generate-pocketbase-types) und die +# Scripts unter scripts/. Zeigt üblicherweise auf dieselbe Instanz wie oben. +PB_TYPEGEN_URL=https://api.stammtisch-hersbruck.de/ + +# Superuser-Token für Schema-Zugriffe. Im PocketBase-Admin-UI erzeugen. +# Niemals committen. +PB_SUPERUSER_TOKEN= +PB_TYPEGEN_TOKEN= +``` + +- [ ] **Step 3: Den Store auf die Variable umstellen** + +In `frontend/src/lib/stores/pocketbase.svelte.ts` die Zeilen 1–4 ersetzen. + +Vorher: +```ts +import PocketBase, {type AuthRecord, type RecordModel} from 'pocketbase' +import type {TypedPocketBase} from '$lib/types' + +export const api = new PocketBase('https://api.stammtisch-hersbruck.de') as TypedPocketBase +``` + +Nachher: +```ts +import PocketBase, {type AuthRecord, type RecordModel} from 'pocketbase' +import {PUBLIC_PB_URL} from '$env/static/public' +import type {TypedPocketBase} from '$lib/types' + +export const api = new PocketBase(PUBLIC_PB_URL) as TypedPocketBase +``` + +`$env/static/public` ist SvelteKit-intern und benötigt keine Abhängigkeit. Nur Variablen mit dem Präfix `PUBLIC_` sind darüber erreichbar — deshalb heißt sie so. + +- [ ] **Step 4: Prüfen, dass keine hartkodierte URL zurückbleibt** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de/frontend +grep -rn "api\.stammtisch-hersbruck\.de" src/ +``` + +Erwartet: **keine Ausgabe**. Treffer in `src/` müssen ebenfalls auf `PUBLIC_PB_URL` umgestellt werden. + +--- + +### Task 3: Frontend verifizieren + +**Files:** +- Keine Änderungen — reine Prüfung. + +**Interfaces:** +- Consumes: Tasks 1 und 2 +- Produces: Nachweis, dass Verschiebung und URL-Umstellung nichts kaputt gemacht haben. + +- [ ] **Step 1: Typprüfung laufen lassen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de/frontend +npm run check 2>&1 | tail -20 +``` + +Erwartet: Die Fehler-/Warnungszahl ist **nicht höher** als in `/tmp/check-vorher.txt` aus Task 1. Bestehende Fehler von vorher sind keine Regression. Ein neuer Fehler zu `$env/static/public` oder `PUBLIC_PB_URL` bedeutet, dass die Variable in `.env` fehlt oder falsch geschrieben ist. + +- [ ] **Step 2: Dev-Server starten** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de/frontend +npm run dev +``` + +Erwartet: Vite startet auf Port 31337 ohne Fehler. + +- [ ] **Step 3: App im Browser prüfen** + +`http://stammtisch-hersbruck.de.localhost:31337` öffnen. + +Erwartet: Die Seite lädt. In den DevTools unter *Network* gehen die API-Aufrufe an `https://api.stammtisch-hersbruck.de` — das belegt, dass `PUBLIC_PB_URL` greift. Die Konsole zeigt keine Fehler zu fehlenden Modulen oder undefinierter URL. + +Danach den Dev-Server mit `Ctrl+C` beenden. + +--- + +### Task 4: Backend-Grundgerüst anlegen + +**Files:** +- Create: `backend/Dockerfile` +- Create: `backend/docker-compose.yaml` +- Create: `backend/entrypoint.sh` +- Create: `backend/.gitignore` +- Create: `backend/.env.example` +- Create: `backend/pb_hooks/.gitkeep` + +**Interfaces:** +- Consumes: nichts aus vorherigen Tasks +- Produces: Ein baubarer PocketBase-Container. Task 5 legt die Migration in `backend/pb_migrations/` ab, die das Dockerfile bereits hineinkopiert; Task 6 startet den Container. + +- [ ] **Step 1: Verzeichnisse anlegen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +mkdir -p backend/pb_migrations backend/pb_hooks +touch backend/pb_hooks/.gitkeep +``` + +`pb_hooks` bleibt vorerst leer — es gibt in diesem Projekt keine serverseitigen Hooks. Das Verzeichnis existiert trotzdem, weil das Dockerfile es kopiert. + +- [ ] **Step 2: `backend/Dockerfile` schreiben** + +```dockerfile +FROM alpine:3.21 + +# Version bewusst gepinnt: Der GitHub-Asset heißt +# pocketbase__linux_amd64.zip, ein "latest" gibt es nicht. Ohne Pin +# zöge jeder Redeploy unangekündigt eine neue Version inklusive möglicher +# DB-Schema-Migrationen. +ARG PB_VERSION=0.39.6 + +RUN apk add --no-cache ca-certificates unzip wget + +RUN wget -q https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip -O /tmp/pb.zip \ + && unzip /tmp/pb.zip -d /pb/ \ + && rm /tmp/pb.zip + +COPY ./pb_migrations /pb/pb_migrations +COPY ./pb_hooks /pb/pb_hooks + +COPY ./entrypoint.sh /pb/entrypoint.sh +RUN chmod +x /pb/entrypoint.sh + +EXPOSE 8090 + +ENTRYPOINT ["/pb/entrypoint.sh"] + +CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb/pb_data", "--migrationsDir=/pb/pb_migrations", "--hooksDir=/pb/pb_hooks"] +``` + +- [ ] **Step 3: `backend/entrypoint.sh` schreiben** + +```sh +#!/bin/sh +set -e + +# Superuser nur beim allerersten Start anlegen. Existiert der Account bereits, +# scheitert "create" von selbst mit "email: Value must be unique." und das +# Passwort bleibt unangetastet. Damit überschreibt ein Redeploy keine +# Passwortänderung, die im Admin-UI gemacht wurde. +# +# Bewusst NICHT "superuser upsert": das würde bei jedem Start das Passwort aus +# der Env zurückschreiben und UI-seitige Änderungen still aushebeln. +# +# Ohne die beiden Variablen wird der Schritt übersprungen; PocketBase zeigt dann +# beim ersten Start einen Installer-Link im Log. +if [ -n "$SUPERUSER_EMAIL" ] && [ -n "$SUPERUSER_PASSWORD" ]; then + # PocketBase liefert auch im Fehlerfall Exitcode 0 und meldet den Fehler nur + # auf stdout — deshalb wird hier die Ausgabe ausgewertet, nicht der Status. + out=$(/pb/pocketbase superuser create "$SUPERUSER_EMAIL" "$SUPERUSER_PASSWORD" \ + --dir=/pb/pb_data 2>&1) || true + + case "$out" in + *"must be unique"*) + echo "Superuser existiert bereits — Passwort bleibt unverändert" + ;; + *"Successfully created"*) + echo "$out" + ;; + *) + # Unerwarteter Fehler (z. B. zu kurzes Passwort): sichtbar machen und + # abbrechen, statt mit unklarem Zustand weiterzulaufen. + echo "Superuser konnte nicht angelegt werden: $out" >&2 + exit 1 + ;; + esac +else + echo "SUPERUSER_EMAIL/SUPERUSER_PASSWORD nicht gesetzt — Superuser wird nicht angelegt" +fi + +exec "$@" +``` + +- [ ] **Step 4: `backend/docker-compose.yaml` schreiben** + +```yaml +services: + pocketbase: + build: + context: . + args: + # Version-Pin. Update: hier bzw. im Dockerfile hochsetzen und committen. + PB_VERSION: ${PB_VERSION:-0.39.6} + restart: unless-stopped + ports: + - "8090:8090" + volumes: + # Persistenz der SQLite-DB inklusive aller Laufzeitdaten, Uploads, + # Mail-Settings und des Superusers. Ohne dieses Mapping ist nach jedem + # Redeploy alles weg — das Schema käme zwar aus pb_migrations zurück, + # die Daten aber nicht. + - ./pb_data:/pb/pb_data + environment: + # Superuser wird nur beim allerersten Start angelegt (siehe entrypoint.sh) + SUPERUSER_EMAIL: ${SUPERUSER_EMAIL:-} + SUPERUSER_PASSWORD: ${SUPERUSER_PASSWORD:-} + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8090/api/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s +``` + +- [ ] **Step 5: `backend/.gitignore` schreiben** + +``` +pb_data/ +*.db +.DS_Store +.env +.env.* +!.env.example +``` + +- [ ] **Step 6: `backend/.env.example` schreiben** + +``` +# Vorlage für die lokale .env — kopieren und ausfüllen: +# cp .env.example .env +# +# Diese Datei enthält nur Platzhalter und ist versioniert. Die echte .env steht +# in .gitignore und darf nicht committet werden. + +# Admin-Zugang, wird nur beim allerersten Start angelegt. Existiert der Account +# bereits, bleibt das Passwort unverändert — spätere Änderungen gehören ins +# Admin-UI, nicht in diese Datei. Mindestens 8 Zeichen. +# Beide Variablen weglassen = PocketBase gibt beim ersten Start einen +# Installer-Link im Log aus (30 Minuten gültig). +SUPERUSER_EMAIL=admin@example.de +SUPERUSER_PASSWORD=bitte-aendern-min-8-zeichen + +# Optional: überschreibt den in docker-compose.yaml gepinnten Default. +# Nur setzen, wenn bewusst eine andere PocketBase-Version gebaut werden soll. +#PB_VERSION=0.39.6 +``` + +- [ ] **Step 7: Dateien prüfen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de/backend +ls -a +sh -n entrypoint.sh && echo "entrypoint.sh: Syntax ok" +``` + +Erwartet: Alle sechs Dateien plus `pb_migrations/` und `pb_hooks/` sind da, und die Syntaxprüfung meldet „ok". + +--- + +### Task 5: Schema-Migration aus der Live-Instanz erzeugen + +**Files:** +- Create: `backend/pb_migrations/1754400000_init_schema.js` + +**Interfaces:** +- Consumes: `backend/pb_migrations/` aus Task 4; `frontend/.env` (für `PB_TYPEGEN_URL` und `PB_SUPERUSER_TOKEN`) aus Task 1 +- Produces: Eine Migration, die beim Containerstart die sechs Collections `users, teams, events, runs, riders, times` anlegt. + +- [ ] **Step 1: Live-Schema abrufen und die Migration generieren** + +Das Schema wird nicht abgeschrieben, sondern aus dem Live-Abruf generiert — 23 KB JSON von Hand zu übertragen wäre fehleranfällig. Der Aufruf ist **rein lesend**. + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +set -a && . ./frontend/.env && set +a + +curl -sL -H "Authorization: $PB_SUPERUSER_TOKEN" \ + "${PB_TYPEGEN_URL%/}/api/collections?perPage=200" -o /tmp/live_schema.json + +python3 - <<'PY' +import json + +with open('/tmp/live_schema.json') as f: + data = json.load(f) + +# System-Collections (_superusers, _mfas, ...) legt PocketBase selbst an. +keep = [c for c in data['items'] if not c['name'].startswith('_')] + +# Reihenfolge so, dass Relationsziele vor ihren Nutzern stehen. +order = {'users': 0, 'teams': 1, 'events': 2, 'runs': 3, 'riders': 4, 'times': 5} +keep.sort(key=lambda c: order.get(c['name'], 99)) + +names = [c['name'] for c in keep] +expected = ['users', 'teams', 'events', 'runs', 'riders', 'times'] +assert names == expected, f'Unerwartete Collections: {names}' + +collections = json.dumps(keep, indent=4, ensure_ascii=False) +ids = json.dumps([c['id'] for c in keep], indent=4) + +migration = f'''/// + +// Schema-Snapshot der sechs fachlichen Collections, abgezogen von der +// produktiven Instanz. System-Collections (_superusers, _mfas, ...) legt +// PocketBase selbst an und stehen deshalb nicht hier drin. +// +// importCollections wird mit deleteMissing = false aufgerufen: Die Migration +// legt an und aktualisiert, löscht aber nichts, was nicht im Snapshot steht. + +migrate((app) => {{ + const collections = {collections} + + app.importCollections(JSON.stringify(collections), false) +}}, (app) => {{ + // Rückwärts: die angelegten Collections wieder entfernen, in umgekehrter + // Reihenfolge, damit keine Relation ins Leere zeigt. + // + // "users" bleibt bewusst stehen: PocketBase legt die Auth-Collection selbst + // an, ein Löschen wäre kein Zurückrollen dieser Migration. + const ids = {ids} + + for (const id of ids.slice().reverse()) {{ + if (id === '_pb_users_auth_') continue + + try {{ + app.delete(app.findCollectionByNameOrId(id)) + }} catch {{ + // Bereits entfernt — nichts zu tun. + }} + }} +}}) +''' + +with open('backend/pb_migrations/1754400000_init_schema.js', 'w') as f: + f.write(migration) + +print('Migration geschrieben,', len(migration), 'Bytes, Collections:', names) +PY +``` + +Erwartet: Die Ausgabe nennt die sechs Collections in genau dieser Reihenfolge. Schlägt das `assert` fehl, hat sich das Live-Schema geändert — dann **abbrechen und melden**, nicht die Erwartung anpassen. + +Der Zugriff scheitert mit HTTP 401, wenn `PB_SUPERUSER_TOKEN` abgelaufen ist. In dem Fall im Admin-UI einen neuen Token erzeugen und in `frontend/.env` eintragen. + +- [ ] **Step 2: Erzeugte Migration prüfen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +node --check backend/pb_migrations/1754400000_init_schema.js \ + && echo "Syntax ok (migrate ist erst zur Laufzeit definiert, das ist erwartet)" +grep -c '"name"' backend/pb_migrations/1754400000_init_schema.js +``` + +Erwartet: `node --check` meldet keinen Syntaxfehler. Die `grep`-Zahl liegt deutlich über 50 (sechs Collections mit allen Feldern). + +- [ ] **Step 3: Sicherstellen, dass keine Tokens in die Migration geraten sind** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +grep -in "token\|secret\|password" backend/pb_migrations/1754400000_init_schema.js | head +``` + +Erwartet: Treffer nur als **Feldnamen** aus dem Schema (`tokenKey`, `password` in der `users`-Collection) — das sind Felddefinitionen, keine Werte. Erscheint irgendwo ein tatsächlicher Tokenwert, ist die Migration unbrauchbar: abbrechen und melden. + +--- + +### Task 6: Backend starten und verifizieren + +**Files:** +- Keine Änderungen — reine Prüfung. + +**Interfaces:** +- Consumes: Tasks 4 und 5 +- Produces: Nachweis, dass ein frischer Container das Schema herstellt. + +- [ ] **Step 1: Prüfen, ob Docker verfügbar ist** + +```bash +docker info >/dev/null 2>&1 && echo "Docker läuft" || echo "Docker NICHT verfügbar" +``` + +Meldet das „NICHT verfügbar", werden die Steps 2–5 übersprungen. Dann gilt: Das Backend ist **ungeprüft** und muss in Task 8 und im Abschlussbericht ausdrücklich so bezeichnet werden. Nicht als erledigt darstellen. + +- [ ] **Step 2: Lokale `.env` anlegen und Container bauen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de/backend +cp .env.example .env +docker compose up -d --build +``` + +Erwartet: Der Build lädt PocketBase 0.39.6 und startet den Container. + +Für die lokale Prüfung genügen die Platzhalter aus `.env.example`; das Passwort erfüllt die Mindestlänge von 8 Zeichen. Diese `.env` ist gitignored. + +- [ ] **Step 3: Health und Superuser-Bootstrap prüfen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de/backend +sleep 5 +curl -s http://127.0.0.1:8090/api/health +echo +docker compose logs | grep -i "superuser\|migrat" | tail -10 +``` + +Erwartet: `/api/health` antwortet mit `{"code":200,...}`. Im Log steht `Successfully created new superuser` und ein Hinweis auf die angewandte Migration. + +- [ ] **Step 4: Schema im Container prüfen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de/backend +set -a && . ./.env && set +a +TOKEN=$(curl -s -X POST http://127.0.0.1:8090/api/collections/_superusers/auth-with-password \ + -H "Content-Type: application/json" \ + -d "{\"identity\":\"$SUPERUSER_EMAIL\",\"password\":\"$SUPERUSER_PASSWORD\"}" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") + +curl -s -H "Authorization: $TOKEN" \ + "http://127.0.0.1:8090/api/collections?perPage=200" \ + | python3 -c " +import sys, json +d = json.load(sys.stdin) +names = sorted(c['name'] for c in d['items'] if not c['name'].startswith('_')) +print('Gefunden:', names) +expected = sorted(['users','teams','events','runs','riders','times']) +print('OK' if names == expected else 'FEHLT: ' + str(set(expected) - set(names))) +" +``` + +Erwartet: `Gefunden:` listet die sechs Collections und die Zeile darunter sagt `OK`. + +- [ ] **Step 5: Container wieder stoppen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de/backend +docker compose down +``` + +`pb_data/` bleibt liegen und ist gitignored. + +--- + +### Task 7: Dokumentation und Root-Dateien anpassen + +**Files:** +- Create: `backend/README.md` +- Modify: `.gitignore` (Root) +- Modify: `README.md` (Root) +- Modify: `CLAUDE.md` +- Delete: `pocketbase_schema.json`, `example_pb_schema.json`, `pocketbase_migrate.zip` + +**Interfaces:** +- Consumes: Tasks 1–6 +- Produces: Ein Repo, dessen Dokumentation die neue Struktur beschreibt. + +- [ ] **Step 1: Veraltete Dateien löschen** + +Alle drei sind untracked — sie sind nach dem Löschen endgültig weg. Das ist so abgestimmt: `pocketbase_schema.json` nennt Collections (`stages`, `organizers`, `results`), die auf der Live-Instanz nicht existieren, und ist damit nachweislich veraltet. + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +rm -f pocketbase_schema.json example_pb_schema.json pocketbase_migrate.zip +ls -1 +``` + +- [ ] **Step 2: `backend/README.md` schreiben** + +```markdown +# stammtisch-hersbruck.de — Backend (PocketBase) + +PocketBase-Instanz für Events, Läufe, Fahrer, Zeiten und Teams. + +## Lokal starten + +`.env` aus der Vorlage anlegen (die `.env` selbst steht in `.gitignore`): + + cp .env.example .env + +und die Werte eintragen — `.env.example` beschreibt jede Variable. Dann: + + docker compose up -d --build + +Admin-UI: http://127.0.0.1:8090/_/ — Login mit `SUPERUSER_EMAIL`/`SUPERUSER_PASSWORD`. + +Damit das Frontend gegen diese Instanz läuft, in `../frontend/.env` +`PUBLIC_PB_URL=http://127.0.0.1:8090` setzen. + +## Schema + +Das Schema liegt als versionierte Migration in `pb_migrations/` und wird beim +Start automatisch angewendet. Es enthält die sechs fachlichen Collections +`users`, `teams`, `events`, `runs`, `riders`, `times`. + +`1754400000_init_schema.js` ist ein Snapshot der produktiven Instanz. Er wurde +per `GET /api/collections` abgezogen und ruft `importCollections` mit +`deleteMissing = false` auf — die Migration legt an und aktualisiert, löscht +aber nichts, was nicht im Snapshot steht. + +Schema-Änderungen im Admin-UI erzeugen in `pb_migrations/` neue Dateien; diese +müssen committet werden. + +Ein frisch gestarteter Container hat damit dasselbe Schema wie die produktive +Instanz — aber **keine** Daten. + +## Datenpersistenz + +Sämtliche Laufzeitdaten liegen in der SQLite-DB unter `/pb/pb_data`: Datensätze, +Uploads, die SMTP-Einstellungen und der Superuser. Ohne gemountetes Volume auf +diesem Pfad sind sie nach jedem Redeploy verloren. + +Das Volume-Mapping `./pb_data:/pb/pb_data` steht in der `docker-compose.yaml`, +damit die Persistenz-Konfiguration versioniert im Repo liegt. + +**Trügerisches Signal:** Die Collections kommen aus der Migration und sind nach +einem Redeploy auch dann da, wenn gar kein Volume existiert. Sie taugen **nicht** +als Nachweis funktionierender Persistenz — das zeigt nur ein Datensatz, der in +keiner Migration steht. + +Mount prüfen: + + docker inspect --format '{{json .Mounts}}' | jq + +Erscheint dort kein Eintrag mit `"Destination": "/pb/pb_data"`, fehlt die Persistenz. + +`pb_data/` steht in `.gitignore` und gehört dort auch hin. + +## Superuser-Verhalten + +`entrypoint.sh` ruft `superuser create` auf — bewusst **nicht** `upsert`. +Existiert der Account bereits, scheitert `create` mit „Value must be unique" und +das Passwort bleibt unangetastet. Eine im Admin-UI vorgenommene +Passwortänderung überlebt damit jeden Redeploy. + +| Situation | Verhalten | +|---|---| +| Erster Start, Variablen gesetzt | Superuser wird angelegt | +| Neustart, Account existiert | „Superuser existiert bereits — Passwort bleibt unverändert" | +| Passwort im UI geändert, dann Neustart | UI-Passwort gilt weiter, Env wird ignoriert | +| Variablen nicht gesetzt | Übersprungen, PocketBase gibt Installer-Link im Log aus | +| Passwort zu kurz (< 8 Zeichen) | Container bricht mit Exitcode 1 ab | + +Passwort später ändern: **im Admin-UI**, nicht über die Env — eine Änderung der +Env-Variable hat keine Wirkung mehr, sobald der Account existiert. + +## PocketBase aktualisieren + +Die Version ist über das Build-Arg `PB_VERSION` gepinnt (Dockerfile und +`docker-compose.yaml`). Wert hochsetzen, committen, neu bauen. + +Ein `latest` gibt es bewusst nicht: Der GitHub-Asset heißt +`pocketbase__linux_amd64.zip`, enthält die Version also im Dateinamen. +Der Pin ist auch gewollt — sonst zieht jeder Rebuild unangekündigt eine neue +Version, inklusive möglicher DB-Schema-Migrationen. + +Vor einem Update: Changelog prüfen +(https://github.com/pocketbase/pocketbase/releases) und `pb_data` sichern — +PocketBase migriert die DB beim Start automatisch, ein Downgrade ist danach +nicht mehr ohne Weiteres möglich. +``` + +- [ ] **Step 3: Root-`.gitignore` ersetzen** + +Die bisherige `.gitignore` stammt aus dem SvelteKit-Template und geht davon aus, dass die App im Root liegt. Vollständiger neuer Inhalt: + +``` +# Dependencies +node_modules + +# Build-Output +.output +.vercel +.netlify +.wrangler +.svelte-kit +build + +# OS +.DS_Store +Thumbs.db + +# Env — enthält Tokens, gehört nie ins Repo +.env +.env.* +!.env.example + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# PocketBase-Laufzeitdaten +backend/pb_data/ +*.db + +# IDE +.idea/ +``` + +Die Muster ohne führenden Schrägstrich greifen in jedem Unterverzeichnis — `node_modules` deckt damit auch `frontend/node_modules` ab. + +- [ ] **Step 4: Root-`README.md` ersetzen** + +```markdown +# stammtisch-hersbruck.de + +Zeitmessung und Verwaltung für Läufe des Stammtisch Hersbruck. + +Das Repository enthält beide Teile der Anwendung: + +| Verzeichnis | Inhalt | +|---|---| +| [`frontend/`](frontend/) | SvelteKit-5-Anwendung (Svelte 5 Runes, Tailwind 4) | +| [`backend/`](backend/) | PocketBase-Instanz — Dockerfile, Schema-Migrationen | + +## Schnellstart + +Frontend gegen die produktive Instanz: + + cd frontend + cp .env.example .env # Werte eintragen + npm ci + npm run dev + +Die App läuft dann auf http://stammtisch-hersbruck.de.localhost:31337 + +Backend lokal (optional — der Default zeigt auf die produktive Instanz): + + cd backend + cp .env.example .env # Werte eintragen + docker compose up -d --build + +Danach in `frontend/.env` `PUBLIC_PB_URL=http://127.0.0.1:8090` setzen. + +Details stehen in [`backend/README.md`](backend/README.md). + +## PocketBase + +Produktiv: https://api.stammtisch-hersbruck.de + +Welche Instanz das Frontend anspricht, entscheidet `PUBLIC_PB_URL` in +`frontend/.env`. Das Schema ist als Migration in `backend/pb_migrations/` +versioniert. +``` + +- [ ] **Step 5: `CLAUDE.md` an die neue Struktur anpassen** + +Drei Stellen ändern, alles andere bleibt. + +**a)** Im Abschnitt „Development Commands" den einleitenden Satz und den Codeblock ersetzen durch: + +````markdown +Alle Frontend-Befehle laufen aus `frontend/`: + +```bash +cd frontend + +# Start development server (runs on http://stammtisch-hersbruck.de.localhost:31337) +npm run dev + +# Build for production +npm run build + +# Preview production build +npm run preview + +# Type-check Svelte files +npm run check + +# Type-check with watch mode +npm run check:watch + +# Generate TypeScript types from PocketBase schema +npm run generate-pocketbase-types +``` + +Backend (PocketBase) aus `backend/`: + +```bash +cd backend + +# PocketBase lokal starten (Admin-UI auf http://127.0.0.1:8090/_/) +docker compose up -d --build + +# Stoppen +docker compose down +``` +```` + +**b)** Im Abschnitt „Backend Integration (PocketBase)" den ersten Aufzählungspunkt ersetzen: + +Vorher: +```markdown +- **API Base URL**: `https://api.stammtisch-hersbruck.de` +``` + +Nachher: +```markdown +- **API Base URL**: konfigurierbar über `PUBLIC_PB_URL` in `frontend/.env`. + Default ist die produktive Instanz `https://api.stammtisch-hersbruck.de`; + für das lokale Backend aus `backend/` auf `http://127.0.0.1:8090` umstellen. +- **Schema**: versioniert als Migration in `backend/pb_migrations/`. Änderungen + im Admin-UI erzeugen dort neue Dateien, die committet werden müssen. +``` + +Im selben Abschnitt die Collections-Zeile korrigieren — `organizers`, `stages` und `results` existieren nicht: + +Vorher: +```markdown +- **Collections**: events, organizers, results, riders, stages, users +``` + +Nachher: +```markdown +- **Collections**: users, teams, events, runs, riders, times +``` + +**c)** Im Abschnitt „Project Configuration" ergänzen: + +```markdown +- **Repo-Struktur**: Monorepo mit `frontend/` (SvelteKit) und `backend/` + (PocketBase). Ein einziges Git-Repo im Root. +``` + +Und den Pfad-Alias-Punkt präzisieren: + +Vorher: +```markdown +- **Path alias**: `@/*` resolves to `./src/lib/*` (configured in svelte.config.js) +``` + +Nachher: +```markdown +- **Path alias**: `@/*` resolves to `./src/lib/*` (configured in frontend/svelte.config.js) +``` + +- [ ] **Step 6: Dokumentation gegenprüfen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +grep -n "organizers\|stages\|results" CLAUDE.md +``` + +Erwartet: Treffer nur dort, wo es fachlich nicht um Collections geht (etwa im Satz über den Zweck der App). Steht `organizers`/`stages` noch in der Collections-Liste, wurde Step 5b nicht vollständig ausgeführt. + +--- + +### Task 8: Initialen Commit anlegen + +**Files:** +- Keine inhaltlichen Änderungen. + +**Interfaces:** +- Consumes: Tasks 1–7 +- Produces: Den ersten Commit des Repos mit der fertigen Struktur. + +- [ ] **Step 1: Prüfen, was committet würde** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +git add -A +git status --short +``` + +- [ ] **Step 2: Sicherstellen, dass keine Secrets und keine Artefakte dabei sind** + +Das ist der wichtigste Schritt dieser Task — `frontend/.env` enthält `PB_SUPERUSER_TOKEN`. + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +git diff --cached --name-only | grep -E "\.env$|\.env\.|node_modules|\.svelte-kit|pb_data" \ + || echo "Sauber: keine .env, keine node_modules, kein pb_data im Index" +``` + +Erwartet: die Meldung „Sauber…". Erscheint stattdessen ein Treffer (außer `.env.example`), **nicht committen**, sondern die Datei mit `git rm --cached ` aus dem Index nehmen und die `.gitignore` korrigieren. + +Zusätzlich zur Sicherheit: + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +git diff --cached | grep -in "PB_SUPERUSER_TOKEN=." | grep -v "example" | head +``` + +Erwartet: **keine Ausgabe**. Ein Treffer bedeutet, dass ein echter Tokenwert im Commit landen würde. + +- [ ] **Step 3: Committen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +git commit -m "$(cat <<'EOF' +chore: Repo-Struktur mit frontend/ und backend/ aufsetzen + +Die SvelteKit-App liegt unter frontend/, das Backend unter backend/ als +eigenständige PocketBase-Instanz mit Dockerfile, docker-compose und +versioniertem Schema. + +- PocketBase-URL über PUBLIC_PB_URL konfigurierbar, Default bleibt die + produktive Instanz +- Schema als Snapshot-Migration der sechs fachlichen Collections + (users, teams, events, runs, riders, times) +- Veraltete Schema-Dateien im Root entfernt: pocketbase_schema.json nannte + Collections, die auf der Live-Instanz nicht existieren + +Co-Authored-By: Claude Opus 5 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Ergebnis prüfen** + +```bash +cd /home/dne/Projekte/stammtisch-hersbruck.de +git log --stat -1 | head -30 +git status --short +``` + +Erwartet: Ein Commit, `git status` ist leer. + +**Kein `git push`.** Der bleibt ausdrücklicher Anweisung des Nutzers vorbehalten. + +--- + +## Abschlussbericht + +Nach Task 8 an den Nutzer berichten: + +- Was verifiziert wurde und womit (`npm run check`, Dev-Server im Browser, `/api/health`, Collection-Abgleich im Container) +- Falls Docker in Task 6 nicht verfügbar war: das Backend ausdrücklich als **ungeprüft** benennen, nicht als erledigt +- Die drei gelöschten Dateien nennen +- Dass nicht gepusht wurde diff --git a/docs/superpowers/specs/2026-08-06-repo-frontend-backend-design.md b/docs/superpowers/specs/2026-08-06-repo-frontend-backend-design.md new file mode 100644 index 0000000..f765f5b --- /dev/null +++ b/docs/superpowers/specs/2026-08-06-repo-frontend-backend-design.md @@ -0,0 +1,209 @@ +# Repo-Umbau auf `frontend/` und `backend/` + +**Datum:** 2026-08-06 +**Status:** Entwurf, vom Nutzer freigegeben + +## Ziel + +Das Repository so umbauen, dass die SvelteKit-App unter `frontend/` und eine +eigenständige, lokal lauffähige PocketBase-Instanz unter `backend/` liegt — +nach dem Vorbild von `therapiezentrum-fortschritt-landingpage`. + +Anders als beim Vorbild bleibt dies ein **Monorepo**: Das vorhandene `.git` im +Root bleibt bestehen, `frontend/` und `backend/` sind Unterverzeichnisse ohne +eigene Repos. + +## Ausgangslage + +- SvelteKit 5 mit Svelte-5-Runes, PocketBase als Backend +- App liegt direkt im Repo-Root (`src/`, `static/`, `scripts/`, Configs) +- Git-Repo existiert, Branch `master`, **noch kein Commit** — alles untracked +- PocketBase läuft ausschließlich remote unter `https://api.stammtisch-hersbruck.de` +- Die URL ist in `src/lib/stores/pocketbase.svelte.ts:4` hartkodiert +- Schema-Pflege bisher über `scripts/migrate-schema.ts` per Superuser-Token +- Es gibt kein `pb_migrations`, kein `pb_hooks`, kein Dockerfile + +### Befund zum vorhandenen Schema + +Der Live-Abruf über `GET /api/collections` liefert die fachlichen Collections +`users, riders, times, events, runs, teams` (plus System-Collections +`_superusers, _externalAuths, _mfas, _otps, _authOrigins`). + +Die Datei `pocketbase_schema.json` im Root nennt dagegen zusätzlich `stages`, +`organizers` und `results`, die **live nicht existieren**. Sie ist damit +nachweislich veraltet. Maßgeblich ist der Live-Stand — er deckt sich mit den +vorhandenen Stores unter `src/lib/stores/`. + +## Zielstruktur + +``` +stammtisch-hersbruck.de/ ← .git bleibt hier (Monorepo) +├── CLAUDE.md +├── README.md +├── .gitignore +├── docs/superpowers/{specs,plans}/ +├── frontend/ +│ ├── src/ static/ scripts/ +│ ├── package.json package-lock.json .npmrc +│ ├── svelte.config.js vite.config.ts tsconfig.json +│ ├── components.json +│ ├── .env .env.example .gitignore +│ └── README.md +└── backend/ + ├── Dockerfile docker-compose.yaml entrypoint.sh + ├── .env.example .gitignore README.md + ├── pb_migrations/ + │ └── _init_schema.js + ├── pb_hooks/ (leer, .gitkeep) + └── pb_data/ (gitignored, entsteht beim ersten Start) +``` + +## Umsetzung + +### 1. Frontend verschieben + +Alle App-Dateien nach `frontend/` verschieben: +`src/`, `static/`, `scripts/`, `package.json`, `package-lock.json`, `.npmrc`, +`svelte.config.js`, `vite.config.ts`, `tsconfig.json`, `components.json`, `.env`. + +`node_modules/` und `.svelte-kit/` werden **nicht** verschoben, sondern gelöscht +und in `frontend/` per `npm ci` neu erzeugt — verschobene `node_modules` +enthalten absolute Pfade in Binaries und Caches. + +Keine Config braucht eine Pfadanpassung: Alle Pfade in `svelte.config.js`, +`vite.config.ts` und `tsconfig.json` sind relativ zum jeweiligen Projekt-Root. +Die Scripts unter `scripts/` lesen `.env` relativ zum Arbeitsverzeichnis und +laufen über npm-Scripts, also aus `frontend/` heraus — sie funktionieren +unverändert. + +### 2. PocketBase-URL konfigurierbar machen + +`src/lib/stores/pocketbase.svelte.ts` verwendet statt der hartkodierten URL: + +```ts +import { PUBLIC_PB_URL } from '$env/static/public' +export const api = new PocketBase(PUBLIC_PB_URL) as TypedPocketBase +``` + +In `frontend/.env` und `frontend/.env.example`: + +``` +PUBLIC_PB_URL=https://api.stammtisch-hersbruck.de +``` + +Der Default bleibt damit die Remote-Instanz — nichts am bisherigen Verhalten +ändert sich. Für Arbeit gegen das lokale Backend wird die Variable auf +`http://127.0.0.1:8090` gesetzt. + +Die bestehenden Variablen `PB_TYPEGEN_URL`, `PB_TYPEGEN_TOKEN` und +`PB_SUPERUSER_TOKEN` bleiben unverändert in `.env` (Typgenerierung und +Migrations-Scripts). + +### 3. Backend aufsetzen + +Übernommen aus dem Vorbild, projektspezifisch angepasst: + +**`Dockerfile`** — Alpine-Basis, PocketBase über Build-Arg `PB_VERSION` +gepinnt, kopiert `pb_migrations/` und `pb_hooks/`, startet über `entrypoint.sh` +mit `serve --http=0.0.0.0:8090 --dir=/pb/pb_data`. + +**`docker-compose.yaml`** — Volume `./pb_data:/pb/pb_data` für Persistenz, +`restart: unless-stopped`, Healthcheck gegen `/api/health`, Port 8090. +Env: `SUPERUSER_EMAIL`, `SUPERUSER_PASSWORD`, `PB_VERSION`. + +Die TZF-spezifischen Notify-Variablen (`TZF_NOTIFY_EMAIL*`, +`TZF_DASHBOARD_URL`) entfallen — dieses Projekt hat keine Mail-Hooks. + +**`entrypoint.sh`** — legt den Superuser über `superuser create` an, bewusst +nicht `upsert`: Existiert der Account schon, scheitert `create` mit +„must be unique" und ein im Admin-UI geändertes Passwort überlebt jeden +Redeploy. Ohne gesetzte Variablen wird der Schritt übersprungen und PocketBase +gibt seinen Installer-Link im Log aus. Da PocketBase auch im Fehlerfall +Exitcode 0 liefert, wird die Ausgabe ausgewertet, nicht der Status. + +**`.gitignore`** — `pb_data/`, `*.db`, `.env`, `.env.*` (Ausnahme +`.env.example`). + +**`README.md`** — lokaler Start, Schema-Verwaltung, Persistenz-Hinweise, +Superuser-Verhalten, Update der PocketBase-Version. + +### 4. Schema-Migration erzeugen + +Aus dem Live-Export entsteht **eine** Snapshot-Migration +`backend/pb_migrations/_init_schema.js` nach dem PocketBase-Muster: + +```js +migrate((app) => { + app.importCollections(JSON.stringify([ /* Collections */ ]), false) +}, (app) => { /* down: Collections wieder entfernen */ }) +``` + +Enthalten sind die sechs fachlichen Collections `users, teams, riders, events, +runs, times` mit ihren Feldern, API-Rules und Indizes. System-Collections +(`_superusers`, `_mfas`, `_otps`, `_externalAuths`, `_authOrigins`) bleiben +außen vor — PocketBase legt sie selbst an. + +`importCollections` mit `deleteMissing = false`, damit die Migration nichts +löscht, was nicht im Snapshot steht. + +Ergebnis: Ein frisch gestarteter Container hat dasselbe Schema wie die +Live-Instanz, ohne deren Daten. + +Die Migration wird **nicht** gegen die Live-Instanz angewendet — sie beschreibt +nur den Zustand, den ein neuer Container herstellen soll. + +### 5. Altlasten entfernen + +Gelöscht werden (alle untracked, also unwiederbringlich): + +- `pocketbase_schema.json` — veraltet (siehe Befund oben), abgelöst durch die Migration +- `example_pb_schema.json` — Beispieldatei ohne Bezug zum Projekt +- `pocketbase_migrate.zip` — altes `pb_data` von 2023 inkl. Bilddaten, gehört nicht ins Repo + +`IMPLEMENTATION_SUMMARY.md`, `POCKETBASE_SCHEMA.md`, `README_SETUP.md` und +`UML_basic.png` bleiben zunächst im Root erhalten. + +### 6. Root-Dateien anpassen + +- **`.gitignore`** im Root: ergänzt um `frontend/node_modules/`, + `frontend/.svelte-kit/`, `frontend/build/`, `backend/pb_data/`, `.env`, + `.env.*` mit Ausnahme `.env.example`. Wichtig: `frontend/.env` enthält + `PB_SUPERUSER_TOKEN` und darf nicht in den Commit gelangen — das wird vor + dem Commit über `git status` geprüft. +- **`CLAUDE.md`**: Pfadangaben auf die neue Struktur umstellen + (Dev-Commands laufen aus `frontend/`), Abschnitt zum Backend ergänzen +- **`README.md`** im Root: kurze Übersicht beider Teile + +### 7. Erster Commit + +Das Repo hat noch keinen Commit. Die fertige Struktur wird als initialer Commit +angelegt — der Umbau erscheint damit nicht als Verschiebung in der Historie, +sondern die Struktur steht von Anfang an richtig da. + +Kein `git push` — der bleibt ausdrücklicher Anweisung vorbehalten. + +## Verifikation + +Der Umbau gilt als erfolgreich, wenn: + +1. `cd frontend && npm ci` fehlerfrei durchläuft +2. `cd frontend && npm run check` ohne neue Fehler durchläuft (Vergleich gegen + den Stand vor dem Umbau — bestehende Fehler zählen nicht als Regression) +3. `cd frontend && npm run dev` startet und die App unter + `http://stammtisch-hersbruck.de.localhost:31337` lädt und Daten von der + Remote-Instanz zeigt +4. `cd backend && docker compose up -d --build` startet und + `curl http://127.0.0.1:8090/api/health` antwortet +5. Im lokalen Admin-UI sind die sechs Collections mit ihren Feldern vorhanden + +Punkt 4 und 5 setzen ein lauffähiges Docker voraus; ist das nicht verfügbar, +wird das ausdrücklich als ungeprüft berichtet statt als erledigt. + +## Bewusst nicht Teil dieses Umbaus + +- Keine Änderung an der Live-Instanz oder deren Daten +- Keine Datenmigration ins lokale Backend +- Kein Deployment-Setup (Coolify o. ä.) +- Keine `pb_hooks` — das Verzeichnis wird leer angelegt +- Kein Ablösen von `scripts/migrate-schema.ts`; das Script bleibt vorerst + unverändert im Frontend diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..e7ed33a --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,19 @@ +# Vorlage für die lokale .env — kopieren und ausfüllen: +# cp .env.example .env +# +# Die echte .env steht in .gitignore und darf nicht committet werden, +# sie enthält den Superuser-Token. + +# Basis-URL der PocketBase-Instanz, die das Frontend anspricht. +# Produktiv: https://api.stammtisch-hersbruck.de +# Lokales Backend aus ../backend: http://127.0.0.1:8090 +PUBLIC_PB_URL=https://api.stammtisch-hersbruck.de + +# Ziel für die Typgenerierung (npm run generate-pocketbase-types) und die +# Scripts unter scripts/. Zeigt üblicherweise auf dieselbe Instanz wie oben. +PB_TYPEGEN_URL=https://api.stammtisch-hersbruck.de/ + +# Superuser-Token für Schema-Zugriffe. Im PocketBase-Admin-UI erzeugen. +# Niemals committen. +PB_SUPERUSER_TOKEN= +PB_TYPEGEN_TOKEN= diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..f45c6bb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,56 @@ +# stammtisch-hersbruck.de — Frontend + +SvelteKit-5-Anwendung (Svelte 5 Runes, Tailwind 4) für Zeitnahme und Verwaltung. + +## Einrichten + +`.env` aus der Vorlage anlegen und ausfüllen — `.env.example` beschreibt jede +Variable: + + cp .env.example .env + npm ci + npm run dev + +Die App läuft dann auf http://stammtisch-hersbruck.de.localhost:31337 + +## Befehle + +| Befehl | Zweck | +|---|---| +| `npm run dev` | Development-Server (Port 31337, strict) | +| `npm run build` | Produktions-Build | +| `npm run preview` | Produktions-Build lokal ansehen | +| `npm run check` | Typprüfung (svelte-check) | +| `npm run check:watch` | Typprüfung im Watch-Modus | +| `npm run generate-pocketbase-types` | `src/lib/types.d.ts` aus dem PocketBase-Schema erzeugen | + +## Welche PocketBase-Instanz? + +`PUBLIC_PB_URL` in `.env` entscheidet, wohin das Frontend spricht: + +| Wert | Instanz | +|---|---| +| `https://api.stammtisch-hersbruck.de` | produktiv (Default) | +| `http://127.0.0.1:8090` | lokales Backend aus `../backend` | + +Die Variable wird von SvelteKit zur **Buildzeit** eingesetzt +(`$env/static/public`). Ein Wechsel der Instanz erfordert deshalb einen +Neustart des Dev-Servers bzw. einen neuen Build — ein blosser Neustart des +Containers genügt nicht. + +`PB_TYPEGEN_URL` und `PB_SUPERUSER_TOKEN` gelten getrennt davon für die +Typgenerierung und die Scripts unter `scripts/`. + +## Scripts + +Die Dateien unter `scripts/` sind Einmal-Werkzeuge gegen eine PocketBase-Instanz +und laufen über `tsx`. Sie lesen ihre Zugangsdaten aus `.env`. + +> **Achtung, veralteter Stand:** `seed-test-data.ts` (`npm run seed`), +> `migrate-schema.ts` und `migrate-teams.ts` stammen aus einer früheren +> Schema-Generation. `seed-test-data.ts` schreibt in die Collections `stages` +> und `results`, die es im aktuellen Schema **nicht mehr gibt** — der Aufruf +> schlägt fehl. Vor einer Verwendung müssen die Scripts auf das aktuelle Schema +> (`users`, `teams`, `events`, `runs`, `riders`, `times`) angepasst werden. + +Das maßgebliche Schema liegt als Migration in `../backend/pb_migrations/`. diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..c5d91b4 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/app.css", + "baseColor": "slate" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..8b0a39b --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4900 @@ +{ + "name": "stammtisch-hersbruck.de", + "version": "2.0.0-alpha", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stammtisch-hersbruck.de", + "version": "2.0.0-alpha", + "dependencies": { + "date-fns": "^4.1.0", + "lucide-svelte": "^0.503.0", + "mode-watcher": "^1.1.0", + "pocketbase": "^0.26.0", + "tippy.js": "^6.3.7" + }, + "devDependencies": { + "@internationalized/date": "^3.12.1", + "@lucide/svelte": "^1.16.0", + "@sveltejs/adapter-auto": "^6.1.0", + "@sveltejs/kit": "^2.43.2", + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.18", + "@tailwindcss/vite": "^4.1.13", + "bits-ui": "^2.18.1", + "clsx": "^2.1.1", + "pocketbase-typegen": "^1.3.1", + "svelte": "^5.39.5", + "svelte-check": "^4.3.2", + "tailwind-merge": "^3.3.1", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.1.13", + "tsx": "^4.19.2", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.2", + "vite": "^7.1.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@internationalized/date": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.1.tgz", + "integrity": "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lucide/svelte": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.16.0.tgz", + "integrity": "sha512-AvvPJnaWxeiNkAljI5MsSEc84yHPLMaWQIAJOcbX7k9au/f9ITS7cxTTQiautDiOFKVOXiYdZ+d6mtl88J+Kbg==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "svelte": "^5" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "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/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", + "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.1.1.tgz", + "integrity": "sha512-cBNt4jgH4KuaNO5gRSB2CZKkGtz+OCZ8lPjRQGjhvVUD4akotnj2weUia6imLl2v07K3IgsQRyM36909miSwoQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.46.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.46.4.tgz", + "integrity": "sha512-J1fd80WokLzIm6EAV7z7C2+/C02qVAX645LZomARARTRJkbbJSY1Jln3wtBZYibUB8c9/5Z6xqLAV39VdbtWCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.3.2", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.1.tgz", + "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "magic-string": "^0.30.17", + "vitefu": "^1.1.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.1.tgz", + "integrity": "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", + "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", + "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.1", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", + "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.5.1" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", + "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", + "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", + "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", + "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", + "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", + "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", + "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", + "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", + "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", + "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.5", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", + "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", + "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.14.tgz", + "integrity": "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", + "tailwindcss": "4.1.14" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bits-ui": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.1.tgz", + "integrity": "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/dom": "^1.7.1", + "esm-env": "^1.1.2", + "runed": "^0.35.1", + "svelte-toolbelt": "^0.10.6", + "tabbable": "^6.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "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/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz", + "integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-flow": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dotenv-flow/-/dotenv-flow-4.1.0.tgz", + "integrity": "sha512-0cwP9jpQBQfyHwvE0cRhraZMkdV45TQedA8AAUZMsFzvmLcQyc1HPv+oX0OOYwLFjIlvgVepQ+WuQHbqDaHJZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dotenv": "^16.0.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", + "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "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-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC", + "optional": true + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "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", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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==", + "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", + "optional": true + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "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-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "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/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/lucide-svelte": { + "version": "0.503.0", + "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.503.0.tgz", + "integrity": "sha512-oT1idMbgJKCCLWI4U7bdUYUZqxS78fiTGJuKGZ5u0NJNyuiUErDu3G7sXkYpfDwwspeid7gZSjrlJbTINEfErw==", + "license": "ISC", + "peerDependencies": { + "svelte": "^3 || ^4 || ^5.0.0-next.42" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/mode-watcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz", + "integrity": "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==", + "license": "MIT", + "dependencies": { + "runed": "^0.25.0", + "svelte-toolbelt": "^0.7.1" + }, + "peerDependencies": { + "svelte": "^5.27.0" + } + }, + "node_modules/mode-watcher/node_modules/runed": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.25.0.tgz", + "integrity": "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/mode-watcher/node_modules/svelte-toolbelt": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz", + "integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==", + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.23.2", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/mode-watcher/node_modules/svelte-toolbelt/node_modules/runed": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz", + "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "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.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.78.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", + "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-gyp/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/node-gyp/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pocketbase": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.26.2.tgz", + "integrity": "sha512-WA8EOBc3QnSJh8rJ3iYoi9DmmPOMFIgVfAmIGux7wwruUEIzXgvrO4u0W2htfQjGIcyezJkdZOy5Xmh7SxAftw==", + "license": "MIT" + }, + "node_modules/pocketbase-typegen": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pocketbase-typegen/-/pocketbase-typegen-1.3.1.tgz", + "integrity": "sha512-nwPkcZU/EFDdGfVJWiaEKstkHWFXvf8RrmE36xANCXPwDo4OI5OS70CN3N/Ruw1CVx87EaGCbquVs0yRD3IqpQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "^9.4.1", + "cross-fetch": "^3.1.5", + "dotenv-flow": "^4.1.0", + "form-data": "^4.0.1", + "sqlite": "^4.1.2", + "sqlite3": "^5.1.4" + }, + "bin": { + "pocketbase-typegen": "dist/index.js" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/runed": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", + "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + } + } + }, + "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/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "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/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sqlite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-4.2.1.tgz", + "integrity": "sha512-Tll0Ndvnwkuv5Hn6WIbh26rZiYQORuH1t5m/or9LUpSmDmmyFG89G9fKrSeugMPxwmEIXoVxqTun4LbizTs4uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sqlite3/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/sqlite3/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/sqlite3/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sqlite3/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sqlite3/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sqlite3/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ssri/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-to-object": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.11.tgz", + "integrity": "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/svelte": { + "version": "5.39.11", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.11.tgz", + "integrity": "sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^2.1.0", + "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.3.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.3.tgz", + "integrity": "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "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" + } + }, + "node_modules/svelte-toolbelt": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", + "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.35.1", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", + "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwind-merge": ">=3.0.0", + "tailwindcss": "*" + }, + "peerDependenciesMeta": { + "tailwind-merge": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, + "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/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "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": "7.1.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", + "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "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-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.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", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9a0e799 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,47 @@ +{ + "name": "stammtisch-hersbruck.de", + "private": true, + "version": "2.0.0-alpha", + "type": "module", + "scripts": { + "dev": "vite dev --mode development", + "build": "vite build", + "preview": "vite preview", + "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", + "generate-pocketbase-types": "set -a && . ./.env && set +a && pocketbase-typegen --url \"$PB_TYPEGEN_URL\" --token \"$PB_SUPERUSER_TOKEN\" --out ./src/lib/types.d.ts", + "seed": "tsx scripts/seed-test-data.ts", + "migrate": "tsx scripts/migrate-schema.ts", + "migrate-teams": "tsx scripts/migrate-teams.ts" + }, + "devDependencies": { + "@internationalized/date": "^3.12.1", + "@lucide/svelte": "^1.16.0", + "@sveltejs/adapter-auto": "^6.1.0", + "@sveltejs/kit": "^2.43.2", + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.18", + "@tailwindcss/vite": "^4.1.13", + "bits-ui": "^2.18.1", + "clsx": "^2.1.1", + "pocketbase-typegen": "^1.3.1", + "svelte": "^5.39.5", + "svelte-check": "^4.3.2", + "tailwind-merge": "^3.3.1", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.1.13", + "tsx": "^4.19.2", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.2", + "vite": "^7.1.7" + }, + "dependencies": { + "date-fns": "^4.1.0", + "lucide-svelte": "^0.503.0", + "mode-watcher": "^1.1.0", + "pocketbase": "^0.26.0", + "tippy.js": "^6.3.7" + } +} diff --git a/frontend/scripts/migrate-schema.ts b/frontend/scripts/migrate-schema.ts new file mode 100644 index 0000000..4ea53ec --- /dev/null +++ b/frontend/scripts/migrate-schema.ts @@ -0,0 +1,245 @@ +/** + * Schema-Migration für PocketBase + * + * Erweitert das vorhandene Schema und migriert bestehende Daten: + * - events: + name (Text), + status (Select draft/active/finished) + * Daten: description → name kopieren, status = 'draft' + * - riders: + number (Text), + firstname (Text), + lastname (Text) + * Daten: bestehender name-Wert → number kopieren + * - times: + correction (Number, Sekunden) + * status erweitern: + dnf, dns, dsq + * + * Aufruf: npx tsx scripts/migrate-schema.ts + */ + +import { readFileSync } from 'node:fs' + +// .env minimal selbst parsen +for (const line of readFileSync('.env', 'utf8').split('\n')) { + const m = line.match(/^([A-Z_]+)=(.*)$/) + if (m && !process.env[m[1]]) process.env[m[1]] = m[2] +} + +const API = process.env.PB_TYPEGEN_URL?.replace(/\/$/, '') ?? 'https://api.stammtisch-hersbruck.de' +const TOKEN = process.env.PB_SUPERUSER_TOKEN + +if (!TOKEN) { + console.error('PB_SUPERUSER_TOKEN fehlt in .env') + process.exit(1) +} + +const headers = { + 'Authorization': TOKEN, + 'Content-Type': 'application/json', +} + +async function req(method: string, path: string, body?: unknown) { + const res = await fetch(`${API}${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }) + if (!res.ok) { + const text = await res.text() + throw new Error(`${method} ${path} → ${res.status}: ${text}`) + } + return res.status === 204 ? null : await res.json() +} + +async function getCollection(name: string) { + return req('GET', `/api/collections/${name}`) +} + +async function updateCollection(name: string, body: any) { + return req('PATCH', `/api/collections/${name}`, body) +} + +async function listRecords(collection: string) { + const all: any[] = [] + let page = 1 + while (true) { + const res: any = await req('GET', `/api/collections/${collection}/records?page=${page}&perPage=200`) + all.push(...res.items) + if (page >= res.totalPages || res.totalPages === 0) break + page++ + } + return all +} + +async function updateRecord(collection: string, id: string, data: any) { + return req('PATCH', `/api/collections/${collection}/records/${id}`, data) +} + +function hasField(coll: any, name: string) { + return coll.fields.some((f: any) => f.name === name) +} + +// ---- Migration ---- + +async function migrateEvents() { + console.log('\n== events ==') + const coll = await getCollection('events') + const fields = [...coll.fields] + + if (!hasField(coll, 'name')) { + fields.push({ + name: 'name', + type: 'text', + required: false, + presentable: true, + max: 0, + min: 0, + pattern: '', + autogeneratePattern: '', + }) + console.log(' + Feld "name" (text)') + } else { + console.log(' = Feld "name" existiert bereits') + } + + if (!hasField(coll, 'status')) { + fields.push({ + name: 'status', + type: 'select', + required: false, + presentable: false, + maxSelect: 1, + values: ['draft', 'active', 'finished'], + }) + console.log(' + Feld "status" (select draft/active/finished)') + } else { + console.log(' = Feld "status" existiert bereits') + } + + await updateCollection('events', { fields }) + + // Daten migrieren + const records = await listRecords('events') + for (const r of records) { + const patch: any = {} + if (!r.name && r.description) patch.name = r.description + if (!r.status) patch.status = 'draft' + if (Object.keys(patch).length) { + await updateRecord('events', r.id, patch) + console.log(` → ${r.id}: ${JSON.stringify(patch)}`) + } + } +} + +async function migrateRiders() { + console.log('\n== riders ==') + const coll = await getCollection('riders') + const fields = [...coll.fields] + + if (!hasField(coll, 'number')) { + fields.push({ + name: 'number', + type: 'text', + required: false, + presentable: true, + max: 0, + min: 0, + pattern: '', + autogeneratePattern: '', + }) + console.log(' + Feld "number" (text)') + } else { + console.log(' = Feld "number" existiert bereits') + } + + if (!hasField(coll, 'firstname')) { + fields.push({ + name: 'firstname', + type: 'text', + required: false, + presentable: true, + max: 0, + min: 0, + pattern: '', + autogeneratePattern: '', + }) + console.log(' + Feld "firstname" (text)') + } else { + console.log(' = Feld "firstname" existiert bereits') + } + + if (!hasField(coll, 'lastname')) { + fields.push({ + name: 'lastname', + type: 'text', + required: false, + presentable: true, + max: 0, + min: 0, + pattern: '', + autogeneratePattern: '', + }) + console.log(' + Feld "lastname" (text)') + } else { + console.log(' = Feld "lastname" existiert bereits') + } + + await updateCollection('riders', { fields }) + + // Daten migrieren: name → number, falls number leer + const records = await listRecords('riders') + for (const r of records) { + if (!r.number && r.name) { + await updateRecord('riders', r.id, { number: r.name }) + console.log(` → ${r.id}: number = "${r.name}"`) + } + } +} + +async function migrateTimes() { + console.log('\n== times ==') + const coll = await getCollection('times') + const fields = [...coll.fields] + let changed = false + + if (!hasField(coll, 'correction')) { + fields.push({ + name: 'correction', + type: 'number', + required: false, + presentable: false, + min: null, + max: null, + onlyInt: false, + }) + console.log(' + Feld "correction" (number)') + changed = true + } else { + console.log(' = Feld "correction" existiert bereits') + } + + // status um dnf, dns, dsq erweitern + const statusField = fields.find((f: any) => f.name === 'status') + if (statusField) { + const current: string[] = statusField.values || [] + const target = ['active', 'finished', 'dnf', 'dns', 'dsq'] + const missing = target.filter((v) => !current.includes(v)) + if (missing.length) { + statusField.values = [...current, ...missing] + console.log(` ~ Feld "status" erweitert um: ${missing.join(', ')}`) + changed = true + } else { + console.log(' = Feld "status" hat bereits alle Werte') + } + } + + if (changed) await updateCollection('times', { fields }) +} + +async function main() { + console.log(`Migration gegen ${API}`) + await migrateEvents() + await migrateRiders() + await migrateTimes() + console.log('\n✓ Migration fertig') +} + +main().catch((e) => { + console.error('\n✗ Fehler:', e.message) + process.exit(1) +}) diff --git a/frontend/scripts/migrate-teams.ts b/frontend/scripts/migrate-teams.ts new file mode 100644 index 0000000..610e43c --- /dev/null +++ b/frontend/scripts/migrate-teams.ts @@ -0,0 +1,329 @@ +/** + * Schema-Migration: Teams einführen + * + * - neue Collection 'teams' (name, owner, admins[], users[]) + * - events, riders, runs, times bekommen ein 'team'-Feld + * - listRule/viewRule auf allen vier wird auf team.users.id ?= @request.auth.id umgestellt + * - events.users wird entfernt + * - Migration der Daten: + * Team "Stammtisch Hersbruck" anlegen + * Owner = aktuell eingeloggter User (über .env: PB_OWNER_EMAIL falls gesetzt, sonst erster events.users) + * users = Union aller events.users + * events/riders/runs/times werden alle diesem Team zugeordnet + * + * Aufruf: npm run migrate-teams + */ + +import { readFileSync } from 'node:fs' + +// .env parsen — Zeilen die mit . anfangen werden an die vorherige Variable angehängt +// (Workaround für zerbrochene JWT-Tokens über mehrere Zeilen) +const env = readFileSync('.env', 'utf8').split('\n') +let lastKey = '' +for (const line of env) { + const m = line.match(/^([A-Z_]+)=(.*)$/) + if (m) { + process.env[m[1]] = m[2] + lastKey = m[1] + } else if (lastKey && line.length > 0) { + process.env[lastKey] = (process.env[lastKey] ?? '') + line + } +} + +const API = process.env.PB_TYPEGEN_URL?.replace(/\/$/, '') ?? 'https://api.stammtisch-hersbruck.de' +const TOKEN = process.env.PB_SUPERUSER_TOKEN + +if (!TOKEN) { + console.error('PB_SUPERUSER_TOKEN fehlt') + process.exit(1) +} + +const headers = { Authorization: TOKEN, 'Content-Type': 'application/json' } + +async function req(method: string, path: string, body?: unknown) { + const res = await fetch(`${API}${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }) + if (!res.ok) throw new Error(`${method} ${path} → ${res.status}: ${await res.text()}`) + return res.status === 204 ? null : await res.json() +} + +async function getCollection(name: string) { + try { + return await req('GET', `/api/collections/${name}`) + } catch { + return null + } +} + +async function createCollection(body: any) { + return req('POST', '/api/collections', body) +} + +async function updateCollection(name: string, body: any) { + return req('PATCH', `/api/collections/${name}`, body) +} + +async function listAll(collection: string) { + const all: any[] = [] + let page = 1 + while (true) { + const res: any = await req('GET', `/api/collections/${collection}/records?page=${page}&perPage=200`) + all.push(...res.items) + if (page >= res.totalPages || res.totalPages === 0) break + page++ + } + return all +} + +async function updateRecord(collection: string, id: string, data: any) { + return req('PATCH', `/api/collections/${collection}/records/${id}`, data) +} + +async function createRecord(collection: string, data: any) { + return req('POST', `/api/collections/${collection}/records`, data) +} + +function hasField(coll: any, name: string) { + return coll.fields.some((f: any) => f.name === name) +} + +const USERS_AUTH = '_pb_users_auth_' + +// ---- Schritt 1: teams Collection anlegen ---- + +async function ensureTeamsCollection() { + console.log('\n== teams Collection ==') + const existing = await getCollection('teams') + if (existing) { + console.log(' = teams existiert bereits') + return existing + } + + const body = { + name: 'teams', + type: 'base', + listRule: 'users.id ?= @request.auth.id', + viewRule: 'users.id ?= @request.auth.id', + createRule: '@request.auth.id != ""', + updateRule: 'owner.id ?= @request.auth.id || admins.id ?= @request.auth.id', + deleteRule: 'owner.id ?= @request.auth.id', + fields: [ + { + name: 'name', + type: 'text', + required: true, + presentable: true, + max: 100, + min: 1, + pattern: '', + autogeneratePattern: '', + }, + { + name: 'owner', + type: 'relation', + required: true, + maxSelect: 1, + minSelect: 1, + collectionId: USERS_AUTH, + cascadeDelete: false, + }, + { + name: 'admins', + type: 'relation', + required: false, + maxSelect: 999, + minSelect: 0, + collectionId: USERS_AUTH, + cascadeDelete: false, + }, + { + name: 'users', + type: 'relation', + required: false, + maxSelect: 999, + minSelect: 0, + collectionId: USERS_AUTH, + cascadeDelete: false, + }, + { + name: 'created', + type: 'autodate', + onCreate: true, + onUpdate: false, + }, + { + name: 'updated', + type: 'autodate', + onCreate: true, + onUpdate: true, + }, + ], + } + + const created = await createCollection(body) + console.log(' + Collection "teams" angelegt') + return created +} + +// ---- Schritt 2: team-Feld zu events/riders/runs/times hinzufügen ---- + +async function addTeamField(collName: string) { + const coll = await getCollection(collName) + if (!coll) throw new Error(`Collection ${collName} fehlt`) + + if (hasField(coll, 'team')) { + console.log(` = ${collName}.team existiert bereits`) + return + } + + const fields = [ + ...coll.fields, + { + name: 'team', + type: 'relation', + required: false, + maxSelect: 1, + minSelect: 0, + collectionId: (await getCollection('teams'))!.id, + cascadeDelete: false, + }, + ] + + await updateCollection(collName, { fields }) + console.log(` + ${collName}.team hinzugefügt`) +} + +// ---- Schritt 3: Default-Team anlegen + alle Records zuordnen ---- + +async function migrateData(teamsId: string) { + console.log('\n== Daten-Migration ==') + + // Owner-User suchen + const ownerEmail = process.env.PB_OWNER_EMAIL || 'stammtisch-hersbruck@dne.name' + const usersRes: any = await req( + 'GET', + `/api/collections/${USERS_AUTH}/records?filter=` + encodeURIComponent(`email = "${ownerEmail}"`), + ) + if (!usersRes.items.length) { + throw new Error(`User mit E-Mail "${ownerEmail}" nicht gefunden. Setze PB_OWNER_EMAIL in .env.`) + } + const owner = usersRes.items[0] + console.log(` Owner: ${owner.email} (${owner.id})`) + + // Existierendes Team suchen + const teamsList: any = await req('GET', `/api/collections/teams/records?filter=` + encodeURIComponent(`name = "Stammtisch Hersbruck"`)) + let team: any = teamsList.items[0] + if (!team) { + // alle User aus events.users sammeln + const events = await listAll('events') + const userSet = new Set([owner.id]) + for (const e of events) { + if (Array.isArray(e.users)) for (const u of e.users) userSet.add(u) + } + team = await createRecord('teams', { + name: 'Stammtisch Hersbruck', + owner: owner.id, + admins: [], + users: [...userSet], + }) + console.log(` + Team "Stammtisch Hersbruck" mit ${userSet.size} Mitgliedern angelegt`) + } else { + console.log(` = Team "Stammtisch Hersbruck" existiert (${team.id})`) + } + + // Datensätze ohne team patchen + for (const collName of ['events', 'riders', 'runs', 'times']) { + const records = await listAll(collName) + let touched = 0 + for (const r of records) { + if (!r.team) { + await updateRecord(collName, r.id, { team: team.id }) + touched++ + } + } + console.log(` → ${collName}: ${touched} Records zugeordnet (${records.length - touched} bereits gesetzt)`) + } + + return team +} + +// ---- Schritt 4: Rules auf team-basiert umstellen ---- + +async function updateRules() { + console.log('\n== Rules auf team-basiert umstellen ==') + + const rules = { + events: { + listRule: 'team.users.id ?= @request.auth.id', + viewRule: 'team.users.id ?= @request.auth.id', + createRule: 'team.users.id ?= @request.auth.id', + updateRule: 'team.users.id ?= @request.auth.id', + deleteRule: 'team.users.id ?= @request.auth.id', + }, + riders: { + listRule: 'team.users.id ?= @request.auth.id', + viewRule: 'team.users.id ?= @request.auth.id', + createRule: 'team.users.id ?= @request.auth.id', + updateRule: 'team.users.id ?= @request.auth.id', + deleteRule: 'team.users.id ?= @request.auth.id', + }, + runs: { + listRule: 'team.users.id ?= @request.auth.id', + viewRule: 'team.users.id ?= @request.auth.id', + createRule: 'team.users.id ?= @request.auth.id', + updateRule: 'team.users.id ?= @request.auth.id', + deleteRule: 'team.users.id ?= @request.auth.id', + }, + times: { + listRule: 'team.users.id ?= @request.auth.id', + viewRule: 'team.users.id ?= @request.auth.id', + createRule: 'team.users.id ?= @request.auth.id', + updateRule: 'team.users.id ?= @request.auth.id', + deleteRule: 'team.users.id ?= @request.auth.id', + }, + } + + for (const [coll, r] of Object.entries(rules)) { + await updateCollection(coll, r) + console.log(` ~ ${coll} Rules aktualisiert`) + } +} + +// ---- Schritt 5: events.users entfernen ---- + +async function removeEventsUsers() { + console.log('\n== events.users entfernen ==') + const coll = await getCollection('events') + if (!coll) return + if (!hasField(coll, 'users')) { + console.log(' = events.users existiert nicht (mehr)') + return + } + const fields = coll.fields.filter((f: any) => f.name !== 'users') + await updateCollection('events', { fields }) + console.log(' - events.users entfernt') +} + +// ---- Main ---- + +async function main() { + console.log(`Migration gegen ${API}`) + const teams = await ensureTeamsCollection() + console.log('\n== team-Feld zu Collections hinzufügen ==') + await addTeamField('events') + await addTeamField('riders') + await addTeamField('runs') + await addTeamField('times') + await migrateData(teams.id) + await updateRules() + await removeEventsUsers() + console.log('\n✓ Teams-Migration fertig') +} + +main().catch((e) => { + console.error('\n✗ Fehler:', e.message) + process.exit(1) +}) diff --git a/frontend/scripts/seed-test-data.ts b/frontend/scripts/seed-test-data.ts new file mode 100644 index 0000000..87c129d --- /dev/null +++ b/frontend/scripts/seed-test-data.ts @@ -0,0 +1,329 @@ +/** + * Test Data Seeding Script for PocketBase + * + * This script creates initial test data for the timing app: + * - Admin user + * - Timekeeper users + * - Test riders + * - Sample event with stages + * + * Usage: + * npx tsx scripts/seed-test-data.ts + * + * Environment Variables (from .env): + * PB_TYPEGEN_URL - PocketBase API URL + * PB_TYPEGEN_TOKEN - Authentication token for impersonation + */ + +import PocketBase from 'pocketbase' +import type { TypedPocketBase } from '../src/lib/types.js' + +// Validate required environment variables +const PB_URL = process.env.PB_TYPEGEN_URL +const PB_TOKEN = process.env.PB_TYPEGEN_TOKEN + +if (!PB_URL) { + console.error('❌ Error: PB_TYPEGEN_URL is not set in .env file') + console.error(' Please set PB_TYPEGEN_URL in your .env file') + process.exit(1) +} + +if (!PB_TOKEN) { + console.error('❌ Error: PB_TYPEGEN_TOKEN is not set in .env file') + console.error(' Please set PB_TYPEGEN_TOKEN in your .env file') + process.exit(1) +} + +// Initialize PocketBase with URL from .env +const pb = new PocketBase(PB_URL) as TypedPocketBase + +// Authenticate using token from .env (impersonation) +pb.authStore.save(PB_TOKEN) + +console.log(`🔗 Connecting to PocketBase at: ${PB_URL}`) +console.log(`🔑 Using authentication token from .env\n`) + +// Configuration +const ADMIN_EMAIL = 'admin@stammtisch-hersbruck.de' +const ADMIN_PASSWORD = 'Admin123!secure' + +async function seedData() { + console.log('🌱 Starting test data seeding...\n') + + try { + // 1. Create Admin User + console.log('👤 Creating admin user...') + let adminUser + try { + adminUser = await pb.collection('users').create({ + email: ADMIN_EMAIL, + password: ADMIN_PASSWORD, + passwordConfirm: ADMIN_PASSWORD, + name: 'Administrator', + role: 'admin', + verified: true + }) + console.log(`✅ Admin created: ${adminUser.email}`) + } catch (e: any) { + if (e.message.includes('already exists')) { + console.log('⚠️ Admin user already exists, logging in...') + await pb.collection('users').authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD) + adminUser = pb.authStore.record + } else { + throw e + } + } + + // 2. Create Timekeeper Users + console.log('\n⏱️ Creating timekeeper users...') + const timekeepers = [] + const timekeeperData = [ + { email: 'timer1@stammtisch-hersbruck.de', name: 'Max Zeitnehmer', password: 'Timer123!' }, + { email: 'timer2@stammtisch-hersbruck.de', name: 'Anna Stoppuhr', password: 'Timer123!' }, + { email: 'timer3@stammtisch-hersbruck.de', name: 'Tom Chronograph', password: 'Timer123!' } + ] + + for (const data of timekeeperData) { + try { + const user = await pb.collection('users').create({ + email: data.email, + password: data.password, + passwordConfirm: data.password, + name: data.name, + role: 'timekeeper', + verified: true + }) + timekeepers.push(user) + console.log(`✅ Timekeeper created: ${user.name} (${user.email})`) + } catch (e: any) { + if (e.message.includes('already exists')) { + console.log(`⚠️ Timekeeper ${data.email} already exists, skipping...`) + // Fetch existing user + const existing = await pb.collection('users').getFirstListItem(`email="${data.email}"`) + timekeepers.push(existing) + } else { + console.error(`❌ Error creating ${data.email}:`, e.message) + } + } + } + + // 3. Create Test Riders + console.log('\n🚴 Creating test riders...') + const riders = [] + const riderData = [ + { number: '1', name: 'Max Müller', email: 'max.mueller@example.com', birthday: '1995-03-15' }, + { number: '2', name: 'Anna Schmidt', email: 'anna.schmidt@example.com', birthday: '1992-07-22' }, + { number: '3', name: 'Tom Weber', email: 'tom.weber@example.com', birthday: '1998-11-08' }, + { number: '17', name: 'Lisa Hoffmann', email: 'lisa.hoffmann@example.com', birthday: '2000-01-30' }, + { number: '23', name: 'Felix Wagner', email: 'felix.wagner@example.com', birthday: '1996-05-12' }, + { number: '42', name: 'Sarah Becker', email: 'sarah.becker@example.com', birthday: '1994-09-25' }, + { number: '99', name: 'Lukas Fischer', email: 'lukas.fischer@example.com', birthday: '1999-12-03' }, + { number: '101', name: 'Julia Schulz', email: 'julia.schulz@example.com', birthday: '1997-04-18' } + ] + + for (const data of riderData) { + try { + const rider = await pb.collection('riders').create({ + number: data.number, + name: data.name, + email: data.email, + birthday: data.birthday + }) + riders.push(rider) + console.log(`✅ Rider created: #${rider.number} ${rider.name}`) + } catch (e: any) { + if (e.message.includes('already exists')) { + console.log(`⚠️ Rider #${data.number} already exists, skipping...`) + const existing = await pb.collection('riders').getFirstListItem(`number="${data.number}"`) + riders.push(existing) + } else { + console.error(`❌ Error creating rider #${data.number}:`, e.message) + } + } + } + + // 4. Create Test Event + console.log('\n🏁 Creating test event...') + let event + try { + // Event date: 2 weeks from now + const eventDate = new Date() + eventDate.setDate(eventDate.getDate() + 14) + + const startTime = new Date(eventDate) + startTime.setHours(9, 0, 0, 0) + + const endTime = new Date(eventDate) + endTime.setHours(17, 0, 0, 0) + + event = await pb.collection('events').create({ + name: 'MTB Hersbruck Cup 2025', + location: 'Hersbruck, Deutschland', + start: startTime.toISOString(), + end: endTime.toISOString(), + status: 'draft', + created_by: adminUser?.id, + timekeepers: timekeepers.map(t => t.id) + }) + console.log(`✅ Event created: ${event.name}`) + console.log(` Date: ${new Date(event.start).toLocaleDateString('de-DE')}`) + console.log(` Timekeepers: ${timekeepers.length} assigned`) + } catch (e: any) { + if (e.message.includes('already exists')) { + console.log('⚠️ Event already exists, fetching...') + event = await pb.collection('events').getFirstListItem('name~"MTB Hersbruck"') + } else { + throw e + } + } + + // 5. Create Stages + console.log('\n🏔️ Creating stages...') + const stages = [] + const stageData = [ + { + name: 'Downhill', + location: 'Steinberg Trail', + order: 1, + start_number: 1, + end_number: 50, + status: 'pending' + }, + { + name: 'Cross-Country', + location: 'Wald-Rundkurs', + order: 2, + start_number: 51, + end_number: 100, + status: 'pending' + }, + { + name: 'Enduro Stage 1', + location: 'Berg-Passage', + order: 3, + start_number: 101, + end_number: 150, + status: 'pending' + } + ] + + for (const data of stageData) { + try { + // Stage times based on event date + const stageStart = new Date(event.start) + stageStart.setHours(stageStart.getHours() + (data.order - 1) * 2) + + const stageEnd = new Date(stageStart) + stageEnd.setHours(stageEnd.getHours() + 2) + + const stage = await pb.collection('stages').create({ + event: event.id, + name: data.name, + location: data.location, + start: stageStart.toISOString(), + end: stageEnd.toISOString(), + order: data.order, + start_number: data.start_number, + end_number: data.end_number, + status: data.status + }) + stages.push(stage) + console.log(`✅ Stage created: ${stage.name} (Startnummern: ${data.start_number}-${data.end_number})`) + } catch (e: any) { + console.error(`❌ Error creating stage ${data.name}:`, e.message) + } + } + + // 6. Create Sample Results (optional - for demo) + console.log('\n⏱️ Creating sample results...') + + // Create 2 finished results for demonstration + if (stages.length > 0 && riders.length >= 3 && timekeepers.length > 0) { + const firstStage = stages[0] + const sampleResults = [ + { + rider: riders[0], + duration: 185.5, // 3:05.5 + status: 'finished' + }, + { + rider: riders[1], + duration: 192.3, // 3:12.3 + status: 'finished' + }, + { + rider: riders[2], + duration: 0, // currently running + status: 'running' + } + ] + + for (const data of sampleResults) { + try { + const now = new Date() + const startTime = new Date(now.getTime() - (data.duration * 1000) - 60000) // Started some time ago + const endTime = data.status === 'finished' ? new Date(startTime.getTime() + data.duration * 1000) : undefined + + const result = await pb.collection('results').create({ + stage: firstStage.id, + rider: data.rider.id, + start: startTime.toISOString(), + end: endTime?.toISOString() || null, + status: data.status, + started_by: timekeepers[0].id, + stopped_by: data.status === 'finished' ? timekeepers[0].id : null, + correction: 0 + }) + + const timeStr = data.status === 'finished' + ? `${Math.floor(data.duration / 60)}:${(data.duration % 60).toFixed(1).padStart(4, '0')}` + : 'RUNNING' + console.log(`✅ Result created: #${data.rider.number} ${data.rider.name} - ${timeStr}`) + } catch (e: any) { + console.error(`❌ Error creating result:`, e.message) + } + } + } + + // Summary + console.log('\n' + '='.repeat(50)) + console.log('✅ Test data seeding completed!') + console.log('='.repeat(50)) + console.log('\n📊 Summary:') + console.log(` Users: ${1 + timekeepers.length} (1 admin, ${timekeepers.length} timekeepers)`) + console.log(` Riders: ${riders.length}`) + console.log(` Events: 1`) + console.log(` Stages: ${stages.length}`) + console.log(` Results: 3 (2 finished, 1 running)`) + + console.log('\n🔑 Login Credentials:') + console.log(` Admin: ${ADMIN_EMAIL} / ${ADMIN_PASSWORD}`) + console.log(` Timekeeper: timer1@stammtisch-hersbruck.de / Timer123!`) + console.log(` Timekeeper: timer2@stammtisch-hersbruck.de / Timer123!`) + console.log(` Timekeeper: timer3@stammtisch-hersbruck.de / Timer123!`) + + console.log('\n🚀 Next steps:') + console.log(' 1. Run: npm run generate-pocketbase-types') + console.log(' 2. Start development: npm run dev') + console.log(' 3. Login with one of the credentials above') + console.log('') + + } catch (error: any) { + console.error('\n❌ Error during seeding:', error) + if (error.response) { + console.error('Response:', error.response) + } + process.exit(1) + } +} + +// Run the seeding +seedData() + .then(() => { + console.log('✅ Seeding completed successfully!') + process.exit(0) + }) + .catch((error) => { + console.error('❌ Seeding failed:', error) + process.exit(1) + }) \ No newline at end of file diff --git a/frontend/scripts/test-token.ts b/frontend/scripts/test-token.ts new file mode 100644 index 0000000..9eb51f2 --- /dev/null +++ b/frontend/scripts/test-token.ts @@ -0,0 +1,54 @@ +/** + * Test-Script: prüft was der PB_SUPERUSER_TOKEN tatsächlich darf. + * Aufruf: npx tsx scripts/test-token.ts + */ +import { readFileSync } from 'node:fs' + +const env = readFileSync('.env', 'utf8').split('\n') +let lastKey = '' +for (const line of env) { + const m = line.match(/^([A-Z_]+)=(.*)$/) + if (m) { + process.env[m[1]] = m[2] + lastKey = m[1] + } else if (lastKey && line.length > 0) { + process.env[lastKey] = (process.env[lastKey] ?? '') + line + } +} + +const TOKEN = process.env.PB_SUPERUSER_TOKEN ?? '' +const API = 'https://api.stammtisch-hersbruck.de' + +// JWT-Payload dekodieren +const [, payloadB64] = TOKEN.split('.') +if (payloadB64) { + const payload = JSON.parse(Buffer.from(payloadB64, 'base64').toString('utf8')) + console.log('Token-Payload:', payload) + console.log('Token-Länge:', TOKEN.length) +} + +async function tryEndpoint(label: string, path: string, init?: RequestInit) { + const res = await fetch(`${API}${path}`, { + ...init, + headers: { Authorization: TOKEN, 'Content-Type': 'application/json', ...(init?.headers ?? {}) }, + }) + const text = await res.text() + console.log(`\n${label}: ${res.status}`) + try { + console.log(JSON.stringify(JSON.parse(text), null, 2).slice(0, 200)) + } catch { + console.log(text.slice(0, 200)) + } +} + +await tryEndpoint('GET /api/collections', '/api/collections?perPage=1') +await tryEndpoint('GET /api/collections/_superusers', '/api/collections/_superusers') +await tryEndpoint('GET /api/collections/events', '/api/collections/events') +await tryEndpoint('POST /api/collections (Probe)', '/api/collections', { + method: 'POST', + body: JSON.stringify({ + name: '__probe_temp__', + type: 'base', + fields: [{ name: 'foo', type: 'text' }], + }), +}) diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..0f3baa7 --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,121 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +/* shadcn theme: yellow (oklch) */ +:root { + --radius: 0.65rem; + --background: oklch(1 0 0); + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.795 0.184 86.047); + --primary-foreground: oklch(0.421 0.095 57.708); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.795 0.184 86.047); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.795 0.184 86.047); + --sidebar-primary-foreground: oklch(0.421 0.095 57.708); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.795 0.184 86.047); +} + +.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.852 0.199 91.936); + --primary-foreground: oklch(0.421 0.095 57.708); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.554 0.135 66.442); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.852 0.199 91.936); + --sidebar-primary-foreground: oklch(0.421 0.095 57.708); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.554 0.135 66.442); +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/frontend/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/lib/assets/favicon.svg b/frontend/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/frontend/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/frontend/src/lib/components/ConfirmDialog.svelte b/frontend/src/lib/components/ConfirmDialog.svelte new file mode 100644 index 0000000..28139fc --- /dev/null +++ b/frontend/src/lib/components/ConfirmDialog.svelte @@ -0,0 +1,36 @@ + + + open, onOpenChange}> + + + {app.confirm.title} + {#if app.confirm.text} + {app.confirm.text} + {/if} + + + {#if app.confirm.error} +
+ {typeof app.confirm.error === 'object' ? (app.confirm.error as any).message ?? 'Fehler' : 'Fehler'} +
+ {/if} + + + + + +
+
diff --git a/frontend/src/lib/components/Logo.svelte b/frontend/src/lib/components/Logo.svelte new file mode 100644 index 0000000..b133633 --- /dev/null +++ b/frontend/src/lib/components/Logo.svelte @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/frontend/src/lib/components/TeamSwitcher.svelte b/frontend/src/lib/components/TeamSwitcher.svelte new file mode 100644 index 0000000..bbff610 --- /dev/null +++ b/frontend/src/lib/components/TeamSwitcher.svelte @@ -0,0 +1,43 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + Teams wechseln + {#each teams.records as t} + {@const isMe = teams.isOwner(t) || teams.isAdmin(t)} + teams.setActive(t.id)} + class={t.id === teams.activeId ? 'bg-accent' : ''} + > + {t.name} + {#if teams.isOwner(t)} + Owner + {:else if teams.isAdmin(t)} + Admin + {/if} + + {/each} + + goto('/dashboard/teams')}> + + Teams verwalten + + + diff --git a/frontend/src/lib/components/ui/accordion/accordion-content.svelte b/frontend/src/lib/components/ui/accordion/accordion-content.svelte new file mode 100644 index 0000000..cf7045e --- /dev/null +++ b/frontend/src/lib/components/ui/accordion/accordion-content.svelte @@ -0,0 +1,22 @@ + + + +
+ {@render children?.()} +
+
diff --git a/frontend/src/lib/components/ui/accordion/accordion-item.svelte b/frontend/src/lib/components/ui/accordion/accordion-item.svelte new file mode 100644 index 0000000..780545c --- /dev/null +++ b/frontend/src/lib/components/ui/accordion/accordion-item.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte b/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte new file mode 100644 index 0000000..0027cdd --- /dev/null +++ b/frontend/src/lib/components/ui/accordion/accordion-trigger.svelte @@ -0,0 +1,32 @@ + + + + svg]:rotate-180", + className + )} + {...restProps} + > + {@render children?.()} + + + diff --git a/frontend/src/lib/components/ui/accordion/accordion.svelte b/frontend/src/lib/components/ui/accordion/accordion.svelte new file mode 100644 index 0000000..117ee37 --- /dev/null +++ b/frontend/src/lib/components/ui/accordion/accordion.svelte @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/lib/components/ui/accordion/index.ts b/frontend/src/lib/components/ui/accordion/index.ts new file mode 100644 index 0000000..ac343a1 --- /dev/null +++ b/frontend/src/lib/components/ui/accordion/index.ts @@ -0,0 +1,16 @@ +import Root from "./accordion.svelte"; +import Content from "./accordion-content.svelte"; +import Item from "./accordion-item.svelte"; +import Trigger from "./accordion-trigger.svelte"; + +export { + Root, + Content, + Item, + Trigger, + // + Root as Accordion, + Content as AccordionContent, + Item as AccordionItem, + Trigger as AccordionTrigger, +}; diff --git a/frontend/src/lib/components/ui/badge/badge.svelte b/frontend/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..51bbc23 --- /dev/null +++ b/frontend/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,49 @@ + + + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/badge/index.ts b/frontend/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..64e0aa9 --- /dev/null +++ b/frontend/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/frontend/src/lib/components/ui/button/button.svelte b/frontend/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..89cedcb --- /dev/null +++ b/frontend/src/lib/components/ui/button/button.svelte @@ -0,0 +1,82 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/frontend/src/lib/components/ui/button/index.ts b/frontend/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..fb585d7 --- /dev/null +++ b/frontend/src/lib/components/ui/button/index.ts @@ -0,0 +1,17 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants, +} from "./button.svelte"; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, +}; diff --git a/frontend/src/lib/components/ui/card/card-action.svelte b/frontend/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..7c48844 --- /dev/null +++ b/frontend/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/card/card-content.svelte b/frontend/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..4f60ee3 --- /dev/null +++ b/frontend/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/card/card-description.svelte b/frontend/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..9b20ac7 --- /dev/null +++ b/frontend/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/frontend/src/lib/components/ui/card/card-footer.svelte b/frontend/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..1efff6d --- /dev/null +++ b/frontend/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/card/card-header.svelte b/frontend/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..7eb69e1 --- /dev/null +++ b/frontend/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/card/card-title.svelte b/frontend/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..1523fc5 --- /dev/null +++ b/frontend/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/card/card.svelte b/frontend/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..2b4f81a --- /dev/null +++ b/frontend/src/lib/components/ui/card/card.svelte @@ -0,0 +1,22 @@ + + +
img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)} + {...restProps} +> + {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/card/index.ts b/frontend/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..4d3fce4 --- /dev/null +++ b/frontend/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; +import Action from "./card-action.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction, +}; diff --git a/frontend/src/lib/components/ui/dialog/dialog-close.svelte b/frontend/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 0000000..de68f2f --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,11 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-content.svelte b/frontend/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000..c0551ee --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,48 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + {#snippet child({ props })} + + {/snippet} + + {/if} + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-description.svelte b/frontend/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000..0102d91 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-footer.svelte b/frontend/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000..e23160a --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,32 @@ + + +
+ {@render children?.()} + {#if showCloseButton} + + {#snippet child({ props })} + + {/snippet} + + {/if} +
diff --git a/frontend/src/lib/components/ui/dialog/dialog-header.svelte b/frontend/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000..c3ce8a2 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte b/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000..19f69f0 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-portal.svelte b/frontend/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 0000000..ccfa79c --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-title.svelte b/frontend/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000..6ff1a4a --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte b/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 0000000..589ee0c --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,11 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog.svelte b/frontend/src/lib/components/ui/dialog/dialog.svelte new file mode 100644 index 0000000..211672c --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/index.ts b/frontend/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..076cef5 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,34 @@ +import Root from "./dialog.svelte"; +import Portal from "./dialog-portal.svelte"; +import Title from "./dialog-title.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; +import Trigger from "./dialog-trigger.svelte"; +import Close from "./dialog-close.svelte"; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, +}; diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 0000000..e0e1971 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..a81b48d --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,44 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else if checked} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..261a5b0 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..433540f --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..aca1f7b --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..c425190 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..e0c534f --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 0000000..274cfef --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..189aef4 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..c8aa07b --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,34 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..90f1b6f --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..ed7cc85 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..b5750d4 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..fab0275 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte new file mode 100644 index 0000000..f044581 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..cb05344 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 0000000..cb4bc62 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/index.ts b/frontend/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..7850c6a --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,54 @@ +import Root from "./dropdown-menu.svelte"; +import Sub from "./dropdown-menu-sub.svelte"; +import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Group from "./dropdown-menu-group.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import Trigger from "./dropdown-menu-trigger.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +import Portal from "./dropdown-menu-portal.svelte"; + +export { + CheckboxGroup, + CheckboxItem, + Content, + Portal, + Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Portal as DropdownMenuPortal, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/frontend/src/lib/components/ui/input/index.ts b/frontend/src/lib/components/ui/input/index.ts new file mode 100644 index 0000000..f47b6d3 --- /dev/null +++ b/frontend/src/lib/components/ui/input/index.ts @@ -0,0 +1,7 @@ +import Root from "./input.svelte"; + +export { + Root, + // + Root as Input, +}; diff --git a/frontend/src/lib/components/ui/input/input.svelte b/frontend/src/lib/components/ui/input/input.svelte new file mode 100644 index 0000000..fe7db38 --- /dev/null +++ b/frontend/src/lib/components/ui/input/input.svelte @@ -0,0 +1,48 @@ + + +{#if type === "file"} + +{:else} + +{/if} diff --git a/frontend/src/lib/components/ui/label/index.ts b/frontend/src/lib/components/ui/label/index.ts new file mode 100644 index 0000000..8bfca0b --- /dev/null +++ b/frontend/src/lib/components/ui/label/index.ts @@ -0,0 +1,7 @@ +import Root from "./label.svelte"; + +export { + Root, + // + Root as Label, +}; diff --git a/frontend/src/lib/components/ui/label/label.svelte b/frontend/src/lib/components/ui/label/label.svelte new file mode 100644 index 0000000..d5e3086 --- /dev/null +++ b/frontend/src/lib/components/ui/label/label.svelte @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/lib/components/ui/select/index.ts b/frontend/src/lib/components/ui/select/index.ts new file mode 100644 index 0000000..4dec358 --- /dev/null +++ b/frontend/src/lib/components/ui/select/index.ts @@ -0,0 +1,37 @@ +import Root from "./select.svelte"; +import Group from "./select-group.svelte"; +import Label from "./select-label.svelte"; +import Item from "./select-item.svelte"; +import Content from "./select-content.svelte"; +import Trigger from "./select-trigger.svelte"; +import Separator from "./select-separator.svelte"; +import ScrollDownButton from "./select-scroll-down-button.svelte"; +import ScrollUpButton from "./select-scroll-up-button.svelte"; +import GroupHeading from "./select-group-heading.svelte"; +import Portal from "./select-portal.svelte"; + +export { + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + GroupHeading, + Portal, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading, + Portal as SelectPortal, +}; diff --git a/frontend/src/lib/components/ui/select/select-content.svelte b/frontend/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 0000000..bb1e2ac --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,45 @@ + + + + + + + {@render children?.()} + + + + diff --git a/frontend/src/lib/components/ui/select/select-group-heading.svelte b/frontend/src/lib/components/ui/select/select-group-heading.svelte new file mode 100644 index 0000000..1fab5f0 --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-group-heading.svelte @@ -0,0 +1,21 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/select/select-group.svelte b/frontend/src/lib/components/ui/select/select-group.svelte new file mode 100644 index 0000000..f666cb2 --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-group.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/select/select-item.svelte b/frontend/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 0000000..32fd5ce --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,38 @@ + + + + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + {/snippet} + diff --git a/frontend/src/lib/components/ui/select/select-label.svelte b/frontend/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 0000000..69bcfdf --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/select/select-portal.svelte b/frontend/src/lib/components/ui/select/select-portal.svelte new file mode 100644 index 0000000..424bcdd --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte b/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte new file mode 100644 index 0000000..94f41cd --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/frontend/src/lib/components/ui/select/select-scroll-up-button.svelte b/frontend/src/lib/components/ui/select/select-scroll-up-button.svelte new file mode 100644 index 0000000..035ea09 --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/frontend/src/lib/components/ui/select/select-separator.svelte b/frontend/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 0000000..3b24bab --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,18 @@ + + + diff --git a/frontend/src/lib/components/ui/select/select-trigger.svelte b/frontend/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 0000000..03d06d0 --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/frontend/src/lib/components/ui/select/select.svelte b/frontend/src/lib/components/ui/select/select.svelte new file mode 100644 index 0000000..05eb663 --- /dev/null +++ b/frontend/src/lib/components/ui/select/select.svelte @@ -0,0 +1,11 @@ + + + diff --git a/frontend/src/lib/components/ui/separator/index.ts b/frontend/src/lib/components/ui/separator/index.ts new file mode 100644 index 0000000..82442d2 --- /dev/null +++ b/frontend/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from "./separator.svelte"; + +export { + Root, + // + Root as Separator, +}; diff --git a/frontend/src/lib/components/ui/separator/separator.svelte b/frontend/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 0000000..5fd8a42 --- /dev/null +++ b/frontend/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,23 @@ + + + diff --git a/frontend/src/lib/components/ui/table/index.ts b/frontend/src/lib/components/ui/table/index.ts new file mode 100644 index 0000000..14695c8 --- /dev/null +++ b/frontend/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from "./table.svelte"; +import Body from "./table-body.svelte"; +import Caption from "./table-caption.svelte"; +import Cell from "./table-cell.svelte"; +import Footer from "./table-footer.svelte"; +import Head from "./table-head.svelte"; +import Header from "./table-header.svelte"; +import Row from "./table-row.svelte"; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow, +}; diff --git a/frontend/src/lib/components/ui/table/table-body.svelte b/frontend/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 0000000..935feae --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-caption.svelte b/frontend/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 0000000..4696cff --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-cell.svelte b/frontend/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 0000000..a998bf6 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-footer.svelte b/frontend/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 0000000..b9b14eb --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0", className)} + {...restProps} +> + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-head.svelte b/frontend/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 0000000..267c4e0 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-header.svelte b/frontend/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 0000000..f47d259 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table-row.svelte b/frontend/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 0000000..90b4e2a --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/table/table.svelte b/frontend/src/lib/components/ui/table/table.svelte new file mode 100644 index 0000000..d95a02e --- /dev/null +++ b/frontend/src/lib/components/ui/table/table.svelte @@ -0,0 +1,17 @@ + + +
+ + {@render children?.()} +
+
diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/frontend/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/frontend/src/lib/stores/app.svelte.ts b/frontend/src/lib/stores/app.svelte.ts new file mode 100644 index 0000000..b90cf05 --- /dev/null +++ b/frontend/src/lib/stores/app.svelte.ts @@ -0,0 +1,179 @@ +import 'tippy.js/animations/shift-toward-subtle.css' +import 'tippy.js/dist/tippy.css' +import tippy from 'tippy.js' // optional +import type {ClientResponseError} from 'pocketbase' + +class AppStore { + theme: string = $state('dark') + + showMobileNavigation: boolean = $state(false) + seo = $state({ + title: 'Title', + description: 'Description', + }) + + // meta: { + // customer: CustomersResponse | undefined + // site: SitesResponse | undefined + // record: RecordsResponse | undefined + // contact: ContactsResponse | undefined + // file: FilesResponse | undefined + // user: UsersResponse | undefined + // } = $state({ + // customer: undefined, + // site: undefined, + // record: undefined, + // contact: undefined, + // file: undefined, + // user: undefined, + // }) + + showGlobalActionMenu = $state(false) + + confirm: { + show: boolean + title: string + text: string + error: boolean | ClientResponseError + yes: () => void + no: () => void + request: (props: any) => void + close: () => void + } = $state({ + show: false, + title: '', + text: '', + error: false, + yes: () => { + }, + no: () => { + }, + + request: (props: any) => { + this.confirm = {...this.confirm, ...props} + this.confirm.show = true + }, + + close: () => { + this.confirm.show = false + this.confirm.title = '' + this.confirm.text = '' + }, + + }) + + + /** + + Global Hotkey handler + + Ignores hotkey handling when focus of user is in generic input-tags or [contenteditable] + + @example + + + + + */ + hotkey = (event: KeyboardEvent, condition: boolean, callback: Function) => { + + // Sicherstellen, dass `event` und `event.target` vorhanden sind + if (!event || !event.target) { + return + } + + // Liste ignorierter Tag-Namen + const ignoredTags = ['INPUT', 'TEXTAREA', 'SELECT', 'OPTION'] + + // Standard-TAG-Prüfung mit optionaler Erweiterung + const isIgnoredTag = ignoredTags.includes((event.target as HTMLElement).tagName?.toUpperCase()) + + // Fokusierbare oder editierbare Inhalte ignorieren + const isEditable = + (event.target as HTMLElement).isContentEditable || // `contenteditable=true` + (event.target as HTMLElement).hasAttribute('contenteditable') // Attribut prüfen für dynamische Inhalte + + if (isIgnoredTag || isEditable) { + // Rücksprung, keine Aktion ausführen + return + } + + // Bedingung und Callback ausführen + if (condition) { + event.preventDefault() // Standardaktion abbrechen + callback() // Callback-Funktion ausführen + } + } + + toggleTheme = () => { + this.theme = this.theme === 'dark' ? 'light' : 'dark' + } +} + +export const app = new AppStore() + + +export const utils = { + + getFileSize: async (fileUrl: string): Promise => { + try { + const response = await fetch(fileUrl, {method: 'HEAD'}) + const contentLength = response.headers.get('content-length') + const bytes = contentLength ? parseInt(contentLength, 10) : 0 + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + if (bytes === 0) return '0 Bytes' + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + } catch (error) { + console.error('Error fetching file size:', error) + return 'Unbekannt' + } + }, + + + sanitize: (input: string) => { + return input.replace(/<\/?("[^"]*"|'[^']*'|[^>])*(>|$)/g, '') + }, + + + excerpt: (input: string, length: number = 20) => { + if (input.length <= 20) return input + return input.substring(0, length) + '…' + }, + + slugify: (input: string) => { + if (!input) return '' + // make lower case and trim + var slug = input.toLowerCase().trim() + // remove accents from charaters + slug = slug.normalize('NFD').replace(/[\u0300-\u036f]/g, '') + // replace invalid chars with spaces + slug = slug.replace(/[^a-z0-9\s-]/g, ' ').trim() + // replace multiple spaces or hyphens with a single hyphen + slug = slug.replace(/[\s-]+/g, '-') + return slug + }, +} + +export const tooltip = ( + element: HTMLElement, + props: { active?: boolean; content?: string; showOnCreate?: boolean }, +) => { + let defaultProps = { + active: true, + animation: 'shift-toward-subtle', + allowHTML: true, + } + props = {...defaultProps, ...props} + if (props.showOnCreate) { + props = { + ...props, + } + } + if (props.active) tippy(element, props) +} diff --git a/frontend/src/lib/stores/events.svelte.ts b/frontend/src/lib/stores/events.svelte.ts new file mode 100644 index 0000000..9896c70 --- /dev/null +++ b/frontend/src/lib/stores/events.svelte.ts @@ -0,0 +1,116 @@ +import { getContext, setContext } from 'svelte' +import { api } from './pocketbase.svelte' +import { app } from './app.svelte' +import { getTeamContext } from './teams.svelte' +import type { EventsResponse } from '$lib/types' + +const KEY = Symbol('event') + +export class EventStore { + records = $state([]) + loading = $state(false) + error = $state(null) + + private unsubscribe: (() => void) | null = null + private teams = getTeamContext() + + /** Records gefiltert nach aktivem Team */ + get scoped(): EventsResponse[] { + const teamId = this.teams.activeId + if (!teamId) return [] + return this.records.filter((r) => r.team === teamId) + } + + async load() { + this.loading = true + this.error = null + try { + this.records = await api.collection('events').getFullList({ + sort: '-created', + requestKey: null, + }) + } catch (e: any) { + this.error = e.message ?? 'Fehler beim Laden der Events' + console.error(e) + } finally { + this.loading = false + } + } + + subscribe() { + if (this.unsubscribe) return + api.collection('events').subscribe('*', (e) => { + const idx = this.records.findIndex((r) => r.id === e.record.id) + if (e.action === 'create' && idx === -1) this.records = [e.record, ...this.records] + else if (e.action === 'update' && idx !== -1) this.records[idx] = e.record + else if (e.action === 'delete' && idx !== -1) this.records = this.records.filter((r) => r.id !== e.record.id) + }).then((unsub) => { + this.unsubscribe = unsub + }).catch((e) => console.warn('events subscribe failed:', e)) + } + + async create(data: Partial) { + const teamId = this.teams.activeId + if (!teamId) throw new Error('Kein aktives Team') + const rec = await api.collection('events').create({ ...data, team: teamId }) + return rec + } + + async edit(id: string, data: Partial) { + return await api.collection('events').update(id, data) + } + + remove(record: EventsResponse) { + return new Promise((resolve, reject) => { + app.confirm.request({ + title: 'Event löschen?', + text: `Möchten Sie das Event „${record.name || record.description}" wirklich löschen?`, + yes: async () => { + try { + await api.collection('events').delete(record.id) + app.confirm.close() + resolve() + } catch (e) { + app.confirm.close() + reject(e) + } + }, + no: () => { + app.confirm.close() + resolve() + }, + }) + }) + } + + getById(id: string) { + return this.records.find((r) => r.id === id) + } + + get active() { + return this.scoped.filter((r) => r.status === 'active') + } + get drafts() { + return this.scoped.filter((r) => r.status === 'draft') + } + get finished() { + return this.scoped.filter((r) => r.status === 'finished') + } + + destroy() { + if (this.unsubscribe) { + this.unsubscribe() + this.unsubscribe = null + } + } +} + +export function setEventContext() { + const store = new EventStore() + setContext(KEY, store) + return store +} + +export function getEventContext(): EventStore { + return getContext(KEY) +} diff --git a/frontend/src/lib/stores/pocketbase.svelte.ts b/frontend/src/lib/stores/pocketbase.svelte.ts new file mode 100644 index 0000000..413423b --- /dev/null +++ b/frontend/src/lib/stores/pocketbase.svelte.ts @@ -0,0 +1,174 @@ +import PocketBase, {type AuthRecord, type RecordModel} from 'pocketbase' +import {PUBLIC_PB_URL} from '$env/static/public' +import type {TypedPocketBase} from '$lib/types' + +export const api = new PocketBase(PUBLIC_PB_URL) as TypedPocketBase + +api.authStore.onChange(async () => { + auth.user = api.authStore.record +}) + +export const getFileURL = (record: object, file: string, options = {}) => { + // https://pocketbase.io/docs/files-handling/#file-url + return api.files.getURL(record, file, options) +} + +class AuthStore { + loading = $state(false) + username = $state('') + password = $state('') + cookie = $state(true) + error: any = $state('') + user = $state(null) + params = {} + + + load = async (cookie: string) => { + api.authStore.loadFromCookie(cookie) + } + + login = async (user = auth.username, password = auth.password) => { + + this.loading = true + this.error = '' + + + if (this.error) { + this.loading = false + return + } + + try { + await api.collection('users').authWithPassword(auth.username, auth.password, this.params) + + this.user = api.authStore.record as AuthRecord | null + + if (auth.user) { + await api.collection('users').update( + auth.user.id, + { + last_login: new Date(Date.now()), + }, + this.params, + ) + } + + if (auth.cookie) { + document.cookie = api.authStore.exportToCookie({httpOnly: false}) + } + + } catch (e) { + this.error = e instanceof Error ? e.message : String(e) + } + + auth.username = auth.password = '' + + this.loading = false + } + + logout = async () => { + api.authStore.clear() + document.cookie = 'pb_auth="";-1; path=/' + } + + + refresh = async () => { + if (api.authStore.isValid) { + await api.collection('users').authRefresh(this.params) + } + } +} + +export const auth = new AuthStore() + + +class CollectionsStore { + + remove = async ({collection, store, record, params}: { + collection: string + store: any + record: Record + params?: Record + }) => { + await api.collection(collection).delete(record.id) + store.records = store.records.filter((i: any) => i.id !== record.id) + } + edit = async ({collection, store, record, params}: { + collection: string + store: any + record: FormData | Record + params?: Record + }) => { + + let id: string + let data: FormData | Record + + if (record instanceof FormData) { + id = record.get('id') as string + data = record + } else { + id = record.id + data = record + } + + // Fetch item via pocketbase API + let item = await api.collection(collection).update(id, data, params) + + // Find the index of the item + const index = store.records.findIndex((i: any) => i.id === id) + if (index !== -1) { + //.. to replace it at the same position + store.records.splice(index, 1, item) + } else { + // Fallback: add to beginning if not found (shouldn't happen in normal cases) + store.records = [ + item, + ...store.records.filter((i: any) => i.id !== id) + ] + } + + return item + } + + create = async ({collection, store, record, params}: { + collection: string + store: any + record: FormData | Record, + params?: Record + }) => { + let data: FormData | Record + + if (record instanceof FormData) { + data = record + } else { + data = record + } + + let item = await api.collection(collection).create(data, params) + store.records = [item, ...store.records] + return item + } + + refresh = async ({collection, store, id, params}: { + collection: string + store: any + id?: string + params?: Record + }) => { + if (id) { + // refreshes only a single record + let record: RecordModel = await api.collection(collection).getOne(id, params) + // and `crossfades` old to new record + store.records = [ + record, + ...store.records.filter((e: RecordModel) => e.id !== id) + ] + return record + } else { + store.records = await api.collection(collection).getFullList(params) + return store.records + } + } +} + +export const collections = new CollectionsStore() \ No newline at end of file diff --git a/frontend/src/lib/stores/riders.svelte.ts b/frontend/src/lib/stores/riders.svelte.ts new file mode 100644 index 0000000..d0fd349 --- /dev/null +++ b/frontend/src/lib/stores/riders.svelte.ts @@ -0,0 +1,144 @@ +import { getContext, setContext } from 'svelte' +import { api } from './pocketbase.svelte' +import { app } from './app.svelte' +import { getTeamContext } from './teams.svelte' +import type { RidersResponse } from '$lib/types' + +const KEY = Symbol('rider') + +function sortByNumber(records: RidersResponse[]) { + return [...records].sort((a, b) => { + const na = parseInt(a.number ?? '0', 10) + const nb = parseInt(b.number ?? '0', 10) + return na - nb + }) +} + +export function fullName(r: RidersResponse) { + const parts = [r.firstname, r.lastname].filter(Boolean) + return parts.join(' ').trim() || r.name || '' +} + +export class RiderStore { + records = $state([]) + loading = $state(false) + error = $state(null) + searchQuery = $state('') + + private unsubscribe: (() => void) | null = null + private teams = getTeamContext() + + get scoped(): RidersResponse[] { + const teamId = this.teams.activeId + if (!teamId) return [] + return this.records.filter((r) => r.team === teamId) + } + + async load() { + this.loading = true + this.error = null + try { + const raw = await api.collection('riders').getFullList({ requestKey: null }) + this.records = sortByNumber(raw) + } catch (e: any) { + this.error = e.message ?? 'Fehler beim Laden der Fahrer' + console.error(e) + } finally { + this.loading = false + } + } + + subscribe() { + if (this.unsubscribe) return + api.collection('riders').subscribe('*', (e) => { + const idx = this.records.findIndex((r) => r.id === e.record.id) + if (e.action === 'create' && idx === -1) { + this.records = sortByNumber([...this.records, e.record]) + } else if (e.action === 'update' && idx !== -1) { + this.records[idx] = e.record + this.records = sortByNumber(this.records) + } else if (e.action === 'delete' && idx !== -1) { + this.records = this.records.filter((r) => r.id !== e.record.id) + } + }).then((unsub) => { + this.unsubscribe = unsub + }).catch((e) => console.warn('riders subscribe failed:', e)) + } + + async create(data: Partial) { + const teamId = this.teams.activeId + if (!teamId) throw new Error('Kein aktives Team') + return await api.collection('riders').create({ ...data, team: teamId }) + } + + async edit(id: string, data: Partial) { + return await api.collection('riders').update(id, data) + } + + remove(record: RidersResponse) { + return new Promise((resolve, reject) => { + app.confirm.request({ + title: 'Fahrer löschen?', + text: `Möchten Sie „#${record.number} ${fullName(record)}" wirklich löschen? Alle zugehörigen Zeiten werden ebenfalls gelöscht.`, + yes: async () => { + try { + await api.collection('riders').delete(record.id) + app.confirm.close() + resolve() + } catch (e) { + app.confirm.close() + reject(e) + } + }, + no: () => { + app.confirm.close() + resolve() + }, + }) + }) + } + + getById(id: string) { + return this.records.find((r) => r.id === id) + } + + getByEvent(eventId: string) { + return this.scoped.filter((r) => r.event === eventId) + } + + isNumberTaken(number: string, eventId: string, excludeId?: string) { + return this.scoped.some( + (r) => r.number === number && r.event === eventId && r.id !== excludeId, + ) + } + + get filtered() { + const q = this.searchQuery.toLowerCase().trim() + if (!q) return this.scoped + return this.scoped.filter((r) => { + return ( + (r.number ?? '').toLowerCase().includes(q) || + (r.firstname ?? '').toLowerCase().includes(q) || + (r.lastname ?? '').toLowerCase().includes(q) || + (r.name ?? '').toLowerCase().includes(q) + ) + }) + } + + destroy() { + if (this.unsubscribe) { + this.unsubscribe() + this.unsubscribe = null + } + } +} + +export function setRiderContext() { + const store = new RiderStore() + setContext(KEY, store) + return store +} + +export function getRiderContext(): RiderStore { + return getContext(KEY) +} diff --git a/frontend/src/lib/stores/runs.svelte.ts b/frontend/src/lib/stores/runs.svelte.ts new file mode 100644 index 0000000..d98bf66 --- /dev/null +++ b/frontend/src/lib/stores/runs.svelte.ts @@ -0,0 +1,108 @@ +import { getContext, setContext } from 'svelte' +import { api } from './pocketbase.svelte' +import { app } from './app.svelte' +import { getTeamContext } from './teams.svelte' +import type { RunsResponse } from '$lib/types' + +const KEY = Symbol('run') + +export class RunStore { + records = $state([]) + loading = $state(false) + error = $state(null) + + private unsubscribe: (() => void) | null = null + private teams = getTeamContext() + + get scoped(): RunsResponse[] { + const teamId = this.teams.activeId + if (!teamId) return [] + return this.records.filter((r) => r.team === teamId) + } + + async load() { + this.loading = true + this.error = null + try { + this.records = await api.collection('runs').getFullList({ + sort: '+created', + requestKey: null, + }) + } catch (e: any) { + this.error = e.message ?? 'Fehler beim Laden der Runs' + console.error(e) + } finally { + this.loading = false + } + } + + subscribe() { + if (this.unsubscribe) return + api.collection('runs').subscribe('*', (e) => { + const idx = this.records.findIndex((r) => r.id === e.record.id) + if (e.action === 'create' && idx === -1) this.records = [...this.records, e.record] + else if (e.action === 'update' && idx !== -1) this.records[idx] = e.record + else if (e.action === 'delete' && idx !== -1) this.records = this.records.filter((r) => r.id !== e.record.id) + }).then((unsub) => { + this.unsubscribe = unsub + }).catch((e) => console.warn('runs subscribe failed:', e)) + } + + async create(data: Partial) { + const teamId = this.teams.activeId + if (!teamId) throw new Error('Kein aktives Team') + return await api.collection('runs').create({ ...data, team: teamId }) + } + + async edit(id: string, data: Partial) { + return await api.collection('runs').update(id, data) + } + + remove(record: RunsResponse) { + return new Promise((resolve, reject) => { + app.confirm.request({ + title: 'Run löschen?', + text: `Möchten Sie „${record.description}" wirklich löschen? Alle zugehörigen Zeiten werden ebenfalls gelöscht.`, + yes: async () => { + try { + await api.collection('runs').delete(record.id) + app.confirm.close() + resolve() + } catch (e) { + app.confirm.close() + reject(e) + } + }, + no: () => { + app.confirm.close() + resolve() + }, + }) + }) + } + + getById(id: string) { + return this.records.find((r) => r.id === id) + } + + getByEvent(eventId: string) { + return this.scoped.filter((r) => r.event === eventId) + } + + destroy() { + if (this.unsubscribe) { + this.unsubscribe() + this.unsubscribe = null + } + } +} + +export function setRunContext() { + const store = new RunStore() + setContext(KEY, store) + return store +} + +export function getRunContext(): RunStore { + return getContext(KEY) +} diff --git a/frontend/src/lib/stores/teams.svelte.ts b/frontend/src/lib/stores/teams.svelte.ts new file mode 100644 index 0000000..9968197 --- /dev/null +++ b/frontend/src/lib/stores/teams.svelte.ts @@ -0,0 +1,200 @@ +import { getContext, setContext } from 'svelte' +import { api, auth } from './pocketbase.svelte' +import { app } from './app.svelte' +import type { TeamsResponse, UsersResponse } from '$lib/types' + +const KEY = Symbol('team') +const LS_KEY = 'activeTeamId' + +export class TeamStore { + records = $state([]) + activeId = $state(null) + loading = $state(false) + error = $state(null) + + private unsubscribe: (() => void) | null = null + + get active(): TeamsResponse | null { + return this.records.find((t) => t.id === this.activeId) ?? null + } + + /** ID des aktuell eingeloggten Users */ + private get me(): string | null { + return api.authStore.record?.id ?? null + } + + isOwner(team: TeamsResponse | null = this.active): boolean { + if (!team || !this.me) return false + return team.owner === this.me + } + + isAdmin(team: TeamsResponse | null = this.active): boolean { + if (!team || !this.me) return false + if (this.isOwner(team)) return true + return (team.admins ?? []).includes(this.me) + } + + isMember(team: TeamsResponse | null = this.active): boolean { + if (!team || !this.me) return false + return (team.users ?? []).includes(this.me) + } + + async load() { + this.loading = true + this.error = null + try { + this.records = await api.collection('teams').getFullList({ + sort: '+name', + requestKey: null, + }) + + // Aktives Team aus localStorage wiederherstellen, sonst erstes nehmen + const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(LS_KEY) : null + if (stored && this.records.some((t) => t.id === stored)) { + this.activeId = stored + } else if (this.records.length > 0) { + this.activeId = this.records[0].id + } else { + this.activeId = null + } + } catch (e: any) { + this.error = e.message ?? 'Fehler beim Laden der Teams' + console.error(e) + } finally { + this.loading = false + } + } + + subscribe() { + if (this.unsubscribe) return + api.collection('teams').subscribe('*', (e) => { + const idx = this.records.findIndex((r) => r.id === e.record.id) + if (e.action === 'create' && idx === -1) this.records = [...this.records, e.record].sort((a, b) => a.name.localeCompare(b.name)) + else if (e.action === 'update' && idx !== -1) this.records[idx] = e.record + else if (e.action === 'delete' && idx !== -1) { + this.records = this.records.filter((r) => r.id !== e.record.id) + if (this.activeId === e.record.id) { + this.activeId = this.records[0]?.id ?? null + } + } + }).then((unsub) => { + this.unsubscribe = unsub + }).catch((e) => console.warn('teams subscribe failed:', e)) + } + + setActive(id: string) { + if (!this.records.some((t) => t.id === id)) return + this.activeId = id + if (typeof localStorage !== 'undefined') localStorage.setItem(LS_KEY, id) + } + + async create(name: string) { + if (!this.me) throw new Error('Nicht eingeloggt') + const team = await api.collection('teams').create({ + name, + owner: this.me, + users: [this.me], + admins: [], + }) + // sofort als aktiv setzen + setTimeout(() => this.setActive(team.id), 0) + return team + } + + async rename(id: string, name: string) { + return await api.collection('teams').update(id, { name }) + } + + /** Mitglied hinzufügen (Owner/Admin only) */ + async addMember(teamId: string, userId: string) { + const team = this.records.find((t) => t.id === teamId) + if (!team) throw new Error('Team nicht gefunden') + const users = [...(team.users ?? [])] + if (!users.includes(userId)) users.push(userId) + return await api.collection('teams').update(teamId, { users }) + } + + /** Mitglied entfernen */ + async removeMember(teamId: string, userId: string) { + const team = this.records.find((t) => t.id === teamId) + if (!team) throw new Error('Team nicht gefunden') + const users = (team.users ?? []).filter((u) => u !== userId) + const admins = (team.admins ?? []).filter((u) => u !== userId) + return await api.collection('teams').update(teamId, { users, admins }) + } + + async promoteToAdmin(teamId: string, userId: string) { + const team = this.records.find((t) => t.id === teamId) + if (!team) throw new Error('Team nicht gefunden') + const admins = [...(team.admins ?? [])] + if (!admins.includes(userId)) admins.push(userId) + return await api.collection('teams').update(teamId, { admins }) + } + + async demoteFromAdmin(teamId: string, userId: string) { + const team = this.records.find((t) => t.id === teamId) + if (!team) throw new Error('Team nicht gefunden') + const admins = (team.admins ?? []).filter((u) => u !== userId) + return await api.collection('teams').update(teamId, { admins }) + } + + /** Verlassen — entfernt sich selbst aus users + admins */ + async leave(teamId: string) { + if (!this.me) return + return await this.removeMember(teamId, this.me) + } + + /** Team löschen (nur Owner) */ + remove(team: TeamsResponse) { + return new Promise((resolve, reject) => { + app.confirm.request({ + title: 'Team löschen?', + text: `Möchtest du das Team „${team.name}" wirklich löschen? Achtung: zugehörige Events, Fahrer und Zeiten werden NICHT gelöscht, sind aber für niemanden mehr sichtbar.`, + yes: async () => { + try { + await api.collection('teams').delete(team.id) + app.confirm.close() + resolve() + } catch (e) { + app.confirm.close() + reject(e) + } + }, + no: () => { + app.confirm.close() + resolve() + }, + }) + }) + } + + /** User per E-Mail suchen (für Einladungs-UI) */ + async findUserByEmail(email: string): Promise { + try { + const res = await api.collection('users').getFirstListItem( + `email = "${email.replaceAll('"', '')}"`, + { requestKey: null }, + ) + return res + } catch { + return null + } + } + + destroy() { + if (this.unsubscribe) { + this.unsubscribe() + this.unsubscribe = null + } + } +} + +export function setTeamContext() { + const store = new TeamStore() + setContext(KEY, store) + return store +} + +export function getTeamContext(): TeamStore { + return getContext(KEY) +} diff --git a/frontend/src/lib/stores/times.svelte.ts b/frontend/src/lib/stores/times.svelte.ts new file mode 100644 index 0000000..f804580 --- /dev/null +++ b/frontend/src/lib/stores/times.svelte.ts @@ -0,0 +1,176 @@ +import { getContext, setContext } from 'svelte' +import { api } from './pocketbase.svelte' +import { app } from './app.svelte' +import { getTeamContext } from './teams.svelte' +import type { TimesResponse, TimesStatusOptions } from '$lib/types' + +const KEY = Symbol('time') + +export type TimeStatus = TimesStatusOptions + +export interface TimeWithDuration extends TimesResponse { + duration?: number + formattedTime?: string +} + +function enrich(t: TimesResponse): TimeWithDuration { + if (!t.start) return t + const start = new Date(t.start).getTime() + const end = t.stop ? new Date(t.stop).getTime() : Date.now() + let duration = (end - start) / 1000 + if (t.correction) duration += t.correction + const minutes = Math.floor(duration / 60) + const seconds = (duration % 60).toFixed(1) + return { + ...t, + duration, + formattedTime: `${minutes}:${seconds.padStart(4, '0')}`, + } +} + +export class TimeStore { + records = $state([]) + loading = $state(false) + error = $state(null) + + private unsubscribe: (() => void) | null = null + private teams = getTeamContext() + + get scoped(): TimesResponse[] { + const teamId = this.teams.activeId + if (!teamId) return [] + return this.records.filter((r) => r.team === teamId) + } + + async load() { + this.loading = true + this.error = null + try { + this.records = await api.collection('times').getFullList({ + sort: '-created', + requestKey: null, + }) + } catch (e: any) { + this.error = e.message ?? 'Fehler beim Laden der Zeiten' + console.error(e) + } finally { + this.loading = false + } + } + + subscribe() { + if (this.unsubscribe) return + api.collection('times').subscribe('*', (e) => { + const idx = this.records.findIndex((r) => r.id === e.record.id) + if (e.action === 'create' && idx === -1) this.records = [e.record, ...this.records] + else if (e.action === 'update' && idx !== -1) this.records[idx] = e.record + else if (e.action === 'delete' && idx !== -1) this.records = this.records.filter((r) => r.id !== e.record.id) + }).then((unsub) => { + this.unsubscribe = unsub + }).catch((e) => console.warn('times subscribe failed:', e)) + } + + async start(runId: string, riderId: string) { + const teamId = this.teams.activeId + if (!teamId) throw new Error('Kein aktives Team') + return await api.collection('times').create({ + run: runId, + rider: riderId, + team: teamId, + start: new Date().toISOString(), + status: 'active' as TimesStatusOptions, + startedBy: api.authStore.record?.id, + correction: 0, + }) + } + + async stop(timeId: string, status: TimeStatus = 'finished' as TimesStatusOptions) { + return await api.collection('times').update(timeId, { + stop: new Date().toISOString(), + status, + stoppedBy: api.authStore.record?.id, + }) + } + + async applyCorrection(timeId: string, seconds: number) { + return await api.collection('times').update(timeId, { correction: seconds }) + } + + async updateStatus(timeId: string, status: TimeStatus) { + return await api.collection('times').update(timeId, { status }) + } + + remove(record: TimesResponse) { + return new Promise((resolve, reject) => { + app.confirm.request({ + title: 'Zeit löschen?', + text: 'Möchten Sie diese Zeit wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.', + yes: async () => { + try { + await api.collection('times').delete(record.id) + app.confirm.close() + resolve() + } catch (e) { + app.confirm.close() + reject(e) + } + }, + no: () => { + app.confirm.close() + resolve() + }, + }) + }) + } + + getByRun(runId: string): TimeWithDuration[] { + return this.scoped.filter((r) => r.run === runId).map(enrich) + } + + getByRider(riderId: string): TimeWithDuration[] { + return this.scoped.filter((r) => r.rider === riderId).map(enrich) + } + + isRiderActive(runId: string, riderId: string) { + return this.scoped.some( + (r) => r.run === runId && r.rider === riderId && r.status === 'active', + ) + } + + getActiveTime(runId: string, riderId: string) { + return this.scoped.find( + (r) => r.run === runId && r.rider === riderId && r.status === 'active', + ) + } + + get running(): TimeWithDuration[] { + return this.scoped.filter((r) => r.status === 'active').map(enrich) + } + + get finished(): TimeWithDuration[] { + return this.scoped.filter((r) => r.status === 'finished').map(enrich) + } + + getLeaderboard(runId: string): TimeWithDuration[] { + return this.getByRun(runId) + .filter((r) => r.status === 'finished' && r.duration !== undefined) + .sort((a, b) => (a.duration ?? 0) - (b.duration ?? 0)) + } + + destroy() { + if (this.unsubscribe) { + this.unsubscribe() + this.unsubscribe = null + } + } +} + +export function setTimeContext() { + const store = new TimeStore() + setContext(KEY, store) + return store +} + +export function getTimeContext(): TimeStore { + return getContext(KEY) +} diff --git a/frontend/src/lib/types.d.ts b/frontend/src/lib/types.d.ts new file mode 100644 index 0000000..a53c3fe --- /dev/null +++ b/frontend/src/lib/types.d.ts @@ -0,0 +1,242 @@ +/** +* This file was @generated using pocketbase-typegen +*/ + +import type PocketBase from 'pocketbase' +import type { RecordService } from 'pocketbase' + +export enum Collections { + Authorigins = "_authOrigins", + Externalauths = "_externalAuths", + Mfas = "_mfas", + Otps = "_otps", + Superusers = "_superusers", + Events = "events", + Riders = "riders", + Runs = "runs", + Teams = "teams", + Times = "times", + Users = "users", +} + +// Alias types for improved usability +export type IsoDateString = string +export type RecordIdString = string +export type HTMLString = string + +type ExpandType = unknown extends T + ? T extends unknown + ? { expand?: unknown } + : { expand: T } + : { expand: T } + +// System fields +export type BaseSystemFields = { + id: RecordIdString + collectionId: string + collectionName: Collections +} & ExpandType + +export type AuthSystemFields = { + email: string + emailVisibility: boolean + username: string + verified: boolean +} & BaseSystemFields + +// Record types for each collection + +export type AuthoriginsRecord = { + collectionRef: string + created?: IsoDateString + fingerprint: string + id: string + recordRef: string + updated?: IsoDateString +} + +export type ExternalauthsRecord = { + collectionRef: string + created?: IsoDateString + id: string + provider: string + providerId: string + recordRef: string + updated?: IsoDateString +} + +export type MfasRecord = { + collectionRef: string + created?: IsoDateString + id: string + method: string + recordRef: string + updated?: IsoDateString +} + +export type OtpsRecord = { + collectionRef: string + created?: IsoDateString + id: string + password: string + recordRef: string + sentTo?: string + updated?: IsoDateString +} + +export type SuperusersRecord = { + created?: IsoDateString + email: string + emailVisibility?: boolean + id: string + password: string + tokenKey: string + updated?: IsoDateString + verified?: boolean +} + +export enum EventsStatusOptions { + "draft" = "draft", + "active" = "active", + "finished" = "finished", +} +export type EventsRecord = { + created?: IsoDateString + description?: string + gps?: string + id: string + location?: string + name?: string + status?: EventsStatusOptions + team?: RecordIdString + updated?: IsoDateString +} + +export type RidersRecord = { + avatar?: string + created?: IsoDateString + event?: RecordIdString + firstname?: string + id: string + lastname?: string + name?: string + number?: string + team?: RecordIdString + updated?: IsoDateString +} + +export type RunsRecord = { + created?: IsoDateString + description?: string + event?: RecordIdString + id: string + team?: RecordIdString + updated?: IsoDateString +} + +export type TeamsRecord = { + admins?: RecordIdString[] + created?: IsoDateString + id: string + name: string + owner: RecordIdString + updated?: IsoDateString + users?: RecordIdString[] +} + +export enum TimesStatusOptions { + "active" = "active", + "finished" = "finished", + "dnf" = "dnf", + "dns" = "dns", + "dsq" = "dsq", +} +export type TimesRecord = { + comment?: string + correction?: number + created?: IsoDateString + id: string + rider?: RecordIdString + run?: RecordIdString + start?: IsoDateString + startedBy?: RecordIdString + status?: TimesStatusOptions + stop?: IsoDateString + stoppedBy?: RecordIdString + team?: RecordIdString + updated?: IsoDateString +} + +export type UsersRecord = { + avatar?: string + created?: IsoDateString + email?: string + emailVisibility?: boolean + id: string + name?: string + password: string + tokenKey: string + updated?: IsoDateString + username: string + verified?: boolean +} + +// Response types include system fields and match responses from the PocketBase API +export type AuthoriginsResponse = Required & BaseSystemFields +export type ExternalauthsResponse = Required & BaseSystemFields +export type MfasResponse = Required & BaseSystemFields +export type OtpsResponse = Required & BaseSystemFields +export type SuperusersResponse = Required & AuthSystemFields +export type EventsResponse = Required & BaseSystemFields +export type RidersResponse = Required & BaseSystemFields +export type RunsResponse = Required & BaseSystemFields +export type TeamsResponse = Required & BaseSystemFields +export type TimesResponse = Required & BaseSystemFields +export type UsersResponse = Required & AuthSystemFields + +// Types containing all Records and Responses, useful for creating typing helper functions + +export type CollectionRecords = { + _authOrigins: AuthoriginsRecord + _externalAuths: ExternalauthsRecord + _mfas: MfasRecord + _otps: OtpsRecord + _superusers: SuperusersRecord + events: EventsRecord + riders: RidersRecord + runs: RunsRecord + teams: TeamsRecord + times: TimesRecord + users: UsersRecord +} + +export type CollectionResponses = { + _authOrigins: AuthoriginsResponse + _externalAuths: ExternalauthsResponse + _mfas: MfasResponse + _otps: OtpsResponse + _superusers: SuperusersResponse + events: EventsResponse + riders: RidersResponse + runs: RunsResponse + teams: TeamsResponse + times: TimesResponse + users: UsersResponse +} + +// Type for usage with type asserted PocketBase instance +// https://github.com/pocketbase/js-sdk#specify-typescript-definitions + +export type TypedPocketBase = PocketBase & { + collection(idOrName: '_authOrigins'): RecordService + collection(idOrName: '_externalAuths'): RecordService + collection(idOrName: '_mfas'): RecordService + collection(idOrName: '_otps'): RecordService + collection(idOrName: '_superusers'): RecordService + collection(idOrName: 'events'): RecordService + collection(idOrName: 'riders'): RecordService + collection(idOrName: 'runs'): RecordService + collection(idOrName: 'teams'): RecordService + collection(idOrName: 'times'): RecordService + collection(idOrName: 'users'): RecordService +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..55b3a91 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..98a1e6b --- /dev/null +++ b/frontend/src/routes/+layout.svelte @@ -0,0 +1,35 @@ + + + + + + + + +{#if ready} + {@render children?.()} + +{/if} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..ce6f451 --- /dev/null +++ b/frontend/src/routes/+page.svelte @@ -0,0 +1,14 @@ + + +
+ +
diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte new file mode 100644 index 0000000..c7aa23a --- /dev/null +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -0,0 +1,170 @@ + + +{#if auth.user} +
+
+
+
+
+ + Zeitnahme +
+ + + +
+ + + + + + +
+
+
+ + {#if isMobileMenuOpen} +
+ +
+ {/if} +
+ +
+ {@render children?.()} +
+
+{/if} diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..05c7c72 --- /dev/null +++ b/frontend/src/routes/dashboard/+page.svelte @@ -0,0 +1,145 @@ + + + + Dashboard – Zeitnahme + + +
+
+

Willkommen, {auth.user?.name || auth.user?.email}!

+ {#if teams.active} +

+ Team {teams.active.name} · {stats.totalEvents} Events, {stats.totalRiders} Fahrer +

+ {:else} +

Kein Team aktiv. Team verwalten.

+ {/if} +
+ +
+ + + Events + + + +
{stats.totalEvents}
+

{stats.activeEvents} aktiv

+
+
+ + + + Fahrer + + + +
{stats.totalRiders}
+

registriert

+
+
+ + + + Laufende Zeiten + + + +
{stats.running}
+

{stats.finished} abgeschlossen

+
+
+
+ + + +
+

Aktive Events

+ {#if events.active.length === 0} + + + +

Keine aktiven Events

+

+ Aktiviere ein Event über die Eventliste, um mit der Zeitnahme zu beginnen. +

+ +
+
+ {:else} +
+ {#each events.active as event} + {@const eventRuns = runs.getByEvent(event.id).length} + + +
+ {event.name || event.description || 'Unbenannt'} + Aktiv +
+ {#if event.location} + + + {event.location} + + {/if} +
+ +

{eventRuns} Run{eventRuns === 1 ? '' : 's'}

+ +
+
+ {/each} +
+ {/if} +
+
diff --git a/frontend/src/routes/dashboard/events/+page.svelte b/frontend/src/routes/dashboard/events/+page.svelte new file mode 100644 index 0000000..391e38b --- /dev/null +++ b/frontend/src/routes/dashboard/events/+page.svelte @@ -0,0 +1,190 @@ + + +Events – Zeitnahme + +
+
+
+

Events

+

Veranstaltungen und Rennen verwalten

+
+ +
+ + {#if events.loading} +
Lade Events…
+ {:else if events.scoped.length === 0} + + + +

Keine Events vorhanden

+

Erstelle dein erstes Event, um zu beginnen.

+ +
+
+ {:else} + {#each groups as group} + {#if group.items.length > 0} +
+

{group.title} ({group.items.length})

+ +
+ {/if} + {/each} + {/if} +
+ + + + + {editing ? 'Event bearbeiten' : 'Neues Event'} + + Trage die Stammdaten ein. Mehr Details lassen sich später ergänzen. + + + +
{ + e.preventDefault() + save() + }} + class="space-y-4" + > +
+ + +
+
+ + +
+
+ + +
+
+ + + + {statusLabels[form.status]} + + + Entwurf + Aktiv + Abgeschlossen + + +
+ + + + + +
+
+
diff --git a/frontend/src/routes/dashboard/events/[id]/+page.svelte b/frontend/src/routes/dashboard/events/[id]/+page.svelte new file mode 100644 index 0000000..2653d1d --- /dev/null +++ b/frontend/src/routes/dashboard/events/[id]/+page.svelte @@ -0,0 +1,352 @@ + + +{event?.name ?? 'Event'} – Zeitnahme + +{#if !event} + + + +

Event nicht gefunden

+

Vermutlich gehört es nicht zum aktiven Team.

+ +
+
+{:else} +
+
+
+ + + Eventliste + +
+

{event.name || 'Unbenannt'}

+ + {event.status === 'active' ? 'Aktiv' : event.status === 'finished' ? 'Abgeschlossen' : 'Entwurf'} + +
+ {#if event.location} +

+ + {event.location} +

+ {/if} + {#if event.description && event.description !== event.name} +

{event.description}

+ {/if} +
+
+ {#if event.status === 'active'} + + {/if} + +
+
+ + + + +
+
+
+

+ + Fahrer + ({eventRiders.length}) +

+

Teilnehmer dieses Events

+
+ +
+ + + + {#if eventRiders.length === 0} +
+ +

Noch keine Fahrer

+

Füge den ersten Fahrer hinzu.

+ +
+ {:else} + + + + # + Name + Aktionen + + + + {#each eventRiders as rider (rider.id)} + + + {rider.number || '—'} + + + {#if fullName(rider)} + {fullName(rider)} + {:else} + — ohne Name — + {/if} + + + + + + + {/each} + + + {/if} +
+
+
+ + + + +
+
+
+

+ + Runs + ({eventRuns.length}) +

+

Einzelne Läufe/Stages des Events

+
+ +
+ + + + {#if eventRuns.length === 0} +
+ +

Noch keine Runs

+

Lege den ersten Lauf an.

+ +
+ {:else} + + + + Bezeichnung + Aktionen + + + + {#each eventRuns as run (run.id)} + + {run.description} + + + + + + {/each} + + + {/if} +
+
+
+
+{/if} + + + + + + {ridingEdit ? 'Fahrer bearbeiten' : 'Neuer Fahrer'} + + Fahrer für „{event?.name}" + + +
{ + e.preventDefault() + saveRider() + }} + class="space-y-4" + > +
+ + +
+
+
+ + +
+
+ + +
+
+ + + + +
+
+
+ + + + + + {runEdit ? 'Run bearbeiten' : 'Neuer Run'} + + Run für „{event?.name}" + + +
{ + e.preventDefault() + saveRun() + }} + class="space-y-4" + > +
+ + +
+ + + + +
+
+
diff --git a/frontend/src/routes/dashboard/results/+page.svelte b/frontend/src/routes/dashboard/results/+page.svelte new file mode 100644 index 0000000..9fe7848 --- /dev/null +++ b/frontend/src/routes/dashboard/results/+page.svelte @@ -0,0 +1,203 @@ + + +Ergebnisse + +
+
+
+

Ergebnisse

+

Zeiten und Rankings

+
+ {#if leaderboard.length > 0} + + {/if} +
+ + + +
+
+ + + {selectedEventName} + + {#each events.scoped as e} + {e.name || e.description} + {/each} + + +
+
+ + + {selectedRunName} + + {#each availableRuns as r} + {r.description} + {/each} + + +
+
+
+
+ + {#if !selectedRunId} + + + +

Wähle Event und Run

+

Um Ergebnisse anzuzeigen, oben auswählen.

+
+
+ {:else if leaderboard.length === 0} + + + +

Noch keine Ergebnisse

+

Für diesen Run wurden noch keine Zeiten erfasst.

+
+
+ {:else} + {#if podium.length > 0} +
+ {#each podium as t, i} + {@const r = getRider(t.rider)} + {#if r} + + + +

Platz {i + 1}

+

#{r.number} {fullName(r)}

+

{t.formattedTime}

+
+
+ {/if} + {/each} +
+ {/if} + + + + Vollständige Rangliste + + + + + + Platz + # + Name + Zeit + Korrektur + + + + {#each leaderboard as t, i (t.id)} + {@const r = getRider(t.rider)} + + + + {i + 1} + + + + {r?.number ?? '—'} + + {r ? fullName(r) : '—'} + {t.formattedTime} + + {t.correction ? `${t.correction > 0 ? '+' : ''}${t.correction.toFixed(1)}s` : '–'} + + + {/each} + + + + + {/if} +
diff --git a/frontend/src/routes/dashboard/teams/+page.svelte b/frontend/src/routes/dashboard/teams/+page.svelte new file mode 100644 index 0000000..26a90bc --- /dev/null +++ b/frontend/src/routes/dashboard/teams/+page.svelte @@ -0,0 +1,341 @@ + + +Teams – Zeitnahme + +
+
+
+

Teams

+

Deine Teams und Mitglieder verwalten

+
+ +
+ + {#if teams.records.length === 0} + + + +

Noch in keinem Team

+

Erstelle dein erstes Team.

+ +
+
+ {:else} +
+ {#each teams.records as t (t.id)} + {@const iAmOwner = t.owner === me} + {@const iAmAdmin = iAmOwner || (t.admins ?? []).includes(me ?? '')} + + +
+
+ + {t.name} + {#if t.id === teams.activeId} + Aktiv + {/if} + + + {(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'} + +
+
+ {#if t.id !== teams.activeId} + + {/if} + {#if iAmAdmin} + + {/if} + {#if !iAmOwner} + + {/if} + {#if iAmOwner} + + {/if} +
+
+
+ +
    + {#each (t.users ?? []) as userId} + {@const isOwner = userId === t.owner} + {@const isAdm = isAdmin(t, userId)} +
  • +
    + {userLabel(userId)} + {#if isOwner} + + + Owner + + {:else if isAdm} + + + Admin + + {/if} + {#if userId === me} + Du + {/if} +
    + {#if iAmOwner && !isOwner} +
    + {#if isAdm} + + {:else} + + {/if} + +
    + {/if} +
  • + {/each} +
+
+
+ {/each} +
+ {/if} +
+ + + + + + Neues Team + Du wirst automatisch Owner. + +
{ + e.preventDefault() + doCreate() + }} + class="space-y-4" + > +
+ + +
+ + + + +
+
+
+ + + + + + Mitglied hinzufügen + + Suche einen registrierten User über seine E-Mail-Adresse. + + +
{ + e.preventDefault() + doSearch() + }} + class="space-y-4" + > +
+ +
+ + +
+
+ + {#if inviteError} +
+ {inviteError} +
+ {/if} + + {#if inviteFound} +
+

{inviteFound.name || inviteFound.email}

+ {#if inviteFound.email} +

{inviteFound.email}

+ {/if} +
+ {/if} + + + + + +
+
+
diff --git a/frontend/src/routes/dashboard/timing/+page.svelte b/frontend/src/routes/dashboard/timing/+page.svelte new file mode 100644 index 0000000..488b9b6 --- /dev/null +++ b/frontend/src/routes/dashboard/timing/+page.svelte @@ -0,0 +1,281 @@ + + +Zeitnahme + +
+
+

Zeitnahme

+

Zeiten für Fahrer starten und stoppen

+
+ + + +
+
+ + + {selectedEventName} + + {#each events.scoped as e} + {e.name || e.description} + {/each} + + +
+
+ + + {selectedRunName} + + {#each availableRuns as r} + {r.description} + {/each} + + + {#if selectedEventId && availableRuns.length === 0} +

+ Keine Runs für dieses Event. Über PocketBase Runs anlegen. +

+ {/if} +
+
+
+
+ + {#if !selectedEventId || !selectedRunId} + + + +

Wähle ein Event und einen Run

+

+ Um mit der Zeitnahme zu beginnen, oben Event und Run auswählen. +

+
+
+ {:else} +
+
+
+ + +
+ + + + {#if filteredRiders().length === 0} +
+ {searchQuery ? 'Keine Treffer' : 'Keine Fahrer für dieses Event'} +
+ {:else} +
    + {#each filteredRiders() as rider (rider.id)} + {@const active = times.isRiderActive(selectedRunId, rider.id)} + {@const activeTime = active ? times.getActiveTime(selectedRunId, rider.id) : undefined} +
  • +
    + + {rider.number || '—'} + +
    +

    {fullName(rider) || '—'}

    +
    +
    +
    + {#if active && activeTime} + {#key tick} +
    + {getElapsed(activeTime.start)} +
    + {/key} + + {:else} + + {/if} +
    +
  • + {/each} +
+ {/if} +
+
+
+ +
+ + + + + Laufende Zeiten ({runningTimes.length}) + + + + {#if runningTimes.length === 0} +

Keine laufenden Zeiten

+ {:else} +
    + {#each runningTimes as t} + {@const r = getRider(t.rider)} + {#if r} + {#key tick} +
  • +

    #{r.number} {fullName(r)}

    +

    {getElapsed(t.start)}

    +
  • + {/key} + {/if} + {/each} +
+ {/if} +
+
+ + + + + + Top 5 + + + + {#if leaderboard.length === 0} +

Noch keine Ergebnisse

+ {:else} +
    + {#each leaderboard as t, i} + {@const r = getRider(t.rider)} + {#if r} +
  1. +
    + + {i + 1} + + #{r.number} {fullName(r)} +
    + {t.formattedTime} +
  2. + {/if} + {/each} +
+ {/if} +
+
+
+
+ {/if} +
diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte new file mode 100644 index 0000000..96ba878 --- /dev/null +++ b/frontend/src/routes/login/+page.svelte @@ -0,0 +1,93 @@ + + +Login – Zeitnahme + +
+ + + Zeitnahme + Melde dich an, um fortzufahren + + +
+
+ + +
+
+ + +
+ + + {#if auth.error} +
+ {auth.error} +
+ {/if} + + +
+
+
+
diff --git a/frontend/static/radl.svg b/frontend/static/radl.svg new file mode 100644 index 0000000..6d59d70 --- /dev/null +++ b/frontend/static/radl.svg @@ -0,0 +1,942 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/static/robots.txt b/frontend/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/frontend/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/frontend/static/schildl.svg b/frontend/static/schildl.svg new file mode 100644 index 0000000..effdd9f --- /dev/null +++ b/frontend/static/schildl.svg @@ -0,0 +1,896 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 0000000..4c0a795 --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,21 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter(), + alias: { + "@/*": "./src/lib/*", + }, + } +}; + +export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..a5567ee --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..4df9c30 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,14 @@ +import tailwindcss from '@tailwindcss/vite' +import { sveltekit } from '@sveltejs/kit/vite' +import { defineConfig } from 'vite' +import pkg from './package.json' + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()], + server: { + host: true, + port: 31337, + strictPort: true, + open: `http://${pkg.name}.localhost:31337`, + }, +}) \ No newline at end of file