feat: Aus der App wird ein Wegweiser
Diese Adresse führte bis September 2026 eine Anwendung: Zeitnahme, Kader, Trails, Renntage. Die heißt jetzt Kurbeler, liegt unter kurbeler.de und führt den Stammtisch als eines von mehreren Teams. Was hier bleibt, ist die Adresse — und die soll niemanden ins Leere laufen lassen. Übrig sind drei vorgerenderte Seiten: Wappen, ein Satz, ein Knopf, dazu Impressum und Datenschutz. Kein Konto, keine Datenbank, kein Formular, keine Cookies, keine Schriften oder Karten von fremden Servern. Die Datenschutzerklärung ist deshalb kurz und nicht die von Kurbeler mit Streichungen — eine Erklärung, die von Konten und Benachrichtigungen spricht, wäre auf dieser Seite schlicht falsch. `adapter-static` statt `adapter-node`, ausgeliefert von nginx: Für drei Seiten ohne eine dynamische Angabe einen JavaScript-Prozess laufen zu lassen, der auf Anfragen wartet, um jedes Mal dieselbe Datei herauszugeben, wäre Beschäftigung ohne Arbeit. Von den Abhängigkeiten bleiben die zwölf, die zum Bauen nötig sind; zur Laufzeit keine einzige. Das Bild ist damit 492 kB statt elf Megabyte — zwei alte Grafiken von je 2,4 MB lagen ungenutzt in `static/`. Das Backend bleibt unangetastet. Es wird vom Wegweiser nicht benutzt, aber darin liegen die Daten des alten Deployments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LTw6xVYgjMy9AQtfs1Gdyu
This commit is contained in:
parent
f773c52b0a
commit
c39599bb61
164 changed files with 814 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
|
||||||
31
frontend/Dockerfile
Normal file
31
frontend/Dockerfile
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Vorkomprimierte Fassungen liegen dank `precompress` im Build daneben
|
||||||
|
# (.gz/.br). Ohne diese Zeile ignoriert nginx sie und packt jede Antwort neu.
|
||||||
|
RUN sed -i 's/^}/ gzip_static on;\n}/' /etc/nginx/nginx.conf || true
|
||||||
|
|
||||||
|
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
|
||||||
29
frontend/nginx.conf
Normal file
29
frontend/nginx.conf
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Ein Server, ein Verzeichnis, drei Seiten.
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# 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