Compare commits
2 commits
abgebroche
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53a8bfe29b | ||
|
|
c39599bb61 |
164 changed files with 821 additions and 22232 deletions
193
CLAUDE.md
193
CLAUDE.md
|
|
@ -1,175 +1,46 @@
|
||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
## Was dieses Repo ist
|
||||||
|
|
||||||
## Project Overview
|
Ein Wegweiser, keine Anwendung. Drei vorgerenderte Seiten unter
|
||||||
|
`stammtisch-hersbruck.de`: Wappen, ein Satz, ein Knopf nach
|
||||||
|
[kurbeler.de](https://kurbeler.de), dazu Impressum und Datenschutz.
|
||||||
|
|
||||||
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, stages, riders, times and teams for a motorsport/cycling event organization.
|
Die Anwendung, die hier bis September 2026 lag, heißt jetzt **Kurbeler** und
|
||||||
|
wird in `~/Dokumente/Projekte/kurbeler` fortgeführt. **Neue Features gehören
|
||||||
|
dorthin, nicht hierher.** Wer hier eine Zeitnahme, einen Kader oder ein Event
|
||||||
|
einbaut, baut sie ein zweites Mal — und ab dem ersten Tag läuft sie
|
||||||
|
auseinander.
|
||||||
|
|
||||||
The repository is a monorepo: the SvelteKit app lives in `frontend/`, the PocketBase instance in `backend/`.
|
## Die Grenze
|
||||||
|
|
||||||
## Development Commands
|
Was diese Seite darf: über sich selbst Auskunft geben und weiterzeigen.
|
||||||
|
|
||||||
Alle Frontend-Befehle laufen aus `frontend/`:
|
Was sie nicht darf: irgendetwas laden, speichern oder abfragen. Kein
|
||||||
|
PocketBase, keine Anmeldung, kein Formular, keine Cookies, keine Schriften und
|
||||||
|
Karten von fremden Servern. Die Datenschutzerklärung sagt das zu, und sie ist
|
||||||
|
nur so lange wahr, wie es so bleibt. Jede Einbindung, die eine Anfrage an einen
|
||||||
|
dritten Server auslöst, macht sie zu einer falschen Auskunft über die
|
||||||
|
Rechtslage.
|
||||||
|
|
||||||
```bash
|
## Aufbau
|
||||||
cd frontend
|
|
||||||
|
|
||||||
# Start development server (runs on http://stammtisch-hersbruck.de.localhost:31337)
|
- `frontend/src/routes/+page.svelte` — der Wegweiser
|
||||||
npm run dev
|
- `frontend/src/routes/imprint`, `privacy` — Rechtstexte, über
|
||||||
|
`LegalPage.svelte` gerahmt
|
||||||
|
- `frontend/src/routes/+layout.ts` — `prerender = true` für alles
|
||||||
|
- `frontend/static/stammtisch-hersbruck.svg` — das Wappen
|
||||||
|
|
||||||
# Build for production
|
`adapter-static`, kein Server im Betrieb. Gebaut wird mit Bun, ausgeliefert
|
||||||
npm run build
|
mit nginx (`frontend/Dockerfile`).
|
||||||
|
|
||||||
# Preview production build
|
## Sprache
|
||||||
npm run preview
|
|
||||||
|
|
||||||
# Type-check Svelte files
|
Oberflächentexte und Kommentare deutsch, Bezeichner und Routenpfade englisch
|
||||||
npm run check
|
(`/imprint`, nicht `/impressum`) — dieselbe Regel wie in Kurbeler.
|
||||||
|
|
||||||
# Type-check with watch mode
|
## Das Backend
|
||||||
npm run check:watch
|
|
||||||
|
|
||||||
# Generate TypeScript types from the schema migration in backend/pb_migrations
|
`backend/` ist Bestand und wird vom Wegweiser nicht benutzt. Nicht anfassen,
|
||||||
# (no running instance, no token, no network access needed)
|
ohne dass es jemand ausdrücklich will: Darin liegen die Daten des alten
|
||||||
npm run generate-pocketbase-types
|
Stammtisch-Deployments.
|
||||||
```
|
|
||||||
|
|
||||||
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/`, kommt per
|
|
||||||
`COPY` ins Image (kein Bind-Mount — der würde das Image-Verzeichnis
|
|
||||||
überdecken). Im Admin-UI erzeugte Migrationen liegen deshalb zunächst nur im
|
|
||||||
Container und müssen mit `docker compose cp` ins Repo geholt werden; siehe
|
|
||||||
`backend/README.md`.
|
|
||||||
- **Type-safe client**: The PocketBase client is typed using auto-generated types in `frontend/src/lib/types.d.ts`
|
|
||||||
- **Collections**: users, teams, team_invites, events, stages, riders, times,
|
|
||||||
trails, trail_versions, trail_flags, trail_markers, trail_comments,
|
|
||||||
event_participants, event_series
|
|
||||||
- **Regeln**: Jede Zugriffsregel, die `@request.auth.id` erwähnt, beginnt mit
|
|
||||||
`@request.auth.id != "" && (…)`. Ohne diesen Vorspann ist eine *leere*
|
|
||||||
Relation gleich dem leeren `@request.auth.id` einer anonymen Anfrage — ein
|
|
||||||
Team ohne Admins stünde damit offen im Netz. Siehe
|
|
||||||
`backend/pb_migrations/1754501700_rules_require_login.js`.
|
|
||||||
- **Hooks**: `backend/pb_hooks/` enthält serverseitiges JS für das, was keine
|
|
||||||
Collection-Regel abbilden kann — derzeit die öffentlichen Routen der
|
|
||||||
Einladungslinks. Jeder Route-Handler läuft in einer eigenen JS-Laufzeit;
|
|
||||||
gemeinsamer Code gehört in ein Modul und wird per
|
|
||||||
``require(`${__hooks}/…`)`` im Handler geholt. Details in
|
|
||||||
`backend/README.md`.
|
|
||||||
- **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 the schema migration in `backend/pb_migrations/`, run `npm run generate-pocketbase-types` to update `frontend/src/lib/types.d.ts`. The script reads the collections array straight out of the migration (`frontend/scripts/schema-to-json.mjs`) — schema and types come from the same versioned source and cannot drift apart. Commit the regenerated file.
|
|
||||||
|
|
||||||
**Async Data Loading**: Root layout (`src/routes/+layout.svelte`) shows pattern:
|
|
||||||
```svelte
|
|
||||||
{#await load()}
|
|
||||||
<!-- loading state -->
|
|
||||||
{:then _}
|
|
||||||
<!-- main content -->
|
|
||||||
{/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.
|
|
||||||
|
|
||||||
**Tooltips**: Never use the `title` attribute for tooltips — appwide. `<Button>`
|
|
||||||
takes a `tooltip="…"` prop (it also becomes the `aria-label`, so icon-only
|
|
||||||
buttons keep an accessible name); every other element uses the action:
|
|
||||||
`use:tooltip={{ content: '…' }}` from `app.svelte.ts`. Both render through
|
|
||||||
tippy.js, which the action initializes, updates and destroys.
|
|
||||||
|
|
||||||
### 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-node` — Deployment läuft über Coolify mit
|
|
||||||
`node build`. `adapter-auto` erkennt Coolify nicht und erzeugt kein `build/`.
|
|
||||||
- **Node**: `engines` verlangt `^20.19 || ^22.12 || >=24`; mit
|
|
||||||
`engine-strict=true` (`.npmrc`) bricht `npm ci` sonst mit `EBADENGINE` ab.
|
|
||||||
- **Deployment-Env**: `PUBLIC_PB_URL` muss dort gesetzt sein — sie wird zur
|
|
||||||
Buildzeit eingesetzt, und die lokale `.env` ist gitignored.
|
|
||||||
|
|
||||||
### File Structure Notes
|
|
||||||
|
|
||||||
- Routes are in `src/routes/` following SvelteKit conventions. **Routennamen
|
|
||||||
immer auf Englisch** (`/invite/[token]`, nicht `/einladung/[token]`) —
|
|
||||||
Oberflächentexte sind deutsch, Pfade nicht.
|
|
||||||
- Der Menüpunkt **Team** (`/dashboard/team`) führt Team und Kader an einer
|
|
||||||
Stelle: Kennzahlen, Teamdaten, Einladungslinks und die Fahrerliste samt der
|
|
||||||
Verwaltungsknöpfe der Teamleitung. Unter **Einstellungen** steht nur noch,
|
|
||||||
was einen selbst betrifft (Profil, Flag-Typen).
|
|
||||||
- **Ein Fahrer, ein Name, eine Richtung**: Ein Konto entsteht immer am Fahrer
|
|
||||||
(`riders.createLogin`), es gibt kein Verknüpfen bestehender Konten und kein
|
|
||||||
Lösen. Angezeigt wird ausschließlich der Fahrername — der Kontoname wäre
|
|
||||||
eine zweite Wahrheit, die niemand geradeziehen könnte, weil
|
|
||||||
`users.updateRule` nur den Kontoinhaber selbst ändern lässt.
|
|
||||||
- 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/`
|
|
||||||
|
|
|
||||||
76
README.md
76
README.md
|
|
@ -1,39 +1,69 @@
|
||||||
# stammtisch-hersbruck.de
|
# stammtisch-hersbruck.de
|
||||||
|
|
||||||
Zeitmessung und Verwaltung für Läufe des Stammtisch Hersbruck.
|
Ein Wegweiser. Mehr ist diese Adresse nicht mehr.
|
||||||
|
|
||||||
Das Repository enthält beide Teile der Anwendung:
|
Bis September 2026 lag hier eine Anwendung: Zeitnahme, Kader, Trails,
|
||||||
|
Renntage. Die heißt seit dem 08.09.2026 **Kurbeler**, liegt unter
|
||||||
|
[kurbeler.de](https://kurbeler.de) und wird in
|
||||||
|
`~/Dokumente/Projekte/kurbeler` fortgeführt — mit der vollständigen Historie
|
||||||
|
dieses Repos. Es war kein Neuanfang, sondern dieselbe Geschichte unter neuem
|
||||||
|
Namen, und der Stammtisch Hersbruck ist dort ein Team unter mehreren.
|
||||||
|
|
||||||
| Verzeichnis | Inhalt |
|
Was hier blieb, ist die Adresse. Wer ein altes Lesezeichen anklickt oder einen
|
||||||
|
Link aus einer Gruppe von damals, soll nicht ins Leere laufen, sondern in
|
||||||
|
einem Blick sehen, wo es weitergeht.
|
||||||
|
|
||||||
|
## Die Seite
|
||||||
|
|
||||||
|
Drei vorgerenderte HTML-Seiten in `frontend/`:
|
||||||
|
|
||||||
|
| Adresse | Inhalt |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [`frontend/`](frontend/) | SvelteKit-5-Anwendung (Svelte 5 Runes, Tailwind 4) |
|
| `/` | Wappen, ein Satz, ein Knopf nach kurbeler.de |
|
||||||
| [`backend/`](backend/) | PocketBase-Instanz — Dockerfile, Schema-Migrationen |
|
| `/imprint` | Impressum nach § 5 DDG |
|
||||||
|
| `/privacy` | Datenschutz — kurz, weil hier nichts passiert |
|
||||||
|
|
||||||
## Schnellstart
|
Kein Konto, keine Datenbank, kein Formular, keine Cookies, keine Schriften
|
||||||
|
oder Karten von fremden Servern. `adapter-static` erzeugt beim Bauen fertiges
|
||||||
|
HTML, nginx liefert es aus. Das ist auch der Grund, warum eine Link-Vorschau
|
||||||
|
in WhatsApp den Text sieht: Die führt kein JavaScript aus.
|
||||||
|
|
||||||
Frontend gegen die produktive Instanz:
|
## Befehle
|
||||||
|
|
||||||
cd frontend
|
Aus `frontend/`, Paketverwalter ist Bun:
|
||||||
cp .env.example .env # Werte eintragen
|
|
||||||
npm ci
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
Die App läuft dann auf http://stammtisch-hersbruck.de.localhost:31337
|
```bash
|
||||||
|
bun install
|
||||||
|
bun run dev # http://localhost:5173
|
||||||
|
bun run build # nach build/
|
||||||
|
bun run check # Typprüfung
|
||||||
|
```
|
||||||
|
|
||||||
Backend lokal (optional — der Default zeigt auf die produktive Instanz):
|
## Deployment
|
||||||
|
|
||||||
cd backend
|
Eigenes Bild, wie bei Kurbeler: `frontend/Dockerfile` baut mit `oven/bun`
|
||||||
cp .env.example .env # Werte eintragen
|
(Version gepinnt) und liefert mit `nginx:alpine` aus;
|
||||||
docker compose up -d --build
|
`frontend/docker-compose.yaml` ist das, was Coolify liest. Build Pack **Docker
|
||||||
|
Compose**, Base Directory `/frontend`, Compose-Datei `/docker-compose.yaml`.
|
||||||
|
|
||||||
Danach in `frontend/.env` `PUBLIC_PB_URL=http://127.0.0.1:8090` setzen.
|
> **Die Domain gehört dem Dienst, nicht der App.** Bei Build Pack Docker
|
||||||
|
> Compose hängt sie an `docker_compose_domains` und nicht mehr an `fqdn`.
|
||||||
|
> Bleibt das Feld leer, läuft der Container gesund und Traefik hat trotzdem
|
||||||
|
> keine Route — von außen sieht das aus wie ein kaputter Build. Derselbe
|
||||||
|
> Stolperstein wie bei Kurbeler, dort steht er ausführlicher.
|
||||||
|
|
||||||
Details stehen in [`backend/README.md`](backend/README.md).
|
## Das Backend
|
||||||
|
|
||||||
## PocketBase
|
`backend/` liegt noch im Repo und die PocketBase-Instanz hinter
|
||||||
|
`api.stammtisch-hersbruck.de` läuft weiter. Der Wegweiser spricht sie nicht an
|
||||||
|
— er spricht mit gar nichts. Ob die Instanz bleibt, ist offen; abgeschaltet
|
||||||
|
wird sie nicht nebenbei, weil die Daten des alten Stammtisch-Deployments darin
|
||||||
|
liegen.
|
||||||
|
|
||||||
Produktiv: https://api.stammtisch-hersbruck.de
|
## Was sonst noch im Repo liegt
|
||||||
|
|
||||||
Welche Instanz das Frontend anspricht, entscheidet `PUBLIC_PB_URL` in
|
Der Zweig `abgebrochener-umbau` hält den Stand fest, in dem hier mit der
|
||||||
`frontend/.env`. Das Schema ist als Migration in `backend/pb_migrations/`
|
Umbenennung zu Kurbeler angefangen wurde, bevor sie ins eigene Repo zog. Er
|
||||||
versioniert.
|
wird nicht weiterentwickelt und ist nur da, damit die Geschichte nachlesbar
|
||||||
|
bleibt — unter anderem enthält er das Stammtisch-Wappen, das nur dort je
|
||||||
|
entstanden ist.
|
||||||
|
|
|
||||||
0
TODO.md
0
TODO.md
BIN
UML_basic.png
BIN
UML_basic.png
Binary file not shown.
|
Before Width: | Height: | Size: 129 KiB |
10
frontend/.dockerignore
Normal file
10
frontend/.dockerignore
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Was nicht ins Bild gehört.
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
node_modules
|
||||||
|
build
|
||||||
|
.svelte-kit
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.log
|
||||||
27
frontend/Dockerfile
Normal file
27
frontend/Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Der Wegweiser als Bild: mit Bun gebaut, von nginx ausgeliefert.
|
||||||
|
#
|
||||||
|
# Kein Node, kein Bun im Betrieb. Übrig sind drei vorgerenderte HTML-Seiten,
|
||||||
|
# ein Wappen und etwas CSS — dafür einen JavaScript-Prozess laufen zu lassen,
|
||||||
|
# der auf Anfragen wartet, um jedes Mal dieselbe Datei herauszugeben, wäre
|
||||||
|
# Beschäftigung ohne Arbeit. nginx tut genau das und sonst nichts.
|
||||||
|
|
||||||
|
ARG BUN_VERSION=1.3.11
|
||||||
|
|
||||||
|
# --- Bauen ------------------------------------------------------------------
|
||||||
|
FROM oven/bun:${BUN_VERSION}-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# --- Ausliefern -------------------------------------------------------------
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/build /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
326
frontend/bun.lock
Normal file
326
frontend/bun.lock
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "stammtisch-hersbruck.de",
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
|
"@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",
|
||||||
|
"svelte": "^5.39.5",
|
||||||
|
"svelte-check": "^4.3.2",
|
||||||
|
"tailwindcss": "^4.1.13",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"vite": "^7.1.7",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="],
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
||||||
|
|
||||||
|
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.1", "", { "os": "android", "cpu": "arm" }, "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.1", "", { "os": "android", "cpu": "arm64" }, "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.1", "", { "os": "none", "cpu": "arm64" }, "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="],
|
||||||
|
|
||||||
|
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="],
|
||||||
|
|
||||||
|
"@sveltejs/kit": ["@sveltejs/kit@2.70.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "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 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg=="],
|
||||||
|
|
||||||
|
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
|
||||||
|
|
||||||
|
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="],
|
||||||
|
|
||||||
|
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="],
|
||||||
|
|
||||||
|
"@tailwindcss/forms": ["@tailwindcss/forms@0.5.11", "", { "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" } }, "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.20", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" } }, "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw=="],
|
||||||
|
|
||||||
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="],
|
||||||
|
|
||||||
|
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||||
|
|
||||||
|
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
||||||
|
|
||||||
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||||
|
|
||||||
|
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||||
|
|
||||||
|
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"devalue": ["devalue@5.9.2", "", {}, "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w=="],
|
||||||
|
|
||||||
|
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="],
|
||||||
|
|
||||||
|
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||||
|
|
||||||
|
"esrap": ["esrap@2.3.7", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-n2nf7fZR3c9yXf0BPEuHuXqT+KW0SJVj4cN5FMEkpCZ3scLjOQWpiccyCxVzCC2q1wubTghuEGzngJY/7Ah0Ow=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||||
|
|
||||||
|
"jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
|
||||||
|
|
||||||
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||||
|
|
||||||
|
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="],
|
||||||
|
|
||||||
|
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||||
|
|
||||||
|
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="],
|
||||||
|
|
||||||
|
"obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="],
|
||||||
|
|
||||||
|
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.63.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.1", "@rollup/rollup-android-arm64": "4.63.1", "@rollup/rollup-darwin-arm64": "4.63.1", "@rollup/rollup-darwin-x64": "4.63.1", "@rollup/rollup-freebsd-arm64": "4.63.1", "@rollup/rollup-freebsd-x64": "4.63.1", "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", "@rollup/rollup-linux-arm-musleabihf": "4.63.1", "@rollup/rollup-linux-arm64-gnu": "4.63.1", "@rollup/rollup-linux-arm64-musl": "4.63.1", "@rollup/rollup-linux-loong64-gnu": "4.63.1", "@rollup/rollup-linux-loong64-musl": "4.63.1", "@rollup/rollup-linux-ppc64-gnu": "4.63.1", "@rollup/rollup-linux-ppc64-musl": "4.63.1", "@rollup/rollup-linux-riscv64-gnu": "4.63.1", "@rollup/rollup-linux-riscv64-musl": "4.63.1", "@rollup/rollup-linux-s390x-gnu": "4.63.1", "@rollup/rollup-linux-x64-gnu": "4.63.1", "@rollup/rollup-linux-x64-musl": "4.63.1", "@rollup/rollup-openbsd-x64": "4.63.1", "@rollup/rollup-openharmony-arm64": "4.63.1", "@rollup/rollup-win32-arm64-msvc": "4.63.1", "@rollup/rollup-win32-ia32-msvc": "4.63.1", "@rollup/rollup-win32-x64-gnu": "4.63.1", "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg=="],
|
||||||
|
|
||||||
|
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="],
|
||||||
|
|
||||||
|
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"svelte": ["svelte@5.57.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg=="],
|
||||||
|
|
||||||
|
"svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||||
|
|
||||||
|
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||||
|
|
||||||
|
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||||
|
|
||||||
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||||
|
|
||||||
|
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
||||||
|
|
||||||
|
"zimmerframe": ["zimmerframe@1.1.5", "", {}, "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
17
frontend/docker-compose.yaml
Normal file
17
frontend/docker-compose.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
BUN_VERSION: ${BUN_VERSION:-1.3.11}
|
||||||
|
restart: unless-stopped
|
||||||
|
# `expose`, nicht `ports`: Der Port gehört ins Docker-Netz. Coolify stellt
|
||||||
|
# die Seite über Traefik zu.
|
||||||
|
expose:
|
||||||
|
- "80"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
40
frontend/nginx.conf
Normal file
40
frontend/nginx.conf
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Ein Server, ein Verzeichnis, drei Seiten.
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Vorkomprimierte Fassungen liegen dank `precompress` im Build daneben
|
||||||
|
# (.gz). Ohne diese Zeile ignoriert nginx sie und packt jede Antwort neu.
|
||||||
|
#
|
||||||
|
# Sie steht hier im Server-Block und nicht per `sed` in der
|
||||||
|
# `nginx.conf` des Bildes: Dort war die erste Zeile, die mit `}` beginnt,
|
||||||
|
# das Ende von `events { … }`. `gzip_static` ist dort keine gültige
|
||||||
|
# Anweisung, nginx verweigerte den Start, und der Container ging sofort
|
||||||
|
# wieder aus — „exited:unhealthy", ohne dass im Deploy-Log etwas
|
||||||
|
# danebenstand.
|
||||||
|
gzip_static on;
|
||||||
|
|
||||||
|
# Die Seiten sind beim Bauen entstanden und ändern sich nur mit dem
|
||||||
|
# nächsten Deployment. Das HTML selbst bleibt trotzdem ungecacht: Sonst
|
||||||
|
# zeigt ein Browser den Wegweiser von vorgestern, wenn sich der Text
|
||||||
|
# ändert.
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ $uri.html =404;
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alles unter /_app/immutable trägt seinen Hash im Namen — das darf ein
|
||||||
|
# Jahr im Cache liegen.
|
||||||
|
location /_app/immutable/ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /stammtisch-hersbruck.svg {
|
||||||
|
add_header Cache-Control "public, max-age=604800";
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 404 /index.html;
|
||||||
|
}
|
||||||
6341
frontend/package-lock.json
generated
6341
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -12,41 +12,21 @@
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepare": "svelte-kit sync || echo ''",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||||
"test": "vitest run",
|
|
||||||
"test:watch": "vitest",
|
|
||||||
"generate-pocketbase-types": "node scripts/schema-to-json.mjs > .schema.tmp.json && pocketbase-typegen --json .schema.tmp.json --out ./src/lib/types.d.ts && rm -f .schema.tmp.json"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@internationalized/date": "^3.12.1",
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
"@lucide/svelte": "^1.42.0",
|
|
||||||
"@sveltejs/adapter-node": "^5.5.7",
|
|
||||||
"@sveltejs/kit": "^2.43.2",
|
"@sveltejs/kit": "^2.43.2",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/typography": "^0.5.18",
|
"@tailwindcss/typography": "^0.5.18",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"bits-ui": "^2.18.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"jsdom": "^29.1.1",
|
|
||||||
"pocketbase-typegen": "^1.3.1",
|
|
||||||
"svelte": "^5.39.5",
|
"svelte": "^5.39.5",
|
||||||
"svelte-check": "^4.3.2",
|
"svelte-check": "^4.3.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
|
||||||
"tailwind-variants": "^3.3.1",
|
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.7"
|
||||||
"vitest": "^4.1.10"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {}
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"dompurify": "^3.4.13",
|
|
||||||
"lucide-svelte": "^0.503.0",
|
|
||||||
"maplibre-gl": "^6.2.0",
|
|
||||||
"mode-watcher": "^1.1.0",
|
|
||||||
"pocketbase": "^0.26.0",
|
|
||||||
"tippy.js": "^6.3.7"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
/**
|
|
||||||
* Extrahiert das Collections-Array aus der PocketBase-Migration und schreibt es
|
|
||||||
* als JSON auf stdout — in der Form, die `pocketbase-typegen --json` erwartet.
|
|
||||||
*
|
|
||||||
* Damit entstehen die TypeScript-Typen aus dem versionierten Schema im Repo,
|
|
||||||
* ohne laufende Instanz, ohne Token und ohne Netzwerkzugriff.
|
|
||||||
*
|
|
||||||
* Aufruf über npm run generate-pocketbase-types.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { readFileSync, readdirSync } from 'node:fs'
|
|
||||||
import { join } from 'node:path'
|
|
||||||
|
|
||||||
const MIGRATIONS_DIR = new URL('../../backend/pb_migrations/', import.meta.url).pathname
|
|
||||||
|
|
||||||
// Die Snapshot-Migration ist die einzige, die ein vollständiges
|
|
||||||
// `const collections = [...]` enthält. Bei mehreren Dateien gewinnt die
|
|
||||||
// jüngste — Migrationen sind nach Zeitstempel benannt.
|
|
||||||
const candidates = readdirSync(MIGRATIONS_DIR)
|
|
||||||
.filter((f) => f.endsWith('.js'))
|
|
||||||
.sort()
|
|
||||||
.reverse()
|
|
||||||
|
|
||||||
const MARKER = 'const collections = '
|
|
||||||
|
|
||||||
for (const file of candidates) {
|
|
||||||
const source = readFileSync(join(MIGRATIONS_DIR, file), 'utf8')
|
|
||||||
const start = source.indexOf(MARKER)
|
|
||||||
if (start === -1) continue
|
|
||||||
|
|
||||||
// Vom öffnenden [ bis zur zugehörigen schließenden Klammer zählen —
|
|
||||||
// ein Regex käme an den verschachtelten Arrays der Feldoptionen nicht vorbei.
|
|
||||||
const from = start + MARKER.length
|
|
||||||
let depth = 0
|
|
||||||
|
|
||||||
for (let i = from; i < source.length; i++) {
|
|
||||||
if (source[i] === '[') depth++
|
|
||||||
else if (source[i] === ']') {
|
|
||||||
depth--
|
|
||||||
if (depth === 0) {
|
|
||||||
process.stdout.write(source.slice(from, i + 1))
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(
|
|
||||||
`Keine Migration mit "${MARKER.trim()}" in ${MIGRATIONS_DIR} gefunden.\n` +
|
|
||||||
`Ohne sie lassen sich die Typen nicht aus dem Repo erzeugen.`
|
|
||||||
)
|
|
||||||
process.exit(1)
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<!-- Installierbar, damit die Zeitnahme an der Strecke ein Funkloch
|
<link rel="icon" href="/stammtisch-hersbruck.svg" />
|
||||||
auch dann uebersteht, wenn jemand die App neu startet. -->
|
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
|
||||||
<meta name="theme-color" content="#eab308" />
|
<meta name="theme-color" content="#eab308" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,78 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Bild eines Nutzers — und wenn keins hinterlegt ist, ein erzeugtes aus
|
|
||||||
* Initialen und einer Farbe.
|
|
||||||
*
|
|
||||||
* Die Farbe kommt aus einer Prüfsumme über die ID: Derselbe Mensch bekommt
|
|
||||||
* damit überall dasselbe Bild, ohne dass es irgendwo gespeichert werden
|
|
||||||
* müsste. Bewusst kein externer Avatar-Dienst — der würde bei jedem
|
|
||||||
* Seitenaufruf verraten, wer hier gerade unterwegs ist.
|
|
||||||
*/
|
|
||||||
import { getFileURL } from '$lib/stores/pocketbase.svelte'
|
|
||||||
|
|
||||||
let {
|
|
||||||
user = null,
|
|
||||||
size = 32,
|
|
||||||
class: className = '',
|
|
||||||
}: {
|
|
||||||
user?: { id?: string; name?: string; email?: string; avatar?: string } | null
|
|
||||||
size?: number
|
|
||||||
class?: string
|
|
||||||
} = $props()
|
|
||||||
|
|
||||||
const label = $derived(user?.name?.trim() || user?.email?.trim() || '')
|
|
||||||
|
|
||||||
/** Erste Buchstaben der ersten beiden Wörter, sonst der erste des Namens. */
|
|
||||||
const initials = $derived.by(() => {
|
|
||||||
if (!label) return '?'
|
|
||||||
|
|
||||||
const words = label.split(/[\s.@_-]+/).filter(Boolean)
|
|
||||||
if (words.length === 0) return '?'
|
|
||||||
if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
|
|
||||||
|
|
||||||
return (words[0][0] + words[1][0]).toUpperCase()
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Farbton aus der ID, hilfsweise aus dem Namen. Sättigung und Helligkeit
|
|
||||||
* bleiben fest, damit weiße Schrift auf jedem Ton lesbar bleibt — auch im
|
|
||||||
* Dunkelmodus, wo eine zufällige Helligkeit schnell blendet.
|
|
||||||
*/
|
|
||||||
const hue = $derived.by(() => {
|
|
||||||
const seed = user?.id || label || '?'
|
|
||||||
let h = 0
|
|
||||||
for (let i = 0; i < seed.length; i++) {
|
|
||||||
h = (h * 31 + seed.charCodeAt(i)) % 360
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
})
|
|
||||||
|
|
||||||
const src = $derived(
|
|
||||||
user?.avatar
|
|
||||||
? getFileURL(user as object, user.avatar, { thumb: `${size * 2}x${size * 2}` })
|
|
||||||
: null,
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if src}
|
|
||||||
<img
|
|
||||||
{src}
|
|
||||||
alt={label}
|
|
||||||
width={size}
|
|
||||||
height={size}
|
|
||||||
class="shrink-0 rounded-full object-cover {className}"
|
|
||||||
style:width="{size}px"
|
|
||||||
style:height="{size}px"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
class="grid shrink-0 place-items-center rounded-full font-medium text-white select-none {className}"
|
|
||||||
style:width="{size}px"
|
|
||||||
style:height="{size}px"
|
|
||||||
style:background="hsl({hue} 55% 42%)"
|
|
||||||
style:font-size="{Math.round(size * 0.4)}px"
|
|
||||||
>
|
|
||||||
{initials}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Uhrenabgleich vor dem Event — der Dialog, den jeder aufruft, der an
|
|
||||||
* diesem Tag Zeiten nimmt.
|
|
||||||
*
|
|
||||||
* Warum das nötig ist: Zeiten entstehen auf den Geräten an der Strecke,
|
|
||||||
* damit im Funkloch überhaupt gestoppt werden kann. Startet aber einer am
|
|
||||||
* Start und stoppt ein anderer im Ziel, steckt der Versatz beider
|
|
||||||
* Telefonuhren in der gemessenen Dauer. Der Abgleich holt beide auf
|
|
||||||
* dieselbe Skala — die des Servers.
|
|
||||||
*
|
|
||||||
* Der Versatz gehört dem Gerät und liegt im localStorage; er überlebt
|
|
||||||
* Seitenwechsel und Abmeldung, aber nicht das Löschen der Websitedaten.
|
|
||||||
*/
|
|
||||||
import { clockSync, ACCURACY_LIMIT_MS, STALE_AFTER_MS } from '$lib/stores/clockSync.svelte'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import * as Dialog from '@/components/ui/dialog'
|
|
||||||
import { CircleCheck, RefreshCw, TriangleAlert } from 'lucide-svelte'
|
|
||||||
|
|
||||||
let { open = $bindable(false) }: { open?: boolean } = $props()
|
|
||||||
|
|
||||||
/** Millisekunden lesbar: unter einer Sekunde in ms, darüber in Sekunden. */
|
|
||||||
function ms(value: number): string {
|
|
||||||
const abs = Math.abs(value)
|
|
||||||
if (abs < 1000) return `${Math.round(value)} ms`
|
|
||||||
return `${(value / 1000).toFixed(value < 10000 ? 2 : 1)} s`
|
|
||||||
}
|
|
||||||
|
|
||||||
function age(value: number): string {
|
|
||||||
const min = Math.round(value / 60000)
|
|
||||||
if (min < 1) return 'gerade eben'
|
|
||||||
if (min < 60) return `vor ${min} min`
|
|
||||||
const h = Math.round(min / 60)
|
|
||||||
return `vor ${h} h`
|
|
||||||
}
|
|
||||||
|
|
||||||
const good = $derived(clockSync.trustworthy)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Dialog.Root bind:open>
|
|
||||||
<Dialog.Content>
|
|
||||||
<Dialog.Header>
|
|
||||||
<Dialog.Title>Uhr abgleichen</Dialog.Title>
|
|
||||||
<Dialog.Description>
|
|
||||||
Gestartet und gestoppt wird auf den Geräten an der Strecke. Damit die
|
|
||||||
Zeit stimmt, auch wenn Start und Ziel verschiedene Telefone sind,
|
|
||||||
stellt jedes Gerät seine Uhr einmal gegen den Server.
|
|
||||||
</Dialog.Description>
|
|
||||||
</Dialog.Header>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div
|
|
||||||
class="flex items-start gap-3 rounded-lg border p-3"
|
|
||||||
class:border-primary={good}
|
|
||||||
class:bg-primary={false}
|
|
||||||
>
|
|
||||||
{#if good}
|
|
||||||
<CircleCheck class="mt-0.5 size-5 shrink-0 text-primary" />
|
|
||||||
{:else}
|
|
||||||
<TriangleAlert class="mt-0.5 size-5 shrink-0 text-amber-600" />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="min-w-0 flex-1 space-y-1">
|
|
||||||
{#if !clockSync.measured}
|
|
||||||
<p class="text-sm font-medium">Noch nicht abgeglichen</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Bis dahin gilt die Uhr dieses Geräts, wie sie ist.
|
|
||||||
</p>
|
|
||||||
{:else}
|
|
||||||
<p class="text-sm font-medium">
|
|
||||||
Dieses Gerät geht
|
|
||||||
{#if Math.abs(clockSync.offset) < 50}
|
|
||||||
richtig
|
|
||||||
{:else if clockSync.offset < 0}
|
|
||||||
{ms(-clockSync.offset)} vor
|
|
||||||
{:else}
|
|
||||||
{ms(clockSync.offset)} nach
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Gemessen {age(clockSync.age ?? 0)}, auf ±{ms(clockSync.accuracy ?? 0)} genau.
|
|
||||||
Die Abweichung wird beim Stoppen herausgerechnet.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{#if clockSync.stale}
|
|
||||||
<p class="text-sm text-amber-600">
|
|
||||||
Die Messung ist älter als
|
|
||||||
{Math.round(STALE_AFTER_MS / 3600000)} Stunden — vor dem Start
|
|
||||||
noch einmal abgleichen.
|
|
||||||
</p>
|
|
||||||
{:else if (clockSync.accuracy ?? 0) > ACCURACY_LIMIT_MS}
|
|
||||||
<p class="text-sm text-amber-600">
|
|
||||||
Die Verbindung war zu langsam für eine genaue Messung. Mit
|
|
||||||
besserem Empfang noch einmal versuchen.
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if clockSync.error}
|
|
||||||
<p class="text-sm text-destructive">{clockSync.error}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
Der Abgleich braucht Netz und dauert eine Sekunde. Danach zählt die
|
|
||||||
Zeitnahme auch im Funkloch richtig weiter — übertragen wird, sobald
|
|
||||||
wieder Empfang da ist.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Dialog.Footer>
|
|
||||||
<Button variant="ghost" onclick={() => (open = false)}>Schließen</Button>
|
|
||||||
<Button onclick={() => clockSync.measure()} disabled={clockSync.busy}>
|
|
||||||
<RefreshCw class="size-4 mr-2 {clockSync.busy ? 'animate-spin' : ''}" />
|
|
||||||
{clockSync.busy ? 'Wird gemessen …' : 'Jetzt abgleichen'}
|
|
||||||
</Button>
|
|
||||||
</Dialog.Footer>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { app } from '$lib/stores/app.svelte'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import * as Dialog from '@/components/ui/dialog'
|
|
||||||
|
|
||||||
let open = $derived(app.confirm.show)
|
|
||||||
|
|
||||||
function onOpenChange(v: boolean) {
|
|
||||||
if (!v) {
|
|
||||||
// ESC oder Backdrop-Click → wie "no"
|
|
||||||
app.confirm.no()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Dialog.Root bind:open={() => open, onOpenChange}>
|
|
||||||
<Dialog.Content>
|
|
||||||
<Dialog.Header>
|
|
||||||
<Dialog.Title>{app.confirm.title}</Dialog.Title>
|
|
||||||
{#if app.confirm.text}
|
|
||||||
<Dialog.Description>{app.confirm.text}</Dialog.Description>
|
|
||||||
{/if}
|
|
||||||
</Dialog.Header>
|
|
||||||
|
|
||||||
{#if app.confirm.error}
|
|
||||||
<div class="rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive">
|
|
||||||
{typeof app.confirm.error === 'object' ? (app.confirm.error as any).message ?? 'Fehler' : 'Fehler'}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Dialog.Footer>
|
|
||||||
<Button variant="ghost" onclick={() => app.confirm.no()}>Abbrechen</Button>
|
|
||||||
<Button variant="destructive" onclick={() => app.confirm.yes()}>Bestätigen</Button>
|
|
||||||
</Dialog.Footer>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Dialog „Neues Team". Liegt als eigene Komponente vor, weil es zwei
|
|
||||||
* Einstiege gibt — den Teamwechsler im Kopf und die Teamseite — und ein
|
|
||||||
* neues Team an beiden Stellen dasselbe bedeuten muss: Team anlegen, den
|
|
||||||
* Standardsatz Flag-Typen dazu, und ab dann ist es das aktive Team.
|
|
||||||
*
|
|
||||||
* Ein Team ohne Flag-Typen kann zwar Marker aufnehmen, aber nichts
|
|
||||||
* einordnen. Deshalb kommt der Standardsatz gleich mit — änderbar und,
|
|
||||||
* wie alle Flag-Typen, allein Sache dieses Teams.
|
|
||||||
*/
|
|
||||||
import { getTeamContext } from '$lib/stores/teams.svelte'
|
|
||||||
import { getTrailFlagContext } from '$lib/stores/trailFlags.svelte'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import * as Dialog from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
|
|
||||||
let { open = $bindable(false) }: { open?: boolean } = $props()
|
|
||||||
|
|
||||||
const teams = getTeamContext()
|
|
||||||
const flags = getTrailFlagContext()
|
|
||||||
|
|
||||||
let name = $state('')
|
|
||||||
let error = $state<string | null>(null)
|
|
||||||
let busy = $state(false)
|
|
||||||
|
|
||||||
async function submit() {
|
|
||||||
if (!name.trim() || busy) return
|
|
||||||
|
|
||||||
busy = true
|
|
||||||
error = null
|
|
||||||
try {
|
|
||||||
const created = await teams.create(name.trim())
|
|
||||||
|
|
||||||
try {
|
|
||||||
await flags.seedDefaults(created.id)
|
|
||||||
} catch (e: any) {
|
|
||||||
error =
|
|
||||||
'Das Team wurde angelegt, die Standard-Flag-Typen aber nicht: ' +
|
|
||||||
(e.message ?? 'unbekannter Fehler') +
|
|
||||||
' Du kannst sie unter „Flag-Typen" nachholen.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
open = false
|
|
||||||
name = ''
|
|
||||||
} catch (e: any) {
|
|
||||||
error = e.message ?? 'Das Team konnte nicht angelegt werden.'
|
|
||||||
} finally {
|
|
||||||
busy = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Dialog.Root bind:open>
|
|
||||||
<Dialog.Content>
|
|
||||||
<Dialog.Header>
|
|
||||||
<Dialog.Title>Neues Team</Dialog.Title>
|
|
||||||
<Dialog.Description>Du wirst automatisch Owner.</Dialog.Description>
|
|
||||||
</Dialog.Header>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="team-name">Name</Label>
|
|
||||||
<Input id="team-name" bind:value={name} placeholder="Mein Team" onkeydown={(e) => e.key === 'Enter' && submit()} />
|
|
||||||
</div>
|
|
||||||
{#if error}
|
|
||||||
<p class="text-sm text-destructive">{error}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<Dialog.Footer>
|
|
||||||
<Button variant="ghost" onclick={() => (open = false)}>Abbrechen</Button>
|
|
||||||
<Button onclick={submit} disabled={!name.trim() || busy}>Erstellen</Button>
|
|
||||||
</Dialog.Footer>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Höhenprofil als SVG-Fläche. Die hervorgehobene Stelle kommt als Prop
|
|
||||||
* herein und geht beim Überfahren als Callback hinaus — die Elternseite
|
|
||||||
* hält den Wert und reicht ihn ebenso an die Karte weiter. So zeigen
|
|
||||||
* Karte und Profil immer dieselbe Stelle, egal wo gerade gezeigt wird.
|
|
||||||
* Gekoppelt wird über die Distanz, nicht über den Index: Die Karte zeigt
|
|
||||||
* eine vereinfachte Punktreihe, das Profil alle Originalpunkte.
|
|
||||||
*/
|
|
||||||
let {
|
|
||||||
elevation,
|
|
||||||
onhover,
|
|
||||||
onselect,
|
|
||||||
highlightDistance = null,
|
|
||||||
height = '120px',
|
|
||||||
}: {
|
|
||||||
elevation: { d: number; ele: number }[]
|
|
||||||
onhover?: (distance: number | null) => void
|
|
||||||
/**
|
|
||||||
* Klick auf eine Stelle des Profils. Damit lässt sich ein Marker auch
|
|
||||||
* über die Höhe finden — im steilen Stück, statt die Linie auf der
|
|
||||||
* Karte abzusuchen.
|
|
||||||
*/
|
|
||||||
onselect?: (distance: number) => void
|
|
||||||
highlightDistance?: number | null
|
|
||||||
height?: string
|
|
||||||
} = $props()
|
|
||||||
|
|
||||||
// Internes Koordinatensystem; das SVG skaliert über viewBox mit.
|
|
||||||
const W = 1000
|
|
||||||
const H = 200
|
|
||||||
|
|
||||||
const stats = $derived.by(() => {
|
|
||||||
if (elevation.length < 2) return null
|
|
||||||
|
|
||||||
const eles = elevation.map((p) => p.ele)
|
|
||||||
const min = Math.min(...eles)
|
|
||||||
const max = Math.max(...eles)
|
|
||||||
const totalD = elevation[elevation.length - 1].d || 1
|
|
||||||
// Bei flachen Strecken wäre die Spanne 0 und die Division ungültig.
|
|
||||||
const span = max - min || 1
|
|
||||||
|
|
||||||
return { min, max, totalD, span }
|
|
||||||
})
|
|
||||||
|
|
||||||
const path = $derived.by(() => {
|
|
||||||
if (!stats) return ''
|
|
||||||
const { min, totalD, span } = stats
|
|
||||||
|
|
||||||
const pts = elevation.map((p) => {
|
|
||||||
const x = (p.d / totalD) * W
|
|
||||||
const y = H - ((p.ele - min) / span) * H
|
|
||||||
return `${x.toFixed(1)},${y.toFixed(1)}`
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fläche schließen: runter zur Grundlinie und zurück zum Anfang
|
|
||||||
return `M ${pts.join(' L ')} L ${W},${H} L 0,${H} Z`
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Der Punkt zur hervorgehobenen Distanz. Gesucht wird binär, weil das
|
|
||||||
* Profil alle Originalpunkte enthält — bei jeder Mausbewegung linear
|
|
||||||
* durch mehrere tausend Einträge zu laufen wäre unnötig teuer.
|
|
||||||
*/
|
|
||||||
const hover = $derived.by(() => {
|
|
||||||
if (!stats || highlightDistance == null) return null
|
|
||||||
|
|
||||||
let lo = 0
|
|
||||||
let hi = elevation.length - 1
|
|
||||||
while (hi - lo > 1) {
|
|
||||||
const mid = (lo + hi) >> 1
|
|
||||||
if (elevation[mid].d <= highlightDistance) lo = mid
|
|
||||||
else hi = mid
|
|
||||||
}
|
|
||||||
|
|
||||||
const p =
|
|
||||||
Math.abs(elevation[lo].d - highlightDistance) <=
|
|
||||||
Math.abs(elevation[hi].d - highlightDistance)
|
|
||||||
? elevation[lo]
|
|
||||||
: elevation[hi]
|
|
||||||
|
|
||||||
return {
|
|
||||||
ele: p.ele,
|
|
||||||
d: p.d,
|
|
||||||
// In Prozent, damit die HTML-Overlays dieselbe Stelle treffen wie
|
|
||||||
// das über viewBox skalierte SVG.
|
|
||||||
left: (p.d / stats.totalD) * 100,
|
|
||||||
top: (1 - (p.ele - stats.min) / stats.span) * 100,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Distanz an der Mausposition; null, wenn es keine Punkte gibt. */
|
|
||||||
function distanceAt(e: MouseEvent): number | null {
|
|
||||||
if (!elevation.length) return null
|
|
||||||
|
|
||||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
||||||
const ratio = (e.clientX - rect.left) / rect.width
|
|
||||||
const idx = Math.round(ratio * (elevation.length - 1))
|
|
||||||
const clamped = Math.max(0, Math.min(elevation.length - 1, idx))
|
|
||||||
|
|
||||||
return elevation[clamped].d
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMove(e: MouseEvent) {
|
|
||||||
const d = distanceAt(e)
|
|
||||||
if (d !== null) onhover?.(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClick(e: MouseEvent) {
|
|
||||||
if (!onselect) return
|
|
||||||
|
|
||||||
const d = distanceAt(e)
|
|
||||||
if (d !== null) onselect(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLeave() {
|
|
||||||
onhover?.(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tastaturweg zum Setzen: Mit den Pfeiltasten die Stelle wählen, mit
|
|
||||||
* Enter oder Leertaste bestätigen. Ohne das wäre das Profil ein reines
|
|
||||||
* Mausziel.
|
|
||||||
*/
|
|
||||||
function handleKey(e: KeyboardEvent) {
|
|
||||||
if (!onselect || !stats) return
|
|
||||||
|
|
||||||
const total = stats.totalD
|
|
||||||
const step = total / 50
|
|
||||||
const current = highlightDistance ?? 0
|
|
||||||
|
|
||||||
if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
|
|
||||||
e.preventDefault()
|
|
||||||
const next = e.key === 'ArrowRight' ? current + step : current - step
|
|
||||||
onhover?.(Math.max(0, Math.min(total, next)))
|
|
||||||
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault()
|
|
||||||
if (highlightDistance != null) onselect(highlightDistance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if stats}
|
|
||||||
<div class="relative w-full" style:height>
|
|
||||||
<!--
|
|
||||||
Die Fläche ist ein echter Knopf: Sie reagiert auf Zeiger und
|
|
||||||
Tastatur, und ein <svg> mit Rollen und tabindex bleibt für
|
|
||||||
Hilfsmittel ein Bild mit angeklebtem Verhalten.
|
|
||||||
-->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="block w-full h-full border-0 bg-transparent p-0"
|
|
||||||
class:cursor-pointer={!!onselect}
|
|
||||||
aria-label={onselect
|
|
||||||
? 'Höhenprofil – hier klicken setzt einen Marker an dieser Stelle'
|
|
||||||
: 'Höhenprofil'}
|
|
||||||
onmousemove={handleMove}
|
|
||||||
onmouseleave={handleLeave}
|
|
||||||
onclick={handleClick}
|
|
||||||
onkeydown={handleKey}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 {W} {H}"
|
|
||||||
preserveAspectRatio="none"
|
|
||||||
class="w-full h-full"
|
|
||||||
role="img"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<!-- Weiss zu 90 %, die Flaeche darunter blass. -->
|
|
||||||
<path
|
|
||||||
d={path}
|
|
||||||
fill="rgb(255 255 255 / 0.2)"
|
|
||||||
stroke="rgb(255 255 255 / 0.9)"
|
|
||||||
stroke-width="2"
|
|
||||||
vector-effect="non-scaling-stroke"
|
|
||||||
/>
|
|
||||||
{#if hover}
|
|
||||||
<line
|
|
||||||
x1={(hover.left / 100) * W}
|
|
||||||
y1="0"
|
|
||||||
x2={(hover.left / 100) * W}
|
|
||||||
y2={H}
|
|
||||||
stroke="rgb(255 255 255 / 0.9)"
|
|
||||||
stroke-width="1"
|
|
||||||
vector-effect="non-scaling-stroke"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if hover}
|
|
||||||
<!--
|
|
||||||
Punkt und Beschriftung liegen als HTML über dem SVG: dessen
|
|
||||||
viewBox wird ungleichmäßig gestreckt, ein Kreis darin würde
|
|
||||||
zur Ellipse und Text mitverzerrt.
|
|
||||||
-->
|
|
||||||
<span
|
|
||||||
class="pointer-events-none absolute size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white ring-2 ring-background"
|
|
||||||
style:left="{hover.left}%"
|
|
||||||
style:top="{hover.top}%"
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
class="pointer-events-none absolute top-0 -translate-x-1/2 rounded bg-background/90 px-1.5 py-0.5 text-xs font-medium whitespace-nowrap shadow-sm"
|
|
||||||
style:left="clamp(2.5rem, {hover.left}%, calc(100% - 2.5rem))"
|
|
||||||
>
|
|
||||||
{Math.round(hover.ele)} m · {(hover.d / 1000).toFixed(2)} km
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between text-xs text-muted-foreground mt-1">
|
|
||||||
<span>{Math.round(stats.min)} m</span>
|
|
||||||
<span>{(stats.totalD / 1000).toFixed(1)} km</span>
|
|
||||||
<span>{Math.round(stats.max)} m</span>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<p class="text-sm text-muted-foreground">Keine Höhendaten vorhanden.</p>
|
|
||||||
{/if}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Rendert das Icon eines Flag-Typs. Der Name kommt aus der Datenbank und
|
|
||||||
* ist Freitext — solange geladen wird und bei unbekannten Namen steht
|
|
||||||
* ersatzweise die Flagge da, statt eine Lücke zu lassen.
|
|
||||||
*/
|
|
||||||
import { Flag } from 'lucide-svelte'
|
|
||||||
import { iconLoader } from '$lib/flagIcons'
|
|
||||||
|
|
||||||
let {
|
|
||||||
name,
|
|
||||||
color,
|
|
||||||
class: className = 'size-4',
|
|
||||||
}: { name?: string; color?: string; class?: string } = $props()
|
|
||||||
|
|
||||||
// Das Versprechen einmal je Name bilden, nicht bei jedem Rendern neu.
|
|
||||||
const loading = $derived(iconLoader(name)?.() ?? null)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Farbe über den Wrapper: Lucide zeichnet mit currentColor. -->
|
|
||||||
<span class="inline-flex shrink-0" style:color>
|
|
||||||
{#if loading}
|
|
||||||
{#await loading}
|
|
||||||
<Flag class={className} />
|
|
||||||
{:then mod}
|
|
||||||
{@const Icon = mod.default as typeof Flag}
|
|
||||||
<Icon class={className} />
|
|
||||||
{:catch}
|
|
||||||
<Flag class={className} />
|
|
||||||
{/await}
|
|
||||||
{:else}
|
|
||||||
<Flag class={className} />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Icon-Auswahl: das Raster ist die Auswahl, nicht ein Vorschlag daneben.
|
|
||||||
*
|
|
||||||
* Frueher stand hier zusaetzlich ein Freitextfeld fuer jeden beliebigen
|
|
||||||
* Namen von lucide.dev. Das verlangte, einen Katalog von tausend Icons zu
|
|
||||||
* kennen und den Namen fehlerfrei abzutippen, und belohnte den Tippfehler
|
|
||||||
* mit einem leeren Kaestchen. Was zur Wahl steht, steht jetzt da.
|
|
||||||
*
|
|
||||||
* Gemeinsam genutzt von der Flag-Verwaltung und dem Marker-Dialog, damit
|
|
||||||
* beide dieselbe Auswahl zeigen.
|
|
||||||
*/
|
|
||||||
import FlagIcon from './FlagIcon.svelte'
|
|
||||||
import { tooltip } from '$lib/stores/app.svelte'
|
|
||||||
import { FLAG_ICON_SUGGESTIONS } from '$lib/flagIcons'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
|
|
||||||
let {
|
|
||||||
value = $bindable(''),
|
|
||||||
color = undefined,
|
|
||||||
id = 'icon-picker',
|
|
||||||
label = 'Icon',
|
|
||||||
}: {
|
|
||||||
value?: string
|
|
||||||
/** Faerbt die Icons im Raster, damit man das Ergebnis sieht. */
|
|
||||||
color?: string
|
|
||||||
id?: string
|
|
||||||
label?: string
|
|
||||||
} = $props()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label id="{id}-label">{label}</Label>
|
|
||||||
|
|
||||||
<!-- Eine Gruppe von Knoepfen, kein Eingabefeld: benannt ueber
|
|
||||||
aria-labelledby statt ueber ein <label for>. -->
|
|
||||||
<div role="group" aria-labelledby="{id}-label" class="grid grid-cols-10 gap-1 rounded-lg border p-2">
|
|
||||||
{#each FLAG_ICON_SUGGESTIONS as name (name)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label={name}
|
|
||||||
use:tooltip={{ content: name }}
|
|
||||||
aria-pressed={value === name}
|
|
||||||
class="grid place-items-center rounded-md p-1.5 hover:bg-accent"
|
|
||||||
class:bg-accent={value === name}
|
|
||||||
class:ring-2={value === name}
|
|
||||||
class:ring-primary={value === name}
|
|
||||||
onclick={() => (value = name)}
|
|
||||||
>
|
|
||||||
<FlagIcon {name} {color} class="size-4" />
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
41
frontend/src/lib/components/LegalPage.svelte
Normal file
41
frontend/src/lib/components/LegalPage.svelte
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Rahmen für Impressum und Datenschutz: schmale Spalte, Fließtext,
|
||||||
|
* ein Weg zurück.
|
||||||
|
*
|
||||||
|
* Beide Seiten sind Prosa und sonst nichts — deshalb ein Bauteil und
|
||||||
|
* zwei dünne Routen, statt zweimal dieselbe Spalte zu bauen.
|
||||||
|
*/
|
||||||
|
import type { Snippet } from 'svelte'
|
||||||
|
|
||||||
|
let { title, updated, children }: { title: string; updated: string; children: Snippet } =
|
||||||
|
$props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{title} · Stammtisch Hersbruck</title>
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-2xl px-6 py-16">
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
|
||||||
|
>
|
||||||
|
← Zurück
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<h1 class="mt-6 text-2xl font-semibold tracking-tight">{title}</h1>
|
||||||
|
<p class="mt-1 text-sm text-muted-foreground">Stand: {updated}</p>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
`prose` aus dem Typografie-Plugin: Ein Rechtstext ist eine Folge von
|
||||||
|
Überschriften und Absätzen, und die soll aussehen wie gesetzt und
|
||||||
|
nicht wie ein Formular.
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="prose prose-sm mt-8 max-w-none prose-headings:font-semibold prose-headings:tracking-tight prose-a:text-foreground"
|
||||||
|
>
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Logo eines Teams. Ist eins hinterlegt, steht es allein; sonst bleibt das
|
|
||||||
* Wappen des Stammtischs als Rückfalloption — zwei übereinanderliegende
|
|
||||||
* SVGs, weil das Radl über dem Schildl leicht übersteht.
|
|
||||||
*
|
|
||||||
* Groesse ueber eine Klasse steuerbar, damit dasselbe Logo im Header
|
|
||||||
* (klein) und auf der Login-Karte (gross) verwendet werden kann.
|
|
||||||
*/
|
|
||||||
import { getFileURL } from '$lib/stores/pocketbase.svelte'
|
|
||||||
import type { TeamsResponse } from '$lib/types'
|
|
||||||
|
|
||||||
let {
|
|
||||||
class: className = 'w-24 h-24',
|
|
||||||
team = null,
|
|
||||||
}: { class?: string; team?: TeamsResponse | null } = $props()
|
|
||||||
|
|
||||||
const url = $derived(team?.logo ? getFileURL(team, team.logo) : null)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="relative {className}" id="logo">
|
|
||||||
{#if url}
|
|
||||||
<img alt="" class="absolute h-full w-full object-contain" src={url} />
|
|
||||||
{:else}
|
|
||||||
<img alt="" class="absolute h-full w-full object-contain" src="/schildl.svg">
|
|
||||||
<img alt="" class="absolute h-full w-full scale-110 object-contain" src="/radl.svg">
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Die Leiste über allen Dashboard-Seiten: alles, was den Zeitnehmer
|
|
||||||
* betrifft, an einer Stelle. Von oben nach unten mit fallender
|
|
||||||
* Dringlichkeit — fehlende Verbindung, nicht übertragene Zeiten, laufende
|
|
||||||
* Uhren.
|
|
||||||
*
|
|
||||||
* Gedacht für die Strecke: Man legt zwischen zwei Startern die Trails
|
|
||||||
* oder die Fahrerliste an, und die laufende Uhr bleibt trotzdem im Blick —
|
|
||||||
* samt Stopp ohne Umweg über die Stage-Seite.
|
|
||||||
*
|
|
||||||
* Aufgeklappt ist der Normalfall: Wer an der Strecke steht, will die
|
|
||||||
* laufenden Zeiten sehen und nicht erst danach fragen. Einklappen geht,
|
|
||||||
* sobald es mehr als eine ist. Bei genau einer entfällt das Klappen.
|
|
||||||
*
|
|
||||||
* Gestoppt wird auch von hier aus nicht direkt: Ein Klick auf die Zeile
|
|
||||||
* öffnet den Zeit-Dialog mit Fahrer und Stage vorbelegt.
|
|
||||||
*/
|
|
||||||
import { getTimeContext } from '$lib/stores/times.svelte'
|
|
||||||
import { riderName, getRiderContext } from '$lib/stores/riders.svelte'
|
|
||||||
import { getStageContext } from '$lib/stores/stages.svelte'
|
|
||||||
import { getEventParticipantContext } from '$lib/stores/eventParticipants.svelte'
|
|
||||||
import { ticker } from '$lib/stores/ticker.svelte'
|
|
||||||
import { timeOutbox } from '$lib/stores/timeOutbox.svelte'
|
|
||||||
import { connection } from '$lib/stores/connection.svelte'
|
|
||||||
import TimeDialog from './TimeDialog.svelte'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { ChevronDown, ChevronUp, CloudOff, RefreshCw, Timer, WifiOff } from 'lucide-svelte'
|
|
||||||
|
|
||||||
const times = getTimeContext()
|
|
||||||
const riders = getRiderContext()
|
|
||||||
const stages = getStageContext()
|
|
||||||
const participants = getEventParticipantContext()
|
|
||||||
const clock = ticker()
|
|
||||||
|
|
||||||
const running = $derived(times.allRunning(clock.now))
|
|
||||||
const single = $derived(running.length === 1)
|
|
||||||
|
|
||||||
let expanded = $state(true)
|
|
||||||
|
|
||||||
// Vorbelegung für den Zeit-Dialog
|
|
||||||
let timeDialog = $state(false)
|
|
||||||
let dialogEvent = $state('')
|
|
||||||
let dialogRider = $state<string | null>(null)
|
|
||||||
let dialogStage = $state<string | null>(null)
|
|
||||||
|
|
||||||
function openTimeDialog(riderId: string | undefined, stageId: string | undefined) {
|
|
||||||
const stage = stageId ? stages.getById(stageId) : undefined
|
|
||||||
if (!stage?.event) return
|
|
||||||
|
|
||||||
dialogEvent = stage.event
|
|
||||||
dialogRider = riderId ?? null
|
|
||||||
dialogStage = stage.id
|
|
||||||
timeDialog = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eine einzelne Zeit steht immer offen da; bei mehreren entscheidet der
|
|
||||||
// Nutzer — voreingestellt ist offen. Fällt die Anzahl wieder auf eine,
|
|
||||||
// ist der Zustand egal.
|
|
||||||
const open = $derived(single || expanded)
|
|
||||||
|
|
||||||
/** Die Startnummer gilt je Event, deshalb der Umweg über die Stage. */
|
|
||||||
function riderLabel(riderId: string | undefined, stageId: string | undefined) {
|
|
||||||
const eventId = stageId ? stages.getById(stageId)?.event : undefined
|
|
||||||
if (!eventId) {
|
|
||||||
const r = riderId ? riders.getById(riderId) : undefined
|
|
||||||
return r ? riderName(r) : 'Unbekannter Fahrer'
|
|
||||||
}
|
|
||||||
return participants.riderLabel(eventId, riderId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function stageLabel(stageId: string | undefined) {
|
|
||||||
const stage = stageId ? stages.getById(stageId) : undefined
|
|
||||||
return stage?.description || 'Stage'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** „seit 3 min" — wie lange die Verbindung schon fehlt. */
|
|
||||||
function since(ms: number): string {
|
|
||||||
const min = Math.floor(ms / 60000)
|
|
||||||
if (min < 1) return 'gerade eben'
|
|
||||||
if (min < 60) return `seit ${min} min`
|
|
||||||
return `seit ${Math.floor(min / 60)} h`
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if running.length > 0 || timeOutbox.pending > 0 || !connection.online}
|
|
||||||
<!-- fixed statt sticky: Die Leiste soll auch beim Scrollen stehen bleiben. -->
|
|
||||||
<div class="fixed bottom-4 right-4 z-50 w-[min(24rem,calc(100vw-2rem))]">
|
|
||||||
<div class="rounded-lg border bg-card text-card-foreground shadow-lg overflow-hidden">
|
|
||||||
<div class="flex items-center gap-2 px-3 py-2 border-b bg-primary/5">
|
|
||||||
{#if running.length > 0}
|
|
||||||
<span class="relative flex size-2 shrink-0">
|
|
||||||
<span class="absolute inline-flex h-full w-full rounded-full bg-primary opacity-60 animate-ping"></span>
|
|
||||||
<span class="relative inline-flex size-2 rounded-full bg-primary"></span>
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<span class="text-sm font-medium">
|
|
||||||
{#if running.length === 0}
|
|
||||||
Zeitnahme
|
|
||||||
{:else if running.length === 1}
|
|
||||||
Zeit läuft
|
|
||||||
{:else}
|
|
||||||
{running.length} Zeiten laufen
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{#if !open}
|
|
||||||
<span class="text-sm tabular-nums font-semibold ml-auto">
|
|
||||||
{running[0].formattedTime}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !single}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="ml-auto size-7 shrink-0"
|
|
||||||
tooltip={expanded ? 'Einklappen' : 'Alle anzeigen'}
|
|
||||||
onclick={() => (expanded = !expanded)}
|
|
||||||
>
|
|
||||||
{#if expanded}
|
|
||||||
<ChevronDown class="size-4" />
|
|
||||||
{:else}
|
|
||||||
<ChevronUp class="size-4" />
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Die fehlende Verbindung steht ueber allem: Sie ist der Grund,
|
|
||||||
aus dem die Zeiten darunter liegenbleiben. Die Meldung geht
|
|
||||||
erst, wenn der Server wieder antwortet — an der Strecke waeren
|
|
||||||
fuenf Sekunden Einblendung dieselbe Katastrophe wie gar keine.
|
|
||||||
-->
|
|
||||||
{#if !connection.online}
|
|
||||||
<div class="flex items-start gap-2 border-b bg-amber-500/15 px-3 py-2">
|
|
||||||
<WifiOff class="mt-0.5 size-4 shrink-0 text-amber-600" />
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<p class="text-xs font-medium">
|
|
||||||
Keine Verbindung
|
|
||||||
{#if connection.offlineFor !== null}
|
|
||||||
<span class="font-normal text-muted-foreground">
|
|
||||||
({since(connection.offlineFor)})
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
Die Zeitnahme läuft weiter. Gestoppt wird auf diesem Gerät,
|
|
||||||
übertragen, sobald der Server wieder antwortet.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="size-7 shrink-0"
|
|
||||||
tooltip="Verbindung prüfen"
|
|
||||||
disabled={connection.checking}
|
|
||||||
onclick={() => connection.check()}
|
|
||||||
>
|
|
||||||
<RefreshCw class="size-4 {connection.checking ? 'animate-spin' : ''}" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Nicht uebertragene Zeiten darunter: Eine Zeit, die nur auf
|
|
||||||
diesem Geraet existiert, ist das Naechstdringende.
|
|
||||||
-->
|
|
||||||
{#if timeOutbox.pending > 0}
|
|
||||||
<div class="flex items-center gap-2 border-b bg-amber-500/10 px-3 py-2">
|
|
||||||
<CloudOff class="size-4 shrink-0 text-amber-600" />
|
|
||||||
<span class="min-w-0 flex-1 text-xs">
|
|
||||||
{timeOutbox.pending}
|
|
||||||
{timeOutbox.pending === 1 ? 'Zeit liegt' : 'Zeiten liegen'} nur auf diesem
|
|
||||||
Gerät.
|
|
||||||
{#if timeOutbox.error}
|
|
||||||
<span class="block text-destructive">{timeOutbox.error}</span>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="size-7 shrink-0"
|
|
||||||
tooltip="Jetzt übertragen"
|
|
||||||
disabled={timeOutbox.flushing}
|
|
||||||
onclick={() => timeOutbox.flush()}
|
|
||||||
>
|
|
||||||
<RefreshCw class="size-4 {timeOutbox.flushing ? 'animate-spin' : ''}" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if open}
|
|
||||||
<ul class="divide-y max-h-[50vh] overflow-y-auto">
|
|
||||||
{#each running as t (t.id)}
|
|
||||||
<li>
|
|
||||||
<!--
|
|
||||||
Die ganze Zeile ist der Knopf, nicht ein Icon
|
|
||||||
an ihrem Rand: An der Strecke wird mit dem
|
|
||||||
Daumen bedient, und ein 16-px-Ziel neben einer
|
|
||||||
fingerbreiten Zeile ist eine Einladung zum
|
|
||||||
Danebentippen.
|
|
||||||
|
|
||||||
Der Name der Stage verlinkt deshalb nicht mehr
|
|
||||||
auf ihre Seite — ein Link im Knopf waere weder
|
|
||||||
gueltiges HTML noch eindeutig zu treffen.
|
|
||||||
-->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-accent"
|
|
||||||
onclick={() => openTimeDialog(t.rider, t.stage)}
|
|
||||||
>
|
|
||||||
<Timer class="size-4 shrink-0 text-muted-foreground" />
|
|
||||||
|
|
||||||
<span class="min-w-0 flex-1">
|
|
||||||
<span class="block truncate text-sm font-medium">
|
|
||||||
{riderLabel(t.rider, t.stage)}
|
|
||||||
</span>
|
|
||||||
<span class="block truncate text-xs text-muted-foreground">
|
|
||||||
{stageLabel(t.stage)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="shrink-0 text-lg font-semibold tabular-nums">
|
|
||||||
{t.formattedTime}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TimeDialog
|
|
||||||
bind:open={timeDialog}
|
|
||||||
eventId={dialogEvent}
|
|
||||||
riderId={dialogRider}
|
|
||||||
stageId={dialogStage}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Ladeanzeige als Mountainbike-Laufrad: Stollenreifen, Felge, gekreuzte
|
|
||||||
* Speichen, Nabe. Das Ventil sitzt bewusst nur an einer Stelle — ohne diese
|
|
||||||
* Unwucht sieht ein achtfach symmetrisches Rad im Stillstand aus, egal wie
|
|
||||||
* schnell es sich dreht.
|
|
||||||
*
|
|
||||||
* Zeichnet in `currentColor` und richtet sich nach der uebergebenen
|
|
||||||
* Groessenklasse, laesst sich also wie ein Lucide-Icon einsetzen.
|
|
||||||
*/
|
|
||||||
let { class: className = 'size-6' }: { class?: string } = $props()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class="animate-spin {className}"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<!-- Stollen: 14 Segmente auf dem Umfang 2πr = 60.95, also je 4.35 breit -->
|
|
||||||
<circle cx="12" cy="12" r="9.7" stroke-dasharray="1.7 2.654" stroke-width="2.6" />
|
|
||||||
<!-- Reifen -->
|
|
||||||
<circle cx="12" cy="12" r="9.4" stroke-width="1.8" />
|
|
||||||
<!-- Felge -->
|
|
||||||
<circle cx="12" cy="12" r="7.3" stroke-width="1.1" />
|
|
||||||
<!-- Speichen, gekreuzt gespannt wie an einem echten Laufrad -->
|
|
||||||
<path
|
|
||||||
d="M12.88 10.2L12 4.7M12.65 10.11L17.16 6.84M13.8 12.88L19.3 12M13.89 12.65L17.16 17.16M11.12 13.8L12 19.3M11.35 13.89L6.84 17.16M10.2 11.12L4.7 12M10.11 11.35L6.84 6.84"
|
|
||||||
opacity=".5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-width=".8"
|
|
||||||
/>
|
|
||||||
<!-- Nabe -->
|
|
||||||
<circle cx="12" cy="12" fill="currentColor" r="1.9" stroke="none" />
|
|
||||||
<!-- Ventil -->
|
|
||||||
<path d="M12 8.4L12 6.6" stroke-linecap="round" stroke-width="1.3" />
|
|
||||||
</svg>
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Der Menuepunkt „Team" in der Kopfleiste — ein Menuepunkt, der sich beim
|
|
||||||
* Ueberfahren aufklappt: darunter der Wechsel zwischen den eigenen Teams
|
|
||||||
* und der Weg zu einem neuen.
|
|
||||||
*
|
|
||||||
* Traegt bits-uis NavigationMenu und nicht das DropdownMenu. Ein Dropdown
|
|
||||||
* ist ein Menue, das auf Ansage aufgeht; eine Navigationsleiste ist etwas
|
|
||||||
* anderes, und die Unterschiede sind genau die, die hier gebraucht werden:
|
|
||||||
* Aufklappen beim Ueberfahren samt Verzoegerung, ein Klick, der nach dem
|
|
||||||
* Hover-Aufklappen nicht wieder zuklappt, kein Fokusfang, keine
|
|
||||||
* Scrollsperre, kein Fokusring nach dem Zeigerbesuch. Das alles stand hier
|
|
||||||
* vorher als Handarbeit — Zeitgeber, ueberschriebene Zeigerhandler,
|
|
||||||
* unterdrueckter Fokus. Die Komponente kann es von sich aus.
|
|
||||||
*
|
|
||||||
* Der Punkt selbst bleibt ein Link auf die Teamseite: Der Trigger wird
|
|
||||||
* ueber das child-Snippet als <a> gezeichnet. bits-ui haelt seinen eigenen
|
|
||||||
* onclick zurueck, wenn per Zeiger aufgeklappt wurde („if opened via
|
|
||||||
* pointer move, we prevent the click event"), der Klick landet also beim
|
|
||||||
* Link.
|
|
||||||
*
|
|
||||||
* Auf dem Touchscreen gibt es kein Ueberfahren: Dort fuehrt der Tipp auf
|
|
||||||
* die Teamseite, und gewechselt wird in deren Liste.
|
|
||||||
*/
|
|
||||||
import { goto } from '$app/navigation'
|
|
||||||
import { getTeamContext } from '$lib/stores/teams.svelte'
|
|
||||||
import CreateTeamDialog from '$lib/components/CreateTeamDialog.svelte'
|
|
||||||
import * as NavigationMenu from '@/components/ui/navigation-menu'
|
|
||||||
import { ChevronDown, Plus, Users } from 'lucide-svelte'
|
|
||||||
|
|
||||||
let {
|
|
||||||
class: className = '',
|
|
||||||
onnavigate,
|
|
||||||
}: { class?: string; onnavigate?: () => void } = $props()
|
|
||||||
|
|
||||||
const teams = getTeamContext()
|
|
||||||
|
|
||||||
let createDialog = $state(false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Team wechseln heisst hier auch: hinsehen. Der Wechsel allein aendert
|
|
||||||
* bloss still, was jede Liste zeigt — die Teamseite macht sichtbar,
|
|
||||||
* worauf man gewechselt hat.
|
|
||||||
*/
|
|
||||||
function switchTo(id: string) {
|
|
||||||
teams.setActive(id)
|
|
||||||
goto('/dashboard/team')
|
|
||||||
onnavigate?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemClass =
|
|
||||||
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-left transition-colors hover:bg-accent'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NavigationMenu.Item value="team">
|
|
||||||
<NavigationMenu.Trigger>
|
|
||||||
{#snippet child({ props })}
|
|
||||||
<a {...props} href="/dashboard/team" class={className} onclick={() => onnavigate?.()}>
|
|
||||||
<Users class="h-4 w-4" />
|
|
||||||
<span>Team</span>
|
|
||||||
<ChevronDown class="h-3 w-3 opacity-50" />
|
|
||||||
</a>
|
|
||||||
{/snippet}
|
|
||||||
</NavigationMenu.Trigger>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
mt-0 statt der 1.5 aus dem Standard: Der Abstand war ein toter Streifen
|
|
||||||
zwischen Menuepunkt und Panel. Wer den Zeiger hinunterbewegte, verliess
|
|
||||||
beides zugleich, und nach delayDuration klappte das Menue zu, bevor ein
|
|
||||||
Eintrag zu treffen war. Ein Hover-Menue darf keine Luecke haben.
|
|
||||||
-->
|
|
||||||
<NavigationMenu.Content class="min-w-56 group-data-[viewport=false]/navigation-menu:mt-0">
|
|
||||||
{#each teams.records as t (t.id)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={() => switchTo(t.id)}
|
|
||||||
class="{itemClass} {t.id === teams.activeId ? 'bg-accent' : ''}"
|
|
||||||
>
|
|
||||||
<span class="flex-1 truncate">{t.name}</span>
|
|
||||||
{#if teams.isOwner(t)}
|
|
||||||
<span class="text-xs text-muted-foreground">Owner</span>
|
|
||||||
{:else if teams.isAdmin(t)}
|
|
||||||
<span class="text-xs text-muted-foreground">Admin</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<p class="px-2 py-1.5 text-sm text-muted-foreground">Noch in keinem Team</p>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<div class="my-1 h-px bg-border"></div>
|
|
||||||
|
|
||||||
<button type="button" onclick={() => (createDialog = true)} class={itemClass}>
|
|
||||||
<Plus class="h-4 w-4" />
|
|
||||||
Team erstellen
|
|
||||||
</button>
|
|
||||||
</NavigationMenu.Content>
|
|
||||||
</NavigationMenu.Item>
|
|
||||||
|
|
||||||
<!-- Ausserhalb des Menues: mit dem Klick klappt es zu, und ein darin
|
|
||||||
gerendeter Dialog verschwaende mit ihm. -->
|
|
||||||
<CreateTeamDialog bind:open={createDialog} />
|
|
||||||
|
|
@ -1,281 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Der eine Dialog für die Zeitnahme. Fahrer und Stage stehen immer beide
|
|
||||||
* darin — auch wenn einer davon aus dem Kontext vorbelegt ist —, damit
|
|
||||||
* beim Starten und Stoppen nie ein Zweifel bleibt, wen und was man
|
|
||||||
* gerade erwischt. Start und Stopp passieren ausschließlich hier: weder
|
|
||||||
* Tabellen noch die Toast-Leiste lösen sie direkt aus.
|
|
||||||
*
|
|
||||||
* Bedient wird das an der Strecke, im Stehen, mit einer Hand am Lenker
|
|
||||||
* und einem Daumen am Telefon. Danach ist alles hier bemessen: Der Fahrer
|
|
||||||
* wird aus Kacheln gewählt statt aus einer Auswahlliste — die Startnummer
|
|
||||||
* gross wie am Trikot, der Name darunter —, und Starten und Stoppen sind
|
|
||||||
* flächige Knöpfe und keine Zeile im Fussbereich.
|
|
||||||
*/
|
|
||||||
import { getTimeContext, type TimeStatus } from '$lib/stores/times.svelte'
|
|
||||||
import { riderName, getRiderContext } from '$lib/stores/riders.svelte'
|
|
||||||
import { getStageContext } from '$lib/stores/stages.svelte'
|
|
||||||
import { getEventParticipantContext } from '$lib/stores/eventParticipants.svelte'
|
|
||||||
import { ticker } from '$lib/stores/ticker.svelte'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import * as Dialog from '@/components/ui/dialog'
|
|
||||||
import * as Select from '@/components/ui/select'
|
|
||||||
import { AlertTriangle, Flag, Play, Square } from 'lucide-svelte'
|
|
||||||
|
|
||||||
let {
|
|
||||||
open = $bindable(false),
|
|
||||||
eventId,
|
|
||||||
riderId = null,
|
|
||||||
stageId = null,
|
|
||||||
}: {
|
|
||||||
open?: boolean
|
|
||||||
/** Event, aus dem Fahrer und Stages zur Auswahl stehen. */
|
|
||||||
eventId: string
|
|
||||||
/** Vorbelegung aus dem Kontext; bleibt im Dialog änderbar. */
|
|
||||||
riderId?: string | null
|
|
||||||
stageId?: string | null
|
|
||||||
} = $props()
|
|
||||||
|
|
||||||
const times = getTimeContext()
|
|
||||||
const riders = getRiderContext()
|
|
||||||
const stages = getStageContext()
|
|
||||||
const participants = getEventParticipantContext()
|
|
||||||
const clock = ticker()
|
|
||||||
|
|
||||||
let selectedRider = $state('')
|
|
||||||
let selectedStage = $state('')
|
|
||||||
let error = $state<string | null>(null)
|
|
||||||
let confirmDnf = $state(false)
|
|
||||||
let busy = $state(false)
|
|
||||||
|
|
||||||
// Beim Öffnen auf die Vorbelegung zurücksetzen — ein Dialog, der noch die
|
|
||||||
// Auswahl vom letzten Mal zeigt, ist an der Strecke eine Fehlerquelle.
|
|
||||||
$effect(() => {
|
|
||||||
if (!open) return
|
|
||||||
|
|
||||||
selectedRider = riderId ?? ''
|
|
||||||
selectedStage = stageId ?? ''
|
|
||||||
error = null
|
|
||||||
confirmDnf = false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Zur Auswahl steht, wer beim Event zugesagt hat — nicht der ganze Kader.
|
|
||||||
const eventRiders = $derived(
|
|
||||||
eventId
|
|
||||||
? participants
|
|
||||||
.confirmedByEvent(eventId)
|
|
||||||
.map((p) => riders.getById(p.rider))
|
|
||||||
.filter((r) => !!r)
|
|
||||||
: [],
|
|
||||||
)
|
|
||||||
const eventStages = $derived(eventId ? stages.getByEvent(eventId) : [])
|
|
||||||
|
|
||||||
const stageLabel = $derived(
|
|
||||||
selectedStage ? stages.getById(selectedStage)?.description ?? 'Stage wählen…' : 'Stage wählen…',
|
|
||||||
)
|
|
||||||
|
|
||||||
const ready = $derived(!!selectedRider && !!selectedStage)
|
|
||||||
|
|
||||||
// Über die laufende Liste gelesen, damit die Uhr im Dialog mitzählt und
|
|
||||||
// verschwindet, sobald jemand anders dieselbe Zeit stoppt.
|
|
||||||
const active = $derived(
|
|
||||||
ready
|
|
||||||
? times
|
|
||||||
.allRunning(clock.now)
|
|
||||||
.find((t) => t.stage === selectedStage && t.rider === selectedRider)
|
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Bereits gewertete Zeit derselben Paarung — Hinweis, keine Sperre. */
|
|
||||||
const existing = $derived(
|
|
||||||
ready && !active
|
|
||||||
? times.getByStage(selectedStage).find((t) => t.rider === selectedRider && t.stop)
|
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
|
|
||||||
async function start() {
|
|
||||||
if (!ready || busy) return
|
|
||||||
|
|
||||||
busy = true
|
|
||||||
error = null
|
|
||||||
try {
|
|
||||||
await times.start(selectedStage, selectedRider)
|
|
||||||
open = false
|
|
||||||
} catch (e: any) {
|
|
||||||
error = e.message ?? 'Die Zeit konnte nicht gestartet werden.'
|
|
||||||
} finally {
|
|
||||||
busy = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stop(status: 'finished' | 'dnf') {
|
|
||||||
if (!active || busy) return
|
|
||||||
|
|
||||||
busy = true
|
|
||||||
error = null
|
|
||||||
try {
|
|
||||||
await times.stop(active.id, status as TimeStatus)
|
|
||||||
open = false
|
|
||||||
} catch (e: any) {
|
|
||||||
error = e.message ?? 'Die Zeit konnte nicht gestoppt werden.'
|
|
||||||
} finally {
|
|
||||||
busy = false
|
|
||||||
confirmDnf = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Dialog.Root bind:open>
|
|
||||||
<Dialog.Content>
|
|
||||||
<Dialog.Header>
|
|
||||||
<Dialog.Title>Zeit erfassen</Dialog.Title>
|
|
||||||
<Dialog.Description>
|
|
||||||
Fahrer und Stage wählen — je nach Stand wird gestartet oder gestoppt.
|
|
||||||
</Dialog.Description>
|
|
||||||
</Dialog.Header>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label id="rider-label">Fahrer</Label>
|
|
||||||
|
|
||||||
{#if eventRiders.length === 0}
|
|
||||||
<p class="text-xs text-muted-foreground">Für dieses Event gibt es noch keine Fahrer.</p>
|
|
||||||
{:else}
|
|
||||||
<!--
|
|
||||||
Kacheln statt Auswahlliste: Eine Liste verlangt
|
|
||||||
aufklappen, zielen und treffen — drei Schritte für
|
|
||||||
etwas, das jeder Zeitnehmer zwanzigmal am Tag macht.
|
|
||||||
Hier steht jeder Fahrer als eigene Fläche da, gross
|
|
||||||
genug für einen Daumen.
|
|
||||||
-->
|
|
||||||
<div
|
|
||||||
role="radiogroup"
|
|
||||||
aria-labelledby="rider-label"
|
|
||||||
class="grid max-h-56 grid-cols-3 gap-2 overflow-y-auto rounded-lg border p-2 sm:grid-cols-4"
|
|
||||||
>
|
|
||||||
{#each eventRiders as r (r.id)}
|
|
||||||
{@const number = participants.numberOf(eventId, r.id)}
|
|
||||||
{@const chosen = selectedRider === r.id}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
aria-checked={chosen}
|
|
||||||
onclick={() => (selectedRider = r.id)}
|
|
||||||
class="flex min-h-20 flex-col items-center justify-center gap-0.5 rounded-lg border p-2 transition-colors hover:bg-accent"
|
|
||||||
class:border-primary={chosen}
|
|
||||||
class:bg-primary={chosen}
|
|
||||||
class:text-primary-foreground={chosen}
|
|
||||||
>
|
|
||||||
<span class="font-startnumber text-2xl leading-none">
|
|
||||||
{number || '–'}
|
|
||||||
</span>
|
|
||||||
<span class="line-clamp-2 text-center text-xs leading-tight">
|
|
||||||
{riderName(r)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label>Stage</Label>
|
|
||||||
<Select.Root type="single" bind:value={selectedStage} disabled={eventStages.length === 0}>
|
|
||||||
<Select.Trigger class="w-full">{stageLabel}</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{#each eventStages as r (r.id)}
|
|
||||||
<Select.Item value={r.id}>{r.description || 'Stage'}</Select.Item>
|
|
||||||
{/each}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
{#if eventStages.length === 0}
|
|
||||||
<p class="text-xs text-muted-foreground">Für dieses Event gibt es noch keine Stages.</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if active}
|
|
||||||
<div class="rounded-lg border bg-primary/5 py-4 text-center">
|
|
||||||
<p class="text-xs text-muted-foreground">läuft seit</p>
|
|
||||||
<p class="font-mono text-4xl font-semibold tabular-nums">{active.formattedTime}</p>
|
|
||||||
</div>
|
|
||||||
{:else if existing}
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Für diese Paarung gibt es bereits eine Zeit ({existing.formattedTime}).
|
|
||||||
Ein neuer Start legt eine zweite an.
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if error}
|
|
||||||
<p class="text-sm text-destructive">{error}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if confirmDnf}
|
|
||||||
<!-- DNF beendet die Zeit ohne Wertung und ist nicht zurückzunehmen. -->
|
|
||||||
<div class="rounded-lg border border-destructive/40 bg-destructive/5 p-3 space-y-3">
|
|
||||||
<p class="text-sm flex items-start gap-2">
|
|
||||||
<AlertTriangle class="size-4 text-destructive shrink-0 mt-0.5" />
|
|
||||||
<span>
|
|
||||||
Wirklich als DNF werten? Die Zeit wird beendet und taucht in keiner
|
|
||||||
Rangliste auf.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<Button variant="ghost" size="sm" onclick={() => (confirmDnf = false)}>
|
|
||||||
Zurück
|
|
||||||
</Button>
|
|
||||||
<Button variant="destructive" size="sm" disabled={busy} onclick={() => stop('dnf')}>
|
|
||||||
DNF bestätigen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<!--
|
|
||||||
Die Handlung steht als Fläche da, nicht als Zeile im
|
|
||||||
Fussbereich: 96 px hoch, über die volle Breite, Symbol über
|
|
||||||
Wort. Wer im Stehen auf die Sekunde drückt, zielt nicht auf
|
|
||||||
einen 36 px hohen Streifen am unteren Rand.
|
|
||||||
|
|
||||||
DNF steht daneben und ist schmaler — dieselbe Höhe, damit es
|
|
||||||
sich treffen lässt, aber nicht dieselbe Breite, damit es nicht
|
|
||||||
versehentlich getroffen wird.
|
|
||||||
-->
|
|
||||||
{#if active}
|
|
||||||
<div class="grid grid-cols-3 gap-2">
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
disabled={busy}
|
|
||||||
class="col-span-2 h-24 flex-col gap-1 text-lg font-semibold"
|
|
||||||
onclick={() => stop('finished')}
|
|
||||||
>
|
|
||||||
<Square class="size-7" />
|
|
||||||
Stoppen
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
disabled={busy}
|
|
||||||
class="h-24 flex-col gap-1 text-base font-semibold"
|
|
||||||
onclick={() => (confirmDnf = true)}
|
|
||||||
>
|
|
||||||
<Flag class="size-6" />
|
|
||||||
DNF
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<Button
|
|
||||||
disabled={!ready || busy}
|
|
||||||
class="h-24 w-full flex-col gap-1 text-lg font-semibold"
|
|
||||||
onclick={start}
|
|
||||||
>
|
|
||||||
<Play class="size-7" />
|
|
||||||
Starten
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Dialog.Footer>
|
|
||||||
<Button variant="ghost" onclick={() => (open = false)}>Abbrechen</Button>
|
|
||||||
</Dialog.Footer>
|
|
||||||
{/if}
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
|
|
@ -1,494 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
/**
|
|
||||||
* Karte für einen Trail. Bewusst ohne Store-Zugriff: Alles kommt über
|
|
||||||
* Props, Ereignisse gehen über Callback-Props zurück. Damit ist die
|
|
||||||
* Komponente in Liste, Detailseite und Setzmodus gleichermaßen nutzbar.
|
|
||||||
*/
|
|
||||||
import { mount, onMount, unmount } from 'svelte'
|
|
||||||
import * as maplibregl from 'maplibre-gl'
|
|
||||||
import type { Map as MapLibreMap, Marker, Popup } from 'maplibre-gl'
|
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
|
||||||
// MapLibre 6 liegt als drei Dateien vor (Hauptdatei, shared, worker) und
|
|
||||||
// lädt den Worker zur Laufzeit relativ zum eigenen Verzeichnis nach. Beides
|
|
||||||
// — Vites Dependency-Optimierung im Dev-Modus wie der Produktionsbuild —
|
|
||||||
// bringt den Worker nicht an eine Stelle, an der er seine Nachbardatei
|
|
||||||
// maplibre-gl-shared.mjs findet. Der Worker startet dann zwar, sein
|
|
||||||
// Import läuft ins Leere, und er antwortet auf keine Nachricht mehr.
|
|
||||||
// Folge: GeoJSON-Quellen bleiben dauerhaft im Ladezustand, es wird keine
|
|
||||||
// Trail-Linie gezeichnet. Rasterkacheln brauchen keinen Worker, deshalb
|
|
||||||
// erscheint die Karte trotzdem und der Fehler sieht nach fehlenden Daten
|
|
||||||
// aus.
|
|
||||||
// ?worker&url bündelt den Worker samt Abhängigkeiten zu einer eigenständigen
|
|
||||||
// Datei — ein blosses ?url würde nur die Rohdatei kopieren und ihren
|
|
||||||
// relativen Import auf ./maplibre-gl-shared.mjs unaufgelöst lassen (404).
|
|
||||||
import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url'
|
|
||||||
import { nearestOnLine, distanceAlong, positionAtDistance } from '$lib/gpx'
|
|
||||||
import FlagIcon from './FlagIcon.svelte'
|
|
||||||
import { trailStatusColor } from '$lib/trailStatus'
|
|
||||||
import { mode } from 'mode-watcher'
|
|
||||||
|
|
||||||
maplibregl.setWorkerUrl(workerUrl)
|
|
||||||
|
|
||||||
type LineGeoJSON = { type: 'LineString'; coordinates: [number, number][] }
|
|
||||||
type MarkerData = {
|
|
||||||
id: string
|
|
||||||
lat: number
|
|
||||||
lng: number
|
|
||||||
color: string
|
|
||||||
resolved: boolean
|
|
||||||
/** Was — der Flag-Typ */
|
|
||||||
label: string
|
|
||||||
/** Icon-Name des Flag-Typs */
|
|
||||||
icon?: string
|
|
||||||
/** Wann — bereits formatiert */
|
|
||||||
when?: string
|
|
||||||
/** Wer — bereits aufgelöst */
|
|
||||||
who?: string
|
|
||||||
note?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
|
||||||
geojson = null,
|
|
||||||
bounds = null,
|
|
||||||
markers = [],
|
|
||||||
status = 'offen',
|
|
||||||
coordDistances = [],
|
|
||||||
highlightDistance = null,
|
|
||||||
onplace,
|
|
||||||
ontrailhover,
|
|
||||||
controls = true,
|
|
||||||
height = '420px',
|
|
||||||
}: {
|
|
||||||
geojson?: LineGeoJSON | null
|
|
||||||
bounds?: [[number, number], [number, number]] | null
|
|
||||||
markers?: MarkerData[]
|
|
||||||
status?: 'offen' | 'eingeschraenkt' | 'gesperrt'
|
|
||||||
coordDistances?: number[]
|
|
||||||
highlightDistance?: number | null
|
|
||||||
/**
|
|
||||||
* Wird nur beim Klick auf die Trail-Linie gerufen — und zwar mit der
|
|
||||||
* darauf projizierten Stelle, nie mit dem rohen Klickpunkt. Ist der
|
|
||||||
* Callback nicht gesetzt, ist die Linie nicht klickbar.
|
|
||||||
*/
|
|
||||||
onplace?: (p: { lat: number; lng: number; distance: number | null }) => void
|
|
||||||
/** Streckenkilometer unter dem Zeiger, null beim Verlassen der Linie. */
|
|
||||||
ontrailhover?: (distance: number | null) => void
|
|
||||||
/**
|
|
||||||
* Zoom-Knöpfe, Maßstab und Quellenangabe. In der Kachelvorschau
|
|
||||||
* abgeschaltet — dort ist die Karte Bild, nicht Werkzeug. Die
|
|
||||||
* Quellenangabe zu OpenStreetMap trägt die große Karte.
|
|
||||||
*/
|
|
||||||
controls?: boolean
|
|
||||||
height?: string
|
|
||||||
} = $props()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Basiskarten von OpenFreeMap: zurückhaltend gezeichnet — die Trail-Linie
|
|
||||||
* liegt dadurch deutlich sichtbarer darüber — und in einer echten dunklen
|
|
||||||
* Fassung. Ein nachträglich abgedunkeltes OSM-Bild wäre nur ein dunkleres
|
|
||||||
* Bild, keine dunkle Karte.
|
|
||||||
*
|
|
||||||
* Vorher lagen hier die Rasterkacheln von CARTO. Die kommen inzwischen
|
|
||||||
* mit einem eingebrannten „API KEY REQUIRED" quer über der Karte — mit
|
|
||||||
* HTTP 200, weshalb es wie ein Darstellungsfehler aussah und nicht wie
|
|
||||||
* eine Sperre. Der Schlüssel aus PUBLIC_CARTO_API_KEY änderte daran
|
|
||||||
* nichts: Die Kachel kam byteidentisch zurück wie ohne ihn.
|
|
||||||
*
|
|
||||||
* OpenFreeMap braucht keinen Schlüssel und kennt kein Limit; die Stile
|
|
||||||
* heißen dort positron und dark und sind genau die, die CARTO als
|
|
||||||
* Positron und Dark Matter geprägt hat. Die Quellenangabe steckt in der
|
|
||||||
* Stildatei, sie muss hier nicht mehr von Hand gesetzt werden.
|
|
||||||
*/
|
|
||||||
const STYLE_LIGHT = 'https://tiles.openfreemap.org/styles/positron'
|
|
||||||
const STYLE_DARK = 'https://tiles.openfreemap.org/styles/dark'
|
|
||||||
|
|
||||||
const isDark = $derived(mode.current === 'dark')
|
|
||||||
|
|
||||||
// Fallback, wenn ein Trail noch keine Version hat: Hersbruck.
|
|
||||||
const FALLBACK_CENTER: [number, number] = [11.4318, 49.5089]
|
|
||||||
|
|
||||||
let container: HTMLDivElement
|
|
||||||
let map = $state<MapLibreMap | null>(null)
|
|
||||||
let ready = $state(false)
|
|
||||||
let markerObjects: Marker[] = []
|
|
||||||
let highlightMarker: Marker | null = null
|
|
||||||
let popup: Popup | null = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MapLibre will für einen Marker ein DOM-Element, Icons kommen aber als
|
|
||||||
* Svelte-Komponenten. `mount` schlägt die Brücke — dafür müssen die
|
|
||||||
* Instanzen von Hand wieder abgeräumt werden, sonst bleiben sie beim
|
|
||||||
* Neuaufbau der Marker hängen.
|
|
||||||
*/
|
|
||||||
let iconInstances: Record<string, unknown>[] = []
|
|
||||||
|
|
||||||
function mountIcon(target: HTMLElement, name: string | undefined, size: string) {
|
|
||||||
iconInstances.push(mount(FlagIcon, { target, props: { name, class: size } }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearIcons() {
|
|
||||||
for (const inst of iconInstances) unmount(inst)
|
|
||||||
iconInstances = []
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
let cleanupMap: (() => void) | null = null
|
|
||||||
|
|
||||||
function build() {
|
|
||||||
const instance = new maplibregl.Map({
|
|
||||||
container,
|
|
||||||
style: isDark ? STYLE_DARK : STYLE_LIGHT,
|
|
||||||
center: FALLBACK_CENTER,
|
|
||||||
zoom: 11,
|
|
||||||
attributionControl: controls ? undefined : false,
|
|
||||||
})
|
|
||||||
map = instance
|
|
||||||
|
|
||||||
if (controls) {
|
|
||||||
instance.addControl(new maplibregl.NavigationControl(), 'top-right')
|
|
||||||
instance.addControl(new maplibregl.ScaleControl(), 'bottom-left')
|
|
||||||
}
|
|
||||||
|
|
||||||
// `style.load` statt `load`: Es feuert auch nach jedem
|
|
||||||
// setStyle. Ein Stilwechsel wirft Quellen und Layer weg, die
|
|
||||||
// Trail-Linie muss danach neu entstehen — die Marker nicht, die
|
|
||||||
// haengen als DOM-Elemente an der Karte und nicht am Stil.
|
|
||||||
instance.on('style.load', () => {
|
|
||||||
ready = true
|
|
||||||
drawTrail(instance)
|
|
||||||
})
|
|
||||||
|
|
||||||
cleanupMap = () => {
|
|
||||||
// Map.remove() räumt Controls auf, aber keine Marker — die tragen
|
|
||||||
// eigene move-Listener auf der Karte und werden hier zuerst entfernt.
|
|
||||||
for (const m of markerObjects) m.remove()
|
|
||||||
markerObjects = []
|
|
||||||
highlightMarker?.remove()
|
|
||||||
highlightMarker = null
|
|
||||||
popup?.remove()
|
|
||||||
popup = null
|
|
||||||
clearIcons()
|
|
||||||
|
|
||||||
instance.remove()
|
|
||||||
map = null
|
|
||||||
ready = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Browser begrenzen WebGL-Kontexte auf rund 8 bis 16. In einer Liste
|
|
||||||
// mit vielen Trails wären das zu viele — deshalb entsteht die Karte
|
|
||||||
// erst, wenn ihre Kachel in den Sichtbereich scrollt.
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.isIntersecting && !map) build()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ rootMargin: '200px' },
|
|
||||||
)
|
|
||||||
observer.observe(container)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect()
|
|
||||||
cleanupMap?.()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hover und Klick hängen an einer unsichtbaren, breiten Kopie der Linie:
|
|
||||||
* die sichtbaren 4 px wären als Ziel zu schmal, und maplibre liefert
|
|
||||||
* layerbezogene Ereignisse nur für das, was wirklich getroffen wurde.
|
|
||||||
* Deshalb landet ein Klick neben dem Trail auch nirgends — Marker können
|
|
||||||
* gar nicht abseits der Strecke entstehen.
|
|
||||||
*
|
|
||||||
* Registriert wird einmalig direkt nach dem Anlegen der Layer; vorher
|
|
||||||
* hätten die layerbezogenen Listener kein Ziel.
|
|
||||||
*/
|
|
||||||
function bindTrailEvents(instance: MapLibreMap) {
|
|
||||||
const project = (e: maplibregl.MapMouseEvent) => {
|
|
||||||
const coords = geojson?.coordinates
|
|
||||||
if (!coords?.length) return null
|
|
||||||
|
|
||||||
const pos = nearestOnLine(coords, e.lngLat.lng, e.lngLat.lat)
|
|
||||||
if (!pos) return null
|
|
||||||
|
|
||||||
return { pos, distance: distanceAlong(pos, coordDistances) }
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.on('mousemove', 'trail-hit', (e) => {
|
|
||||||
const hit = project(e)
|
|
||||||
if (!hit) return
|
|
||||||
|
|
||||||
instance.getCanvas().style.cursor = onplace ? 'pointer' : ''
|
|
||||||
ontrailhover?.(hit.distance)
|
|
||||||
})
|
|
||||||
|
|
||||||
instance.on('mouseleave', 'trail-hit', () => {
|
|
||||||
instance.getCanvas().style.cursor = ''
|
|
||||||
ontrailhover?.(null)
|
|
||||||
})
|
|
||||||
|
|
||||||
instance.on('click', 'trail-hit', (e) => {
|
|
||||||
if (!onplace) return
|
|
||||||
|
|
||||||
const hit = project(e)
|
|
||||||
if (!hit) return
|
|
||||||
|
|
||||||
// Ein offenes Marker-Popup gehört zur vorherigen Stelle.
|
|
||||||
popup?.remove()
|
|
||||||
onplace({ lat: hit.pos.lat, lng: hit.pos.lng, distance: hit.distance })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Die layerbezogenen Listener bleiben am Kartenobjekt haengen, auch wenn
|
|
||||||
* ihr Layer beim Stilwechsel verschwindet und gleich darauf wieder
|
|
||||||
* entsteht. Ein zweites bindTrailEvents wuerde sie deshalb nur
|
|
||||||
* verdoppeln.
|
|
||||||
*/
|
|
||||||
let trailEventsBound = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trail-Linie zeichnen bzw. aktualisieren. Zwei Aufrufer: der Effekt, wenn
|
|
||||||
* sich Strecke oder Zustand aendern, und `style.load` nach jedem
|
|
||||||
* Stilwechsel — dann ist die Quelle fort und muss neu angelegt werden.
|
|
||||||
*/
|
|
||||||
function drawTrail(instance: MapLibreMap) {
|
|
||||||
const data = geojson
|
|
||||||
const color = trailStatusColor(status)
|
|
||||||
const src = instance.getSource('trail') as maplibregl.GeoJSONSource | undefined
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
src?.setData({ type: 'FeatureCollection', features: [] })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const feature = { type: 'Feature' as const, properties: {}, geometry: data }
|
|
||||||
|
|
||||||
if (src) {
|
|
||||||
src.setData(feature)
|
|
||||||
instance.setPaintProperty('trail-line', 'line-color', color)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.addSource('trail', { type: 'geojson', data: feature })
|
|
||||||
instance.addLayer({
|
|
||||||
id: 'trail-line',
|
|
||||||
type: 'line',
|
|
||||||
source: 'trail',
|
|
||||||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
|
||||||
paint: { 'line-color': color, 'line-width': 4 },
|
|
||||||
})
|
|
||||||
instance.addLayer({
|
|
||||||
id: 'trail-hit',
|
|
||||||
type: 'line',
|
|
||||||
source: 'trail',
|
|
||||||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
|
||||||
paint: { 'line-color': color, 'line-width': 20, 'line-opacity': 0 },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!trailEventsBound) {
|
|
||||||
bindTrailEvents(instance)
|
|
||||||
trailEventsBound = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trail-Linie zeichnen bzw. aktualisieren
|
|
||||||
$effect(() => {
|
|
||||||
if (!map || !ready) return
|
|
||||||
drawTrail(map)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Beim Themenwechsel den ganzen Stil tauschen. Kacheln allein tun es
|
|
||||||
* nicht: Hell und dunkel unterscheiden sich hier nicht im Bild, sondern
|
|
||||||
* in der Zeichenvorschrift.
|
|
||||||
*/
|
|
||||||
$effect(() => {
|
|
||||||
if (!map || !ready) return
|
|
||||||
map.setStyle(isDark ? STYLE_DARK : STYLE_LIGHT)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kartenausschnitt auf den Track setzen
|
|
||||||
$effect(() => {
|
|
||||||
if (!map || !ready || !bounds) return
|
|
||||||
map.fitBounds(bounds, { padding: 40, duration: 400, maxZoom: 16 })
|
|
||||||
})
|
|
||||||
|
|
||||||
/** Popup-Inhalt zu einem Marker: was, wann, wer — plus Notiz. */
|
|
||||||
function popupContent(data: MarkerData): HTMLElement {
|
|
||||||
const root = document.createElement('div')
|
|
||||||
root.className = 'trail-popup'
|
|
||||||
|
|
||||||
const head = document.createElement('div')
|
|
||||||
head.className = 'trail-popup-head'
|
|
||||||
|
|
||||||
const icon = document.createElement('span')
|
|
||||||
icon.className = 'trail-popup-icon'
|
|
||||||
icon.style.color = data.color
|
|
||||||
mountIcon(icon, data.icon, 'size-4')
|
|
||||||
head.appendChild(icon)
|
|
||||||
|
|
||||||
const title = document.createElement('strong')
|
|
||||||
// textContent, nicht innerHTML: Label und Notiz sind Nutzereingaben.
|
|
||||||
title.textContent = data.label
|
|
||||||
head.appendChild(title)
|
|
||||||
|
|
||||||
if (data.resolved) {
|
|
||||||
const badge = document.createElement('span')
|
|
||||||
badge.className = 'trail-popup-badge'
|
|
||||||
badge.textContent = 'erledigt'
|
|
||||||
head.appendChild(badge)
|
|
||||||
}
|
|
||||||
|
|
||||||
root.appendChild(head)
|
|
||||||
|
|
||||||
if (data.note) {
|
|
||||||
const note = document.createElement('p')
|
|
||||||
note.className = 'trail-popup-note'
|
|
||||||
note.textContent = data.note
|
|
||||||
root.appendChild(note)
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = document.createElement('p')
|
|
||||||
meta.className = 'trail-popup-meta'
|
|
||||||
meta.textContent = [data.when, data.who].filter(Boolean).join(' · ')
|
|
||||||
if (meta.textContent) root.appendChild(meta)
|
|
||||||
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marker neu aufbauen — bei überschaubaren Mengen einfacher und
|
|
||||||
// zuverlässiger als einzelne Aktualisierungen.
|
|
||||||
$effect(() => {
|
|
||||||
if (!map || !ready) return
|
|
||||||
const instance = map
|
|
||||||
|
|
||||||
for (const m of markerObjects) m.remove()
|
|
||||||
markerObjects = []
|
|
||||||
clearIcons()
|
|
||||||
|
|
||||||
for (const data of markers) {
|
|
||||||
const el = document.createElement('button')
|
|
||||||
el.type = 'button'
|
|
||||||
el.title = data.label
|
|
||||||
el.setAttribute('aria-label', data.label)
|
|
||||||
el.style.cssText = `
|
|
||||||
width: 26px; height: 26px; border-radius: 9999px;
|
|
||||||
border: 2px solid white; cursor: pointer; padding: 0;
|
|
||||||
display: grid; place-items: center; color: white;
|
|
||||||
background: ${data.color};
|
|
||||||
opacity: ${data.resolved ? '0.35' : '1'};
|
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,.4);
|
|
||||||
`
|
|
||||||
mountIcon(el, data.icon, 'size-3.5')
|
|
||||||
// Der Klick bleibt beim Marker: Er darf kein neues Marker-Fenster
|
|
||||||
// öffnen, sondern zeigt nur, was hier schon gemeldet wurde.
|
|
||||||
el.addEventListener('click', (ev) => {
|
|
||||||
ev.stopPropagation()
|
|
||||||
|
|
||||||
popup?.remove()
|
|
||||||
popup = new maplibregl.Popup({
|
|
||||||
offset: 14,
|
|
||||||
closeButton: false,
|
|
||||||
className: 'trail-popup-shell',
|
|
||||||
})
|
|
||||||
.setLngLat([data.lng, data.lat])
|
|
||||||
.setDOMContent(popupContent(data))
|
|
||||||
.addTo(instance)
|
|
||||||
})
|
|
||||||
el.addEventListener('focus', () => {
|
|
||||||
el.style.outline = '2px solid #0ea5e9'
|
|
||||||
el.style.outlineOffset = '2px'
|
|
||||||
})
|
|
||||||
el.addEventListener('blur', () => {
|
|
||||||
el.style.outline = 'none'
|
|
||||||
})
|
|
||||||
|
|
||||||
markerObjects.push(
|
|
||||||
new maplibregl.Marker({ element: el })
|
|
||||||
.setLngLat([data.lng, data.lat])
|
|
||||||
.addTo(instance),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Stelle aus dem Höhenprofil hervorheben. Die Kopplung läuft über die
|
|
||||||
// Distanz, nicht über den Index: elevation hat einen Eintrag pro
|
|
||||||
// Originalpunkt, coordinates nur die vereinfachten.
|
|
||||||
$effect(() => {
|
|
||||||
if (!map || !ready) return
|
|
||||||
|
|
||||||
highlightMarker?.remove()
|
|
||||||
highlightMarker = null
|
|
||||||
|
|
||||||
const coords = geojson?.coordinates
|
|
||||||
if (highlightDistance == null || !coords?.length) return
|
|
||||||
|
|
||||||
const at = positionAtDistance(coords, coordDistances, highlightDistance)
|
|
||||||
if (!at) return
|
|
||||||
|
|
||||||
const el = document.createElement('div')
|
|
||||||
el.style.cssText = `
|
|
||||||
width: 12px; height: 12px; border-radius: 9999px;
|
|
||||||
background: #0ea5e9; border: 2px solid white;
|
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,.4);
|
|
||||||
`
|
|
||||||
|
|
||||||
highlightMarker = new maplibregl.Marker({ element: el }).setLngLat(at).addTo(map)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={container}
|
|
||||||
style:height
|
|
||||||
class="w-full rounded-lg overflow-hidden border"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Popups hängt maplibre selbst in den Karten-Container, deshalb greifen die
|
|
||||||
Regeln nur global. Bewusst helle Fläche wie das maplibre-Standardpopup —
|
|
||||||
im Dunkelmodus liegt es trotzdem lesbar über der hellen Karte.
|
|
||||||
-->
|
|
||||||
<style>
|
|
||||||
:global(.trail-popup-shell .maplibregl-popup-content) {
|
|
||||||
padding: 0.6rem 0.75rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow: 0 4px 12px rgb(0 0 0 / 0.18);
|
|
||||||
font-family: inherit;
|
|
||||||
max-width: 15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.trail-popup-head) {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.trail-popup-icon) {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.trail-popup-badge) {
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
padding: 0 0.35rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
background: #e2e8f0;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.trail-popup-note) {
|
|
||||||
margin: 0.35rem 0 0;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.trail-popup-meta) {
|
|
||||||
margin: 0.35rem 0 0;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<AccordionPrimitive.Content
|
|
||||||
bind:ref
|
|
||||||
data-slot="accordion-content"
|
|
||||||
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
<div class={cn("pb-4 pt-0", className)}>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
</AccordionPrimitive.Content>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: AccordionPrimitive.ItemProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<AccordionPrimitive.Item
|
|
||||||
bind:ref
|
|
||||||
data-slot="accordion-item"
|
|
||||||
class={cn("border-b last:border-b-0", className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
|
||||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
level = 3,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
|
|
||||||
level?: AccordionPrimitive.HeaderProps["level"];
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<AccordionPrimitive.Header {level} class="flex">
|
|
||||||
<AccordionPrimitive.Trigger
|
|
||||||
data-slot="accordion-trigger"
|
|
||||||
bind:ref
|
|
||||||
class={cn(
|
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium outline-none transition-all hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
<ChevronDownIcon
|
|
||||||
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
|
|
||||||
/>
|
|
||||||
</AccordionPrimitive.Trigger>
|
|
||||||
</AccordionPrimitive.Header>
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
value = $bindable(),
|
|
||||||
...restProps
|
|
||||||
}: AccordionPrimitive.RootProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<AccordionPrimitive.Root
|
|
||||||
bind:ref
|
|
||||||
bind:value={value as never}
|
|
||||||
data-slot="accordion"
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
<script lang="ts" module>
|
|
||||||
import { type VariantProps, tv } from "tailwind-variants";
|
|
||||||
|
|
||||||
export const badgeVariants = tv({
|
|
||||||
base: "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none",
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
||||||
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
|
||||||
destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
|
|
||||||
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
||||||
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
href,
|
|
||||||
class: className,
|
|
||||||
variant = "default",
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
|
||||||
variant?: BadgeVariant;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:element
|
|
||||||
this={href ? "a" : "span"}
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="badge"
|
|
||||||
{href}
|
|
||||||
class={cn(badgeVariants({ variant }), className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</svelte:element>
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export { default as Badge } from "./badge.svelte";
|
|
||||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
<script lang="ts" module>
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
|
||||||
import { type VariantProps, tv } from "tailwind-variants";
|
|
||||||
|
|
||||||
export const buttonVariants = tv({
|
|
||||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:not-aria-[haspopup]:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
||||||
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
||||||
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
||||||
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
||||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
||||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
||||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
||||||
icon: "size-8",
|
|
||||||
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
||||||
"icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
||||||
"icon-lg": "size-9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
|
||||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
|
||||||
|
|
||||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
|
||||||
WithElementRef<HTMLAnchorAttributes> & {
|
|
||||||
variant?: ButtonVariant;
|
|
||||||
size?: ButtonSize;
|
|
||||||
/**
|
|
||||||
* Hinweistext als Tooltip. Ersetzt `title` appweit und dient
|
|
||||||
* zugleich als Beschriftung, wenn der Knopf nur ein Icon zeigt —
|
|
||||||
* ohne das bliebe er für Screenreader namenlos.
|
|
||||||
*/
|
|
||||||
tooltip?: string;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { tooltip as tooltipAction } from "$lib/stores/app.svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
class: className,
|
|
||||||
variant = "default",
|
|
||||||
size = "default",
|
|
||||||
ref = $bindable(null),
|
|
||||||
href = undefined,
|
|
||||||
type = "button",
|
|
||||||
disabled,
|
|
||||||
tooltip,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: ButtonProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if href}
|
|
||||||
<a
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="button"
|
|
||||||
class={cn(buttonVariants({ variant, size }), className)}
|
|
||||||
href={disabled ? undefined : href}
|
|
||||||
aria-disabled={disabled}
|
|
||||||
role={disabled ? "link" : undefined}
|
|
||||||
tabindex={disabled ? -1 : undefined}
|
|
||||||
aria-label={tooltip}
|
|
||||||
{...restProps}
|
|
||||||
use:tooltipAction={{ content: tooltip }}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</a>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="button"
|
|
||||||
class={cn(buttonVariants({ variant, size }), className)}
|
|
||||||
{type}
|
|
||||||
{disabled}
|
|
||||||
aria-label={tooltip}
|
|
||||||
{...restProps}
|
|
||||||
use:tooltipAction={{ content: tooltip }}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="card-action"
|
|
||||||
class={cn(
|
|
||||||
"cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="card-content"
|
|
||||||
class={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<p
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="card-description"
|
|
||||||
class={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</p>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="card-footer"
|
|
||||||
class={cn("bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="card-header"
|
|
||||||
class={cn(
|
|
||||||
"gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="card-title"
|
|
||||||
class={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
size = "default",
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="card"
|
|
||||||
data-size={size}
|
|
||||||
class={cn("ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>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?.()}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
type = "button",
|
|
||||||
...restProps
|
|
||||||
}: DialogPrimitive.CloseProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {type} {...restProps} />
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
|
||||||
import DialogPortal from "./dialog-portal.svelte";
|
|
||||||
import type { Snippet } from "svelte";
|
|
||||||
import * as Dialog from "./index.js";
|
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
|
||||||
import type { ComponentProps } from "svelte";
|
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
|
||||||
import XIcon from '@lucide/svelte/icons/x';
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
portalProps,
|
|
||||||
children,
|
|
||||||
showCloseButton = true,
|
|
||||||
...restProps
|
|
||||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
|
||||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
|
||||||
children: Snippet;
|
|
||||||
showCloseButton?: boolean;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DialogPortal {...portalProps}>
|
|
||||||
<Dialog.Overlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
bind:ref
|
|
||||||
data-slot="dialog-content"
|
|
||||||
class={cn(
|
|
||||||
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
{#if showCloseButton}
|
|
||||||
<DialogPrimitive.Close data-slot="dialog-close">
|
|
||||||
{#snippet child({ props })}
|
|
||||||
<Button variant="ghost" class="absolute top-2 right-2" size="icon-sm" {...props}>
|
|
||||||
<XIcon />
|
|
||||||
<span class="sr-only">Close</span>
|
|
||||||
</Button>
|
|
||||||
{/snippet}
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
{/if}
|
|
||||||
</DialogPrimitive.Content>
|
|
||||||
</DialogPortal>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: DialogPrimitive.DescriptionProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
bind:ref
|
|
||||||
data-slot="dialog-description"
|
|
||||||
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
showCloseButton = false,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
||||||
showCloseButton?: boolean;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="dialog-footer"
|
|
||||||
class={cn("bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
{#if showCloseButton}
|
|
||||||
<DialogPrimitive.Close>
|
|
||||||
{#snippet child({ props })}
|
|
||||||
<Button variant="outline" {...props}>Close</Button>
|
|
||||||
{/snippet}
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="dialog-header"
|
|
||||||
class={cn("gap-2 flex flex-col", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: DialogPrimitive.OverlayProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DialogPrimitive.Overlay
|
|
||||||
bind:ref
|
|
||||||
data-slot="dialog-overlay"
|
|
||||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let { ...restProps }: DialogPrimitive.PortalProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DialogPrimitive.Portal {...restProps} />
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: DialogPrimitive.TitleProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
bind:ref
|
|
||||||
data-slot="dialog-title"
|
|
||||||
class={cn("text-base leading-none font-medium", className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
type = "button",
|
|
||||||
...restProps
|
|
||||||
}: DialogPrimitive.TriggerProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {type} {...restProps} />
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DialogPrimitive.Root bind:open {...restProps} />
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
value = $bindable([]),
|
|
||||||
...restProps
|
|
||||||
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.CheckboxGroup
|
|
||||||
bind:ref
|
|
||||||
bind:value
|
|
||||||
data-slot="dropdown-menu-checkbox-group"
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
|
||||||
import CheckIcon from '@lucide/svelte/icons/check';
|
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
|
||||||
import type { Snippet } from "svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
checked = $bindable(false),
|
|
||||||
indeterminate = $bindable(false),
|
|
||||||
class: className,
|
|
||||||
children: childrenProp,
|
|
||||||
...restProps
|
|
||||||
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
|
|
||||||
children?: Snippet;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
bind:ref
|
|
||||||
bind:checked
|
|
||||||
bind:indeterminate
|
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
|
||||||
class={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{#snippet children({ checked, indeterminate })}
|
|
||||||
<span
|
|
||||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
|
||||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
|
||||||
>
|
|
||||||
{#if indeterminate}
|
|
||||||
<MinusIcon />
|
|
||||||
{:else if checked}
|
|
||||||
<CheckIcon />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{@render childrenProp?.()}
|
|
||||||
{/snippet}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
|
||||||
import DropdownMenuPortal from "./dropdown-menu-portal.svelte";
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
import type { ComponentProps } from "svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
sideOffset = 4,
|
|
||||||
align = "start",
|
|
||||||
portalProps,
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: DropdownMenuPrimitive.ContentProps & {
|
|
||||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPortal {...portalProps}>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
bind:ref
|
|
||||||
data-slot="dropdown-menu-content"
|
|
||||||
{sideOffset}
|
|
||||||
{align}
|
|
||||||
class={cn(
|
|
||||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-(--bits-dropdown-menu-anchor-width) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPortal>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
import type { ComponentProps } from "svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
inset,
|
|
||||||
...restProps
|
|
||||||
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
|
|
||||||
inset?: boolean;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.GroupHeading
|
|
||||||
bind:ref
|
|
||||||
data-slot="dropdown-menu-group-heading"
|
|
||||||
data-inset={inset}
|
|
||||||
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8", className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
inset,
|
|
||||||
variant = "default",
|
|
||||||
...restProps
|
|
||||||
}: DropdownMenuPrimitive.ItemProps & {
|
|
||||||
inset?: boolean;
|
|
||||||
variant?: "default" | "destructive";
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
bind:ref
|
|
||||||
data-slot="dropdown-menu-item"
|
|
||||||
data-inset={inset}
|
|
||||||
data-variant={variant}
|
|
||||||
class={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
inset,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
||||||
inset?: boolean;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="dropdown-menu-label"
|
|
||||||
data-inset={inset}
|
|
||||||
class={cn("text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7 data-[inset]:pl-8", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Portal {...restProps} />
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
value = $bindable(),
|
|
||||||
...restProps
|
|
||||||
}: DropdownMenuPrimitive.RadioGroupProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
|
||||||
bind:ref
|
|
||||||
bind:value
|
|
||||||
data-slot="dropdown-menu-radio-group"
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
import CheckIcon from '@lucide/svelte/icons/check';
|
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children: childrenProp,
|
|
||||||
...restProps
|
|
||||||
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
bind:ref
|
|
||||||
data-slot="dropdown-menu-radio-item"
|
|
||||||
class={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{#snippet children({ checked })}
|
|
||||||
<span
|
|
||||||
class="absolute right-2 flex items-center justify-center pointer-events-none"
|
|
||||||
data-slot="dropdown-menu-radio-item-indicator"
|
|
||||||
>
|
|
||||||
{#if checked}
|
|
||||||
<CheckIcon />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{@render childrenProp?.({ checked })}
|
|
||||||
{/snippet}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: DropdownMenuPrimitive.SeparatorProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
bind:ref
|
|
||||||
data-slot="dropdown-menu-separator"
|
|
||||||
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<span
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="dropdown-menu-shortcut"
|
|
||||||
class={cn("text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</span>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: DropdownMenuPrimitive.SubContentProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
bind:ref
|
|
||||||
data-slot="dropdown-menu-sub-content"
|
|
||||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-lg p-1 shadow-lg ring-1 duration-100 w-auto", className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
inset,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: DropdownMenuPrimitive.SubTriggerProps & {
|
|
||||||
inset?: boolean;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
bind:ref
|
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
|
||||||
data-inset={inset}
|
|
||||||
class={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
<ChevronRightIcon class="ml-auto" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Sub bind:open {...restProps} />
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<DropdownMenuPrimitive.Root bind:open {...restProps} />
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import Root from "./input.svelte";
|
|
||||||
|
|
||||||
export {
|
|
||||||
Root,
|
|
||||||
//
|
|
||||||
Root as Input,
|
|
||||||
};
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
|
|
||||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
|
||||||
|
|
||||||
type Props = WithElementRef<
|
|
||||||
Omit<HTMLInputAttributes, "type"> &
|
|
||||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
|
||||||
>;
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
value = $bindable(),
|
|
||||||
type,
|
|
||||||
files = $bindable(),
|
|
||||||
class: className,
|
|
||||||
"data-slot": dataSlot = "input",
|
|
||||||
...restProps
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if type === "file"}
|
|
||||||
<input
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot={dataSlot}
|
|
||||||
class={cn(
|
|
||||||
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
type="file"
|
|
||||||
bind:files
|
|
||||||
bind:value
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<input
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot={dataSlot}
|
|
||||||
class={cn(
|
|
||||||
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors file:h-6 file:text-sm file:font-medium focus-visible:ring-3 aria-invalid:ring-3 md:text-sm file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{type}
|
|
||||||
bind:value
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import Root from "./label.svelte";
|
|
||||||
|
|
||||||
export {
|
|
||||||
Root,
|
|
||||||
//
|
|
||||||
Root as Label,
|
|
||||||
};
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Label as LabelPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: LabelPrimitive.RootProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
bind:ref
|
|
||||||
data-slot="label"
|
|
||||||
class={cn(
|
|
||||||
"gap-2 text-sm leading-none font-medium group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import Content from "./navigation-menu-content.svelte";
|
|
||||||
import Indicator from "./navigation-menu-indicator.svelte";
|
|
||||||
import Item from "./navigation-menu-item.svelte";
|
|
||||||
import Link from "./navigation-menu-link.svelte";
|
|
||||||
import List from "./navigation-menu-list.svelte";
|
|
||||||
import Trigger from "./navigation-menu-trigger.svelte";
|
|
||||||
import Viewport from "./navigation-menu-viewport.svelte";
|
|
||||||
import Root from "./navigation-menu.svelte";
|
|
||||||
|
|
||||||
export {
|
|
||||||
Root,
|
|
||||||
Content,
|
|
||||||
Indicator,
|
|
||||||
Item,
|
|
||||||
Link,
|
|
||||||
List,
|
|
||||||
Trigger,
|
|
||||||
Viewport,
|
|
||||||
//
|
|
||||||
Root as NavigationMenuRoot,
|
|
||||||
Content as NavigationMenuContent,
|
|
||||||
Indicator as NavigationMenuIndicator,
|
|
||||||
Item as NavigationMenuItem,
|
|
||||||
Link as NavigationMenuLink,
|
|
||||||
List as NavigationMenuList,
|
|
||||||
Trigger as NavigationMenuTrigger,
|
|
||||||
Viewport as NavigationMenuViewport,
|
|
||||||
};
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: NavigationMenuPrimitive.ContentProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NavigationMenuPrimitive.Content
|
|
||||||
bind:ref
|
|
||||||
data-slot="navigation-menu-content"
|
|
||||||
class={cn(
|
|
||||||
"p-1 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-lg group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:duration-300 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 top-0 left-0 w-full group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none md:absolute md:w-auto",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: NavigationMenuPrimitive.IndicatorProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NavigationMenuPrimitive.Indicator
|
|
||||||
bind:ref
|
|
||||||
data-slot="navigation-menu-indicator"
|
|
||||||
class={cn(
|
|
||||||
"data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:animate-in data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
<div class="rounded-tl-sm bg-border shadow-md relative top-[60%] h-2 w-2 rotate-45"></div>
|
|
||||||
</NavigationMenuPrimitive.Indicator>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: NavigationMenuPrimitive.ItemProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NavigationMenuPrimitive.Item
|
|
||||||
bind:ref
|
|
||||||
data-slot="navigation-menu-item"
|
|
||||||
class={cn("cn-navigation-menu-item relative", className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: NavigationMenuPrimitive.LinkProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NavigationMenuPrimitive.Link
|
|
||||||
bind:ref
|
|
||||||
data-slot="navigation-menu-link"
|
|
||||||
class={cn("flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none hover:bg-muted focus:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-1 in-data-[slot=navigation-menu-content]:rounded-md data-active:bg-muted/50 data-active:hover:bg-muted data-active:focus:bg-muted [&_svg:not([class*='size-'])]:size-4", className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: NavigationMenuPrimitive.ListProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NavigationMenuPrimitive.List
|
|
||||||
bind:ref
|
|
||||||
data-slot="navigation-menu-list"
|
|
||||||
class={cn(
|
|
||||||
"gap-0 group flex flex-1 list-none items-center justify-center",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<script lang="ts" module>
|
|
||||||
import { tv } from "tailwind-variants";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
export const navigationMenuTriggerStyle = tv({
|
|
||||||
base: "rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all hover:bg-muted focus:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-1 disabled:opacity-50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted data-open:bg-muted/50 data-open:hover:bg-muted data-open:focus:bg-muted group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center outline-none disabled:pointer-events-none",
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
|
||||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: NavigationMenuPrimitive.TriggerProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NavigationMenuPrimitive.Trigger
|
|
||||||
bind:ref
|
|
||||||
data-slot="navigation-menu-trigger"
|
|
||||||
class={cn(navigationMenuTriggerStyle(), "group", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
<ChevronDownIcon class="relative top-px ml-1 size-3 transition duration-300 group-data-popup-open/navigation-menu-trigger:rotate-180 group-data-open/navigation-menu-trigger:rotate-180" aria-hidden="true" />
|
|
||||||
</NavigationMenuPrimitive.Trigger>
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: NavigationMenuPrimitive.ViewportProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class={cn("absolute start-0 top-full isolate z-50 flex justify-center")}>
|
|
||||||
<NavigationMenuPrimitive.Viewport
|
|
||||||
bind:ref
|
|
||||||
data-slot="navigation-menu-viewport"
|
|
||||||
class={cn(
|
|
||||||
"rounded-lg bg-popover text-popover-foreground shadow ring-1 ring-foreground/10 duration-100 data-open:animate-in data-open:zoom-in-90 data-closed:animate-out data-closed:zoom-out-90 origin-top-center relative mt-1.5 h-[calc(var(--bits-navigation-menu-viewport-height)+1rem)] w-full overflow-hidden md:w-[calc(var(--bits-navigation-menu-viewport-width)+1rem)]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
import NavigationMenuViewport from "./navigation-menu-viewport.svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
viewport = true,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: NavigationMenuPrimitive.RootProps & {
|
|
||||||
viewport?: boolean;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NavigationMenuPrimitive.Root
|
|
||||||
bind:ref
|
|
||||||
data-slot="navigation-menu"
|
|
||||||
data-viewport={viewport}
|
|
||||||
class={cn(
|
|
||||||
"max-w-max group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
{#if viewport}
|
|
||||||
<NavigationMenuViewport />
|
|
||||||
{/if}
|
|
||||||
</NavigationMenuPrimitive.Root>
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
|
||||||
import SelectPortal from "./select-portal.svelte";
|
|
||||||
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
|
||||||
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
|
||||||
import type { ComponentProps } from "svelte";
|
|
||||||
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
sideOffset = 4,
|
|
||||||
portalProps,
|
|
||||||
children,
|
|
||||||
preventScroll = true,
|
|
||||||
...restProps
|
|
||||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
|
||||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SelectPortal {...portalProps}>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
bind:ref
|
|
||||||
{sideOffset}
|
|
||||||
{preventScroll}
|
|
||||||
data-slot="select-content"
|
|
||||||
class={cn(
|
|
||||||
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-lg shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 overflow-x-hidden overflow-y-auto",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.Viewport
|
|
||||||
class={cn(
|
|
||||||
"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPortal>
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
import type { ComponentProps } from "svelte";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SelectPrimitive.GroupHeading
|
|
||||||
bind:ref
|
|
||||||
data-slot="select-group-heading"
|
|
||||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</SelectPrimitive.GroupHeading>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: SelectPrimitive.GroupProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SelectPrimitive.Group
|
|
||||||
bind:ref
|
|
||||||
data-slot="select-group"
|
|
||||||
class={cn("scroll-my-1 p-1", className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
|
||||||
import CheckIcon from '@lucide/svelte/icons/check';
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
children: childrenProp,
|
|
||||||
...restProps
|
|
||||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
bind:ref
|
|
||||||
{value}
|
|
||||||
data-slot="select-item"
|
|
||||||
class={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 focus:bg-accent data-highlighted:bg-accent data-highlighted:text-accent-foreground focus:text-accent-foreground relative flex w-full cursor-default items-center outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{#snippet children({ selected, highlighted })}
|
|
||||||
<span class="absolute end-2 flex size-3.5 items-center justify-center">
|
|
||||||
{#if selected}
|
|
||||||
<CheckIcon class="cn-select-item-indicator-icon" />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{#if childrenProp}
|
|
||||||
{@render childrenProp({ selected, highlighted })}
|
|
||||||
{:else}
|
|
||||||
{label || value}
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
...restProps
|
|
||||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={ref}
|
|
||||||
data-slot="select-label"
|
|
||||||
class={cn("text-muted-foreground px-1.5 py-1 text-xs", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let { ...restProps }: SelectPrimitive.PortalProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SelectPrimitive.Portal {...restProps} />
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
|
||||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
bind:ref
|
|
||||||
data-slot="select-scroll-down-button"
|
|
||||||
class={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
<ChevronDownIcon />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
|
||||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
|
||||||
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
bind:ref
|
|
||||||
data-slot="select-scroll-up-button"
|
|
||||||
class={cn("bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full", className)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
<ChevronUpIcon />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
|
||||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
|
||||||
import { cn } from "$lib/utils.js";
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
...restProps
|
|
||||||
}: SeparatorPrimitive.RootProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Separator
|
|
||||||
bind:ref
|
|
||||||
data-slot="select-separator"
|
|
||||||
class={cn("bg-border -mx-1 my-1 h-px pointer-events-none", className)}
|
|
||||||
{...restProps}
|
|
||||||
/>
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
|
||||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
|
||||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
|
||||||
|
|
||||||
let {
|
|
||||||
ref = $bindable(null),
|
|
||||||
class: className,
|
|
||||||
children,
|
|
||||||
size = "default",
|
|
||||||
...restProps
|
|
||||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
|
||||||
size?: "sm" | "default";
|
|
||||||
} = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
bind:ref
|
|
||||||
data-slot="select-trigger"
|
|
||||||
data-size={size}
|
|
||||||
class={cn(
|
|
||||||
"border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-lg border bg-transparent py-2 pr-2 pl-2.5 text-sm transition-colors select-none focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...restProps}
|
|
||||||
>
|
|
||||||
{@render children?.()}
|
|
||||||
<ChevronDownIcon class="text-muted-foreground size-4 pointer-events-none" />
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { Select as SelectPrimitive } from "bits-ui";
|
|
||||||
|
|
||||||
let {
|
|
||||||
open = $bindable(false),
|
|
||||||
value = $bindable(),
|
|
||||||
...restProps
|
|
||||||
}: SelectPrimitive.RootProps = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import Root from "./separator.svelte";
|
|
||||||
|
|
||||||
export {
|
|
||||||
Root,
|
|
||||||
//
|
|
||||||
Root as Separator,
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue