Add design specs and personas
Feature spec, system design, design system (colors/typography/components), and per-view HTML specs for Erbstücke Wannsee. Also includes Claude personas used during design sessions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
# Erbstücke-Reservierung — Design-Spezifikation
|
||||
|
||||
**Datum:** 2026-05-04
|
||||
**Status:** Freigegeben zur Implementierung
|
||||
**Sprache:** Deutsch (gesamte UI)
|
||||
|
||||
---
|
||||
|
||||
## 1. Projektziel & Kontext
|
||||
|
||||
Eine kurzlebige Webanwendung, die Familienmitgliedern ermöglicht, Gegenstände aus dem Nachlass der Großmutter zu reservieren. Das Projekt läuft bis alle Gegenstände vergeben sind und wird dann geschlossen. Einfachheit und schnelle Umsetzung haben Vorrang vor Langlebigkeit.
|
||||
|
||||
**Primäre Akteure:**
|
||||
- **Familienmitglieder** — reservieren Artikel, kein Konto erforderlich
|
||||
- **Admins** (Marcel + Tante) — verwalten Inventar und Zugangscodes
|
||||
|
||||
**Nicht-Ziele:**
|
||||
- Kein Text-Volltextsuche (Stöbern ist visuell)
|
||||
- Keine E-Mail- oder SMS-Versandautomatik für Codes
|
||||
- Keine Auktions- oder Bietfunktion
|
||||
- Keine Zahlungsabwicklung
|
||||
- Keine Mehrsprachigkeit (nur Deutsch)
|
||||
|
||||
---
|
||||
|
||||
## 2. Nutzerrollen & Zugang
|
||||
|
||||
### 2.1 Familienmitglieder (Code-basiert)
|
||||
|
||||
- Zugang ausschließlich per persönlichem Code
|
||||
- **Primärer Weg:** Link mit Code als URL-Parameter (z. B. `?code=AB3K7MN2`) — Code wird automatisch extrahiert und in `localStorage` gespeichert
|
||||
- **Fallback:** manuelle Code-Eingabe auf dem Gate Screen
|
||||
- Code ist alphanumerisch, kurz genug zum manuellen Eintippen (8 Zeichen, Großbuchstaben + Ziffern)
|
||||
- Jeder Code hat genau einen **Display-Namen**, der vom Admin beim Erstellen vergeben wird
|
||||
- Der Display-Name wird in der App oben angezeigt: „Angemeldet als: [Name]"
|
||||
- **Kein Code = kein Inhalt sichtbar** — der Gate Screen zeigt nichts außer dem Code-Eingabefeld
|
||||
|
||||
### 2.2 Admins (Konto-basiert)
|
||||
|
||||
- Eigene Login-Seite mit Benutzername und Passwort
|
||||
- Zwei feste Admin-Konten (Marcel, Tante) — kein Selbstregistrierungssystem
|
||||
- Admins sehen eine separate Admin-Oberfläche, nicht die Familiengalerie
|
||||
|
||||
---
|
||||
|
||||
## 3. Datenmodell
|
||||
|
||||
| Entität | Felder |
|
||||
|---|---|
|
||||
| **Artikel** | ID · Titel (optional) · Fotos (geordnete Liste, erstes = Galerie-Thumbnail) · Kategorie · Notiz (optional) · Erstelldatum |
|
||||
| **Code** | ID · Code-String · Display-Name · Erstelldatum |
|
||||
| **Reservierung** | ID · Artikel-ID · Code-ID · Zeitstempel |
|
||||
|
||||
**Kategorien (fix, nicht erweiterbar durch User):**
|
||||
Bücher · Möbel · Küchenutensilien · Schmuck · Kunstwerke · Kleidung · Werkzeug · Dokumente/Erinnerungsstücke
|
||||
|
||||
---
|
||||
|
||||
## 4. Familienmitglied-Erlebnis
|
||||
|
||||
### 4.1 Gate Screen (kein Code vorhanden)
|
||||
|
||||
- Minimale Ansicht: App-Name/Icon, einzeiliger Erklärungstext, Code-Eingabefeld (zentriert, Großbuchstaben, Letter-Spacing), „Weiter"-Button
|
||||
- Hinweis: „Noch kein Code? Wende dich an Marcel oder Tante."
|
||||
- Falscher Code → Fehlermeldung: „Code nicht bekannt — bitte prüfe die Eingabe."
|
||||
- Richtiger Code → Session wird gespeichert, direkt zur Galerie
|
||||
|
||||
### 4.2 Galerie
|
||||
|
||||
**Layout (responsiv):**
|
||||
- Telefon (≤ 767 px): 1 Spalte — horizontale Karte (Foto links 72 px, Kategorie + Status rechts)
|
||||
- Tablet (768–1023 px): 2 Spalten — quadratische Karten (Foto oben, Kategorie + Status unten)
|
||||
- Desktop (≥ 1024 px): 2 Spalten — wie Tablet, aber in einem zentrierten Container mit horizontalem Padding (kein 3-Spalten-Layout)
|
||||
|
||||
**Kategorie-Filter:**
|
||||
- Horizontale Tab-Leiste oben, scrollbar bei Überlauf
|
||||
- Tabs: „Alle" + eine Schaltfläche pro Kategorie
|
||||
- Jeweils nur ein Filter aktiv
|
||||
|
||||
**Status-Anzeige pro Karte:**
|
||||
- Frei: grüner Text „✓ Frei"
|
||||
- Reserviert von mir: grüner Badge „✓ Meine Reservierung"
|
||||
- Reserviert von jemand anderem: roter Text „● [Display-Name]"
|
||||
|
||||
### 4.3 Artikel-Modal
|
||||
|
||||
Öffnet beim Antippen einer Karte. Enthält:
|
||||
- Foto-Galerie (wischbar, Zähler „1 / 3 →")
|
||||
- Kategorie-Badge
|
||||
- Optionale Notiz (nur wenn vorhanden)
|
||||
- Reservierungsstatus-Bereich (passt sich an):
|
||||
- **Frei:** „Reservieren"-Button (grün)
|
||||
- **Von mir reserviert:** Badge „✓ Meine Reservierung" + „Reservierung aufheben"-Button (Outline, rot)
|
||||
- **Von jemand anderem:** „Reserviert von [Name]" + deaktivierter Button „Nicht verfügbar"
|
||||
|
||||
**Reservierungslogik:**
|
||||
- Erste Reservierung gewinnt (keine Konfliktvermittlung durch Admins)
|
||||
- Familienmitglieder können andere kontaktieren — dafür ist der Display-Name sichtbar
|
||||
- Kein „Vielleicht"/„Auf Wunschliste" — nur reserviert oder frei
|
||||
|
||||
---
|
||||
|
||||
## 5. Admin-Erlebnis
|
||||
|
||||
### 5.1 Navigation
|
||||
|
||||
- Sidebar (dunkel, linksbündig) mit vier Bereichen
|
||||
- Auf Mobilgeräten: Sidebar klappt zu Hamburger-Menü
|
||||
- Bereiche: Inventar · Codes · Reservierungen · Übersicht
|
||||
|
||||
### 5.2 Inventar
|
||||
|
||||
**Listenansicht:**
|
||||
- Thumbnail + Kategorie + Reservierungsstatus pro Zeile
|
||||
- „+ Artikel hinzufügen"-Button oben
|
||||
- Artikel bearbeiten und löschen möglich
|
||||
- Löschen nur wenn Artikel nicht reserviert ist (sonst Hinweis: Reservierung zuerst aufheben)
|
||||
|
||||
**Artikel hinzufügen / bearbeiten — Kamera-first (mobile-optimiert):**
|
||||
1. Großer Kamera-Button oben → öffnet direkt Gerätekamera (kein Datei-Browser als Einstieg)
|
||||
2. Aufgenommene Fotos erscheinen als Thumbnail-Streifen; weiteres Foto hinzufügen möglich
|
||||
3. Erstes Foto = Galerie-Thumbnail (Reihenfolge verschiebbar)
|
||||
4. Kategorie-Dropdown (Pflichtfeld)
|
||||
5. Titel-Textfeld (optional)
|
||||
6. Notiz-Freitextfeld (optional)
|
||||
6. „Speichern" / „Abbrechen"
|
||||
|
||||
### 5.3 Code-Verwaltung
|
||||
|
||||
- Liste aller ausgestellten Codes: Display-Name · Code-String (Monospace) · Anzahl Reservierungen
|
||||
- „Neuen Code erstellen": Display-Name eingeben → Code wird automatisch generiert
|
||||
- „Link kopieren": kopiert fertigen Link mit Code in die Zwischenablage
|
||||
- „Löschen": löscht Code und **alle zugehörigen Reservierungen** — mit Bestätigungsdialog und Warnung
|
||||
|
||||
### 5.4 Reservierungsübersicht
|
||||
|
||||
- Nach Person gruppiert
|
||||
- Pro Person: Name als dunkle Kopfzeile, darunter eine Liste der reservierten Artikel
|
||||
- Pro Artikel: Thumbnail (48 px) · Artikelname · Kategorie-Badge
|
||||
- Personen ohne Reservierungen werden ausgegraut aber angezeigt
|
||||
|
||||
### 5.5 Übersicht (Dashboard)
|
||||
|
||||
- Kennzahlen-Zeile: Gesamtartikel · Reserviert · Frei
|
||||
- Tabelle: Aufschlüsselung nach Kategorie (Spalten: Kategorie · Gesamt · Reserviert · Frei)
|
||||
- Keine Charts oder Diagramme
|
||||
|
||||
---
|
||||
|
||||
## 6. Nicht-funktionale Anforderungen
|
||||
|
||||
| ID | Anforderung |
|
||||
|---|---|
|
||||
| NFR-SEC-001 | Codes sind kryptographisch zufällig generiert (nicht erratbar) |
|
||||
| NFR-SEC-002 | Admin-Passwörter werden gehasht gespeichert (bcrypt o. Ä.) |
|
||||
| NFR-SEC-003 | Familienmitglieder können ausschließlich ihre eigenen Reservierungen aufheben |
|
||||
| NFR-SEC-004 | Admin-Bereich ist von Familienmitglied-Bereich strikt getrennt (kein Code-Zugang zum Admin) |
|
||||
| NFR-PERF-001 | Galerie mit bis zu 500 Artikeln muss auf Mobilgerät flüssig scrollen |
|
||||
| NFR-PERF-002 | Fotos werden serverseitig auf eine sinnvolle Maximalgröße komprimiert (max. ~800 KB pro Bild) |
|
||||
| NFR-ACC-001 | WCAG 2.1 Level AA als Ziel (Kontrast, Fokus-Indikatoren, Alt-Texte auf Fotos) |
|
||||
| NFR-RESP-001 | Breakpoints: Telefon ≤ 767 px · Tablet 768–1023 px · Desktop ≥ 1024 px |
|
||||
| NFR-DATA-001 | Keine personenbezogenen Daten außer Display-Namen (kein DSGVO-kritischer Scope) |
|
||||
| NFR-DATA-002 | Reservierungen werden atomar gespeichert — wenn zwei Personen gleichzeitig denselben Artikel reservieren, gewinnt genau eine (keine doppelte Reservierung möglich) |
|
||||
|
||||
---
|
||||
|
||||
## 7. Priorisiertes Backlog (MoSCoW)
|
||||
|
||||
| ID | Story | MoSCoW | Aufwand |
|
||||
|---|---|---|---|
|
||||
| US-AUTH-001 | Als Familienmitglied öffne ich den Link und bin sofort eingeloggt | Must | XS |
|
||||
| US-AUTH-002 | Als Familienmitglied gebe ich meinen Code manuell ein | Must | XS |
|
||||
| US-AUTH-003 | Als Admin melde ich mich mit Benutzername und Passwort an | Must | S |
|
||||
| US-GAL-001 | Als Familienmitglied sehe ich alle Artikel als responsives Grid | Must | M |
|
||||
| US-GAL-002 | Als Familienmitglied filtere ich nach Kategorie | Must | S |
|
||||
| US-GAL-003 | Als Familienmitglied öffne ich ein Artikel-Modal mit allen Fotos | Must | M |
|
||||
| US-RES-001 | Als Familienmitglied reserviere ich einen freien Artikel | Must | S |
|
||||
| US-RES-002 | Als Familienmitglied hebe ich meine Reservierung auf | Must | XS |
|
||||
| US-RES-003 | Als Familienmitglied sehe ich wer einen reservierten Artikel hat | Must | XS |
|
||||
| US-ADM-001 | Als Admin füge ich einen Artikel hinzu (Kamera-first, mobile) | Must | M |
|
||||
| US-ADM-002 | Als Admin bearbeite ich einen bestehenden Artikel | Must | S |
|
||||
| US-ADM-003 | Als Admin lösche ich einen unreservierten Artikel | Must | XS |
|
||||
| US-ADM-004 | Als Admin erstelle ich einen Code mit Display-Name | Must | S |
|
||||
| US-ADM-005 | Als Admin kopiere ich den fertigen Link für einen Code | Must | XS |
|
||||
| US-ADM-006 | Als Admin lösche ich einen Code (mit Warnung) | Must | XS |
|
||||
| US-ADM-007 | Als Admin sehe ich die Reservierungsübersicht nach Person mit Fotos | Must | M |
|
||||
| US-ADM-008 | Als Admin sehe ich das Dashboard mit Kennzahlen | Should | S |
|
||||
|
||||
---
|
||||
|
||||
## 8. Offene Fragen / TBD-Register
|
||||
|
||||
| ID | Frage | Relevant für |
|
||||
|---|---|---|
|
||||
| OQ-001 | Wo wird die App gehostet? (beeinflusst Deployment-Komplexität) | Alle US |
|
||||
| OQ-002 | Wie viele Admin-Konten werden benötigt — genau 2, oder erweiterbar? | US-AUTH-003 |
|
||||
| OQ-003 | Soll der App-Name „Erbstücke der Familie" lauten oder anders? | Gate Screen, Browser-Tab |
|
||||
| OQ-004 | Maximale Anzahl Fotos pro Artikel? (Empfehlung: 10) | US-ADM-001 |
|
||||
|
||||
---
|
||||
|
||||
## 9. Glossar
|
||||
|
||||
| Begriff | Bedeutung |
|
||||
|---|---|
|
||||
| Code | Alphanumerischer 8-Zeichen-String, der ein Familienmitglied identifiziert |
|
||||
| Display-Name | Vom Admin vergebener Anzeigename, der dem Code zugeordnet ist |
|
||||
| Gate Screen | Eingabeseite für Familienmitglieder ohne aktive Session |
|
||||
| Reservierung | Zuordnung eines Artikels zu einem Code (first-come-first-served) |
|
||||
| Admin | Privilegierter Nutzer (Marcel oder Tante) mit Konto-basiertem Login |
|
||||
197
docs/superpowers/specs/2026-05-04-erbstuecke-system-design.md
Normal file
197
docs/superpowers/specs/2026-05-04-erbstuecke-system-design.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Erbstücke-Reservierung — System Design
|
||||
|
||||
**Datum:** 2026-05-04
|
||||
**Status:** Freigegeben zur Implementierung
|
||||
**Referenz:** 2026-05-04-erbstuecke-reservierung-design.md
|
||||
|
||||
---
|
||||
|
||||
## 1. Stack
|
||||
|
||||
| Schicht | Technologie | Begründung |
|
||||
|---|---|---|
|
||||
| Framework | SvelteKit (Node adapter) | SSR + server actions in einem Prozess, kein separates Backend nötig |
|
||||
| Datenbank | SQLite via `better-sqlite3` | Zero-Ops, kein DB-Container, WAL-Modus für atomare Reservierungen |
|
||||
| Foto-Verarbeitung | `sharp` | Resize + WebP-Konvertierung serverseitig bei Upload |
|
||||
| Passwort-Hashing | `bcryptjs` | Admin-Authentifizierung |
|
||||
| Deployment | Docker (single container) + Caddy (Host) | Root-Server, Subdomain, TLS via Caddy |
|
||||
|
||||
---
|
||||
|
||||
## 2. Deployment
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
volumes:
|
||||
- db:/app/db
|
||||
- uploads:/app/uploads
|
||||
environment:
|
||||
DATABASE_PATH: /app/db/erbstuecke.db
|
||||
UPLOAD_DIR: /app/uploads
|
||||
SESSION_SECRET: <random-string>
|
||||
ADMIN_MARCEL_PASSWORD_HASH: <bcrypt-hash>
|
||||
ADMIN_RENATE_PASSWORD_HASH: <bcrypt-hash>
|
||||
ADMIN_BERIT_PASSWORD_HASH: <bcrypt-hash>
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
db:
|
||||
uploads:
|
||||
```
|
||||
|
||||
Caddy auf dem Host übernimmt TLS-Terminierung und Reverse-Proxy zum Container. Die drei Admin-Konten (Marcel, Renate, Berit) sind fest in Umgebungsvariablen — kein Admin-UI zur Benutzerverwaltung.
|
||||
|
||||
---
|
||||
|
||||
## 3. Datenbankschema
|
||||
|
||||
Schema wird beim App-Start angelegt, falls die DB-Datei noch nicht existiert. Kein Migrations-Framework.
|
||||
|
||||
```sql
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS codes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE, -- 8-stellig, z. B. "AB3K7MN2"
|
||||
display_name TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS artikel (
|
||||
id INTEGER PRIMARY KEY,
|
||||
titel TEXT, -- optional
|
||||
kategorie TEXT NOT NULL,
|
||||
notiz TEXT, -- optional
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS artikel_fotos (
|
||||
id INTEGER PRIMARY KEY,
|
||||
artikel_id INTEGER NOT NULL REFERENCES artikel(id) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL, -- 0 = Galerie-Thumbnail
|
||||
filename TEXT NOT NULL, -- relativer Pfad im Uploads-Volume
|
||||
UNIQUE(artikel_id, position)
|
||||
);
|
||||
|
||||
-- UNIQUE auf artikel_id = atomare First-come-first-served-Garantie
|
||||
CREATE TABLE IF NOT EXISTS reservierungen (
|
||||
id INTEGER PRIMARY KEY,
|
||||
artikel_id INTEGER NOT NULL UNIQUE REFERENCES artikel(id),
|
||||
code_id INTEGER NOT NULL REFERENCES codes(id) ON DELETE CASCADE,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
```
|
||||
|
||||
Die `UNIQUE`-Constraint auf `reservierungen.artikel_id` ist die vollständige Nebenläufigkeitsstrategie: zwei gleichzeitige INSERTs, einer gewinnt, einer bekommt einen Constraint-Fehler — kein Application-Level-Locking erforderlich.
|
||||
|
||||
---
|
||||
|
||||
## 4. Authentifizierung & Sessions
|
||||
|
||||
### Familienmitglieder
|
||||
|
||||
- Code wird einmalig gegen die `codes`-Tabelle geprüft
|
||||
- Bei Erfolg: HTTP-only Same-Site-Cookie `family_code` gesetzt
|
||||
- `hooks.server.ts` liest Cookie bei jedem Request → `locals.familyCode` + `locals.displayName`
|
||||
- URL-Parameter `?code=AB3K7MN2` wird auf dem Gate-Screen automatisch validiert und der Cookie gesetzt
|
||||
- Kein Inhalt ohne gültigen Cookie sichtbar
|
||||
|
||||
### Admins
|
||||
|
||||
- Login-Formular: Benutzername (`Marcel` | `Renate` | `Berit`) + Passwort
|
||||
- Server prüft Benutzername → lädt zugehörigen Bcrypt-Hash aus Env → `bcrypt.compare`
|
||||
- Bei Erfolg: HTTP-only Cookie `admin_session` mit Admin-Name
|
||||
- `hooks.server.ts` → `locals.admin`
|
||||
- Unbekannter Benutzername gibt dieselbe Fehlermeldung wie falsches Passwort (kein User-Enumeration)
|
||||
|
||||
### Trennung
|
||||
|
||||
- Gültiger `family_code`-Cookie gewährt null Zugang zu `/admin/*`
|
||||
- Gültiger `admin_session`-Cookie gewährt null Zugang zur Familiengalerie
|
||||
- `+layout.server.ts` unter `/admin/` prüft `locals.admin` und leitet bei Fehlen zu `/admin/login` um
|
||||
|
||||
---
|
||||
|
||||
## 5. Foto-Handling
|
||||
|
||||
### Upload
|
||||
|
||||
1. Admin sendet Multipart-Formular
|
||||
2. SvelteKit Server-Action empfängt Dateien via `request.formData()`
|
||||
3. `sharp` verkleinert auf max. 1200 px lange Seite, konvertiert zu WebP (~800 KB Ziel)
|
||||
4. Datei wird geschrieben nach `/app/uploads/{artikel_id}/{uuid}.webp`
|
||||
5. `artikel_fotos`-Zeile mit relativem Pfad und Position wird eingefügt
|
||||
|
||||
### Auslieferung
|
||||
|
||||
Route `GET /uploads/[...path]/+server.ts` streamt die Datei vom Disk. URLs sind UUID-basiert und nicht erratbar — keine Auth-Prüfung erforderlich.
|
||||
|
||||
### Reihenfolge ändern
|
||||
|
||||
Admin verschiebt Thumbnails → Client sendet neues `position`-Array → Server aktualisiert alle `artikel_fotos`-Zeilen in einer Transaktion.
|
||||
|
||||
### Löschen
|
||||
|
||||
- Einzelnes Foto: `artikel_fotos`-Zeile löschen + Datei von Disk entfernen
|
||||
- Artikel löschen: CASCADE löscht `artikel_fotos`-Zeilen, Server entfernt Dateien im `uploads/{artikel_id}/`-Verzeichnis
|
||||
|
||||
---
|
||||
|
||||
## 6. Routenstruktur
|
||||
|
||||
```
|
||||
src/
|
||||
hooks.server.ts # Cookie → locals.familyCode / locals.admin
|
||||
routes/
|
||||
+page.svelte # Gate Screen
|
||||
+page.server.ts # ?code= validieren, Cookie setzen
|
||||
uploads/[...path]/
|
||||
+server.ts # Foto von Disk streamen
|
||||
galerie/
|
||||
+page.svelte # Responsives Grid + Kategorie-Filter
|
||||
+page.server.ts # Artikel + Reservierungsstatus laden
|
||||
artikel/[id]/
|
||||
+page.server.ts # Reservieren / Aufheben (Form Actions)
|
||||
admin/
|
||||
+layout.server.ts # Auth-Guard
|
||||
+layout.svelte # Sidebar-Navigation
|
||||
login/
|
||||
+page.svelte
|
||||
+page.server.ts # Bcrypt-Verify, Session-Cookie setzen
|
||||
inventar/
|
||||
+page.svelte
|
||||
+page.server.ts # CRUD Artikel + Foto-Upload
|
||||
codes/
|
||||
+page.svelte
|
||||
+page.server.ts # Codes verwalten, Link kopieren
|
||||
reservierungen/
|
||||
+page.svelte
|
||||
+page.server.ts # Übersicht nach Person gruppiert
|
||||
uebersicht/
|
||||
+page.svelte
|
||||
+page.server.ts # Dashboard-Kennzahlen
|
||||
lib/
|
||||
db.ts # better-sqlite3-Singleton, WAL on init
|
||||
photos.ts # sharp resize + write/delete
|
||||
auth.ts # bcrypt verify, Cookie-Helpers
|
||||
```
|
||||
|
||||
Alle Daten werden in `+page.server.ts`-Dateien geladen — kein separater API-Layer, keine Client-seitigen Fetch-Calls für Datenzugriff. Mutationen laufen über SvelteKit Form Actions (progressive enhancement, funktioniert ohne JS).
|
||||
|
||||
---
|
||||
|
||||
## 7. Nicht-funktionale Entscheidungen
|
||||
|
||||
| Anforderung | Umsetzung |
|
||||
|---|---|
|
||||
| NFR-SEC-001 Codes kryptographisch zufällig | `crypto.randomBytes` → zufällige Auswahl aus A-Z + 0-9, 8 Zeichen |
|
||||
| NFR-SEC-002 Passwörter gehasht | bcrypt, work factor 12 |
|
||||
| NFR-SEC-003 Nur eigene Reservierungen aufheben | Server prüft `reservierungen.code_id = locals.familyCode.id` |
|
||||
| NFR-SEC-004 Admin-Trennung | Separate Cookie-Namen, separate Auth-Guards in hooks |
|
||||
| NFR-PERF-001 500 Artikel flüssig scrollen | SSR-gerenderte HTML-Liste, kein Client-Side-Fetch beim Laden |
|
||||
| NFR-PERF-002 Fotos max. ~800 KB | sharp WebP-Konvertierung bei Upload |
|
||||
| NFR-DATA-002 Atomare Reservierungen | UNIQUE-Constraint auf `reservierungen.artikel_id` |
|
||||
@@ -0,0 +1,602 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<title>Erbstücke Wannsee — Design System</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;0,700;1,400&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:'Inter',system-ui,sans-serif;background:#EAE5DC;color:#1C2820;line-height:1.5;font-size:13px;padding:48px 32px 120px}
|
||||
.doc{max-width:1200px;margin:0 auto}
|
||||
|
||||
/* ── Masthead ── */
|
||||
.mh{padding-bottom:24px;border-bottom:3px solid #5B7A66;margin-bottom:56px}
|
||||
.mh .kicker{font-size:9px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:#C4874A}
|
||||
.mh h1{font-family:'Lora',Georgia,serif;font-size:30px;font-weight:700;color:#1C2820;letter-spacing:-.4px;margin-top:7px}
|
||||
.mh p{font-size:13.5px;color:#6B6050;max-width:820px;line-height:1.8;margin-top:12px}
|
||||
.byline{font-size:9px;color:#999;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;margin-top:16px}
|
||||
.tag-row{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
|
||||
.tag{background:#5B7A66;color:#fff;padding:3px 9px;border-radius:2px;font-size:8.5px;font-weight:800;letter-spacing:.8px;text-transform:uppercase}
|
||||
.tag.amber{background:#C4874A}
|
||||
.tag.outline{background:transparent;color:#5B7A66;border:1px solid #5B7A66}
|
||||
.tag.gray{background:#6B6050;color:#fff}
|
||||
|
||||
/* ── Section rhythm ── */
|
||||
.section{margin-bottom:80px}
|
||||
.section+.section{border-top:1px dashed #C8C0B4;padding-top:72px}
|
||||
.section-kicker{font-size:9px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:#C4874A;display:block;margin-bottom:6px}
|
||||
.section h2{font-family:'Lora',Georgia,serif;font-size:24px;font-weight:700;color:#1C2820;margin-bottom:10px}
|
||||
.section-desc{font-size:13px;color:#6B6050;line-height:1.75;max-width:780px;margin-bottom:28px}
|
||||
|
||||
/* ── Color swatches ── */
|
||||
.swatch-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:10px;margin-bottom:24px}
|
||||
.swatch{border-radius:8px;overflow:hidden;border:1px solid rgba(0,0,0,.08);background:#fff}
|
||||
.swatch-color{height:72px}
|
||||
.swatch-info{padding:9px 11px}
|
||||
.swatch-name{font-size:10px;font-weight:700;color:#1C2820;margin-bottom:1px}
|
||||
.swatch-token{font-family:monospace;font-size:10px;color:#5B7A66;margin-bottom:2px}
|
||||
.swatch-hex{font-family:monospace;font-size:10px;color:#888}
|
||||
.swatch-rule{font-size:9px;color:#999;margin-top:3px;line-height:1.4}
|
||||
|
||||
/* ── Token / impl tables ── */
|
||||
.token-table,.impl-table{width:100%;border-collapse:collapse;font-size:12px;margin-bottom:24px;background:#fff;border-radius:6px;overflow:hidden;border:1px solid #E0D8CC}
|
||||
.token-table th,.impl-table th{text-align:left;padding:9px 12px;font-size:9px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:.5px;border-bottom:2px solid #E0D8CC;background:#F7F4EF}
|
||||
.token-table td,.impl-table td{padding:8px 12px;border-bottom:1px solid #EDE8E0;color:#444;vertical-align:top;line-height:1.55}
|
||||
.token-table tr:last-child td,.impl-table tr:last-child td{border-bottom:none}
|
||||
.token-table td:first-child{font-family:monospace;font-size:11px;color:#5B7A66;white-space:nowrap}
|
||||
.impl-table td:first-child{font-weight:700;color:#1C2820;width:20%}
|
||||
.impl-table td:nth-child(3){color:#777;font-size:11px;width:10%}
|
||||
.impl-table td:last-child{color:#888;font-size:11px;font-style:italic}
|
||||
.dot{display:inline-block;width:14px;height:14px;border-radius:3px;border:1px solid rgba(0,0,0,.1);vertical-align:middle;margin-right:6px}
|
||||
td code,th code{font-family:monospace;font-size:11px;background:#F2EDE4;padding:1px 5px;border-radius:2px;color:#5B7A66}
|
||||
|
||||
/* ── Contrast grid ── */
|
||||
.contrast-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;margin-bottom:24px}
|
||||
.contrast-card{background:#fff;border:1px solid #E0D8CC;border-radius:6px;padding:14px 16px}
|
||||
.contrast-row{display:flex;align-items:center;gap:10px;padding:7px 0;border-bottom:1px solid #F0EDE6;font-size:11px}
|
||||
.contrast-row:last-child{border-bottom:none}
|
||||
.contrast-chip{width:90px;height:28px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;flex-shrink:0}
|
||||
.contrast-ratio{font-family:monospace;font-weight:700;color:#1C2820;width:48px;flex-shrink:0}
|
||||
.pass-aa{background:#DCFCE7;color:#166534;font-size:9px;font-weight:800;padding:2px 6px;border-radius:3px}
|
||||
.pass-aaa{background:#DBEAFE;color:#1E40AF;font-size:9px;font-weight:800;padding:2px 6px;border-radius:3px}
|
||||
.contrast-desc{color:#666}
|
||||
|
||||
/* ── Type specimens ── */
|
||||
.type-specimen{background:#fff;border:1px solid #E0D8CC;border-radius:8px;padding:22px 26px;margin-bottom:12px}
|
||||
.type-meta{font-size:9px;font-weight:800;color:#AAA;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:12px}
|
||||
.type-label{font-size:10px;color:#999;margin-top:8px;line-height:1.5}
|
||||
|
||||
/* ── Component preview grid ── */
|
||||
.comp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;margin-bottom:16px}
|
||||
.comp-box{background:#fff;border:1px solid #E0D8CC;border-radius:8px;padding:18px 20px}
|
||||
.comp-label{font-size:9px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:1px;margin-bottom:14px}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.btn{display:inline-flex;align-items:center;justify-content:center;font-family:'Inter',sans-serif;font-weight:700;border-radius:6px;cursor:pointer;border:none;min-height:44px;padding:0 20px;font-size:13px;text-decoration:none}
|
||||
.btn-primary{background:#5B7A66;color:#fff}
|
||||
.btn-accent{background:#C4874A;color:#fff}
|
||||
.btn-outline{background:transparent;border:1.5px solid #5B7A66;color:#5B7A66;padding:0 19px}
|
||||
.btn-danger{background:#FBF0F0;border:1.5px solid #9B6060;color:#9B6060;padding:0 19px}
|
||||
.btn-ghost{background:transparent;color:#6B6050;padding:0 12px}
|
||||
.btn-disabled{background:#F2EDE4;border:1.5px solid #E0D8CC;color:#AAA;cursor:not-allowed}
|
||||
.btn-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px}
|
||||
|
||||
/* ── Chips ── */
|
||||
.chip{display:inline-flex;align-items:center;font-size:10px;font-weight:700;padding:3px 10px;border-radius:12px;text-transform:uppercase;letter-spacing:.3px}
|
||||
.chip-sage{background:#DFF0E6;color:#2E6645;border:1px solid #A8D5B8}
|
||||
.chip-amber{background:#FBF0E0;color:#8A5A1A;border:1px solid #E8C48A}
|
||||
.chip-gray{background:#F3F0EC;color:#5A5040;border:1px solid #DDD8CC}
|
||||
.chip-row{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px}
|
||||
|
||||
/* ── Status indicators ── */
|
||||
.status-free{font-size:12px;font-weight:700;color:#4A7C5C}
|
||||
.status-mine{display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:700;background:#E8F5EC;color:#2E6645;padding:3px 10px;border-radius:10px;border:1px solid #A8D5B8}
|
||||
.status-taken{font-size:12px;font-weight:700;color:#9B6060}
|
||||
|
||||
/* ── Gallery cards ── */
|
||||
.gcard{background:#fff;border:1px solid #E0D8CC;border-radius:8px;overflow:hidden;display:flex;margin-bottom:8px}
|
||||
.gcard-img{width:72px;height:72px;background:#C4B8A8;flex-shrink:0}
|
||||
.gcard-body{padding:10px 12px;display:flex;flex-direction:column;justify-content:center;gap:3px}
|
||||
.gcard-cat{font-size:9px;color:#6B6050;font-weight:800;text-transform:uppercase;letter-spacing:.6px}
|
||||
.gcard-title{font-family:'Lora',Georgia,serif;font-size:12px;color:#1C2820;font-weight:600;line-height:1.3}
|
||||
|
||||
/* ── Filter tabs ── */
|
||||
.pill{background:transparent;color:#6B6050;border:1.5px solid #E0D8CC;font-family:'Inter',sans-serif;font-size:10px;font-weight:700;padding:5px 11px;border-radius:20px;min-height:32px;white-space:nowrap;cursor:pointer}
|
||||
.pill.on{background:#5B7A66;color:#fff;border-color:#5B7A66}
|
||||
|
||||
/* ── Nav bar ── */
|
||||
.app-nav{height:44px;background:#5B7A66;display:flex;align-items:center;padding:0 14px;border-radius:6px;margin-bottom:8px}
|
||||
.app-logo{font-family:'Lora',Georgia,serif;font-size:13px;font-weight:700;color:#fff}
|
||||
.app-user{margin-left:auto;font-size:10px;color:rgba(255,255,255,.75);font-weight:600}
|
||||
|
||||
/* ── Focus ring ── */
|
||||
.focus-ex{outline:3px solid rgba(91,122,102,.45);outline-offset:2px}
|
||||
|
||||
/* ── Do / Don't ── */
|
||||
.do-dont{display:grid;grid-template-columns:1fr 1fr;gap:0;border:1.5px solid #E0D8CC;border-radius:8px;overflow:hidden;margin-bottom:16px}
|
||||
.dd-head{padding:9px 16px;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;border-bottom:1.5px solid #E0D8CC}
|
||||
.do-head{background:#F0FDF4;color:#166534;border-right:1px solid #E0D8CC}
|
||||
.dont-head{background:#FFF5F5;color:#991B1B}
|
||||
.dd-body{padding:16px 18px;background:#fff;font-size:12px;color:#444;line-height:1.6}
|
||||
.dd-body:first-of-type{border-right:1px solid #F0EDE6;background:#FAFFFE}
|
||||
.dd-body:last-of-type{background:#FFFAFA}
|
||||
.do-item::before{content:'✓ ';color:#16A34A;font-weight:700}
|
||||
.dont-item::before{content:'✕ ';color:#DC2626;font-weight:700}
|
||||
.rule-item{display:block;margin-bottom:6px}
|
||||
.rule-item code{font-size:10.5px;background:#F2EDE4;padding:1px 4px;border-radius:2px;color:#5B7A66}
|
||||
|
||||
/* ── Notes box ── */
|
||||
.notes{background:#F9F7F3;border-left:3px solid #C4874A;padding:14px 20px;border-radius:0 4px 4px 0;margin-top:24px}
|
||||
.notes .nh{font-size:9px;font-weight:800;letter-spacing:1px;text-transform:uppercase;color:#C4874A;margin-bottom:8px}
|
||||
.notes ul{list-style:none;display:flex;flex-direction:column;gap:6px}
|
||||
.notes li{font-size:12px;color:#333;padding-left:16px;position:relative;line-height:1.7}
|
||||
.notes li::before{content:"•";position:absolute;left:0;color:#C4874A;font-weight:800}
|
||||
.notes li code{font-family:monospace;font-size:11px;background:#F2EDE4;padding:1px 4px;border-radius:2px;color:#5B7A66}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="doc">
|
||||
|
||||
<!-- ════ MASTHEAD ════ -->
|
||||
<div class="mh">
|
||||
<div class="kicker">UI/UX Spec · Implementation-ready</div>
|
||||
<h1>Erbstücke Wannsee — Design System</h1>
|
||||
<p>
|
||||
Vollständige Design-Grundlage für die Erbstücke-Wannsee-App: Farb-Token, Typografie, Kernkomponenten.
|
||||
Warm-pastell Palette (Salbei & Amber) für einen würdevollen, familiären Kontext.
|
||||
Kein Dark Mode. Nur Deutsch. WCAG 2.1 AA als Mindeststandard.
|
||||
</p>
|
||||
<div class="byline">Leonie Voss · 2026-05-05 · Final · Gilt für alle Views</div>
|
||||
<div class="tag-row">
|
||||
<span class="tag">design system</span>
|
||||
<span class="tag amber">salbei & amber</span>
|
||||
<span class="tag outline">WCAG AA</span>
|
||||
<span class="tag outline">lora + inter</span>
|
||||
<span class="tag gray">kein dark mode</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════ 01 — FARB-TOKEN ════ -->
|
||||
<div class="section">
|
||||
<span class="section-kicker">01 — Farben</span>
|
||||
<h2>Farb-Token</h2>
|
||||
<p class="section-desc">
|
||||
Alle Farben werden als CSS Custom Properties definiert und via Tailwind-Erweiterung (<code>theme.extend.colors</code>) als semantische Klassen verfügbar gemacht.
|
||||
Kein Inline-Hex, keine Tailwind-Standard-Farben direkt im Markup.
|
||||
</p>
|
||||
|
||||
<div class="swatch-grid">
|
||||
<div class="swatch">
|
||||
<div class="swatch-color" style="background:#F2EDE4;border-bottom:1px solid #E0D8CC"></div>
|
||||
<div class="swatch-info">
|
||||
<div class="swatch-name">Canvas</div>
|
||||
<div class="swatch-token">--color-canvas</div>
|
||||
<div class="swatch-hex">#F2EDE4</div>
|
||||
<div class="swatch-rule">Seitenhintergrund (body)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-color" style="background:#FFFFFF;border-bottom:1px solid #E0D8CC"></div>
|
||||
<div class="swatch-info">
|
||||
<div class="swatch-name">Surface</div>
|
||||
<div class="swatch-token">--color-surface</div>
|
||||
<div class="swatch-hex">#FFFFFF</div>
|
||||
<div class="swatch-rule">Karten, Modals, Inputs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-color" style="background:#E0D8CC"></div>
|
||||
<div class="swatch-info">
|
||||
<div class="swatch-name">Line</div>
|
||||
<div class="swatch-token">--color-line</div>
|
||||
<div class="swatch-hex">#E0D8CC</div>
|
||||
<div class="swatch-rule">Rahmen, Trennlinien</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-color" style="background:#5B7A66"></div>
|
||||
<div class="swatch-info">
|
||||
<div class="swatch-name">Primary (Salbei)</div>
|
||||
<div class="swatch-token">--color-primary</div>
|
||||
<div class="swatch-hex">#5B7A66</div>
|
||||
<div class="swatch-rule">Nav, Buttons, aktive Tabs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-color" style="background:#4A6855"></div>
|
||||
<div class="swatch-info">
|
||||
<div class="swatch-name">Primary Dark</div>
|
||||
<div class="swatch-token">--color-primary-dark</div>
|
||||
<div class="swatch-hex">#4A6855</div>
|
||||
<div class="swatch-rule">Hover auf Primary-Buttons</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-color" style="background:#C4874A"></div>
|
||||
<div class="swatch-info">
|
||||
<div class="swatch-name">Accent (Amber)</div>
|
||||
<div class="swatch-token">--color-accent</div>
|
||||
<div class="swatch-hex">#C4874A</div>
|
||||
<div class="swatch-rule">CTAs, Highlights, Admin-Sidebar aktiv</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-color" style="background:#1C2820"></div>
|
||||
<div class="swatch-info">
|
||||
<div class="swatch-name">Ink</div>
|
||||
<div class="swatch-token">--color-ink</div>
|
||||
<div class="swatch-hex">#1C2820</div>
|
||||
<div class="swatch-rule">Haupttext, Überschriften</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-color" style="background:#6B6050"></div>
|
||||
<div class="swatch-info">
|
||||
<div class="swatch-name">Ink Muted</div>
|
||||
<div class="swatch-token">--color-ink-muted</div>
|
||||
<div class="swatch-hex">#6B6050</div>
|
||||
<div class="swatch-rule">Labels, Metadaten, Hilfstexte</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-color" style="background:#4A7C5C"></div>
|
||||
<div class="swatch-info">
|
||||
<div class="swatch-name">Status Free</div>
|
||||
<div class="swatch-token">--color-status-free</div>
|
||||
<div class="swatch-hex">#4A7C5C</div>
|
||||
<div class="swatch-rule">„✓ Frei"-Text und Badge-Hintergrund</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-color" style="background:#9B6060"></div>
|
||||
<div class="swatch-info">
|
||||
<div class="swatch-name">Status Taken</div>
|
||||
<div class="swatch-token">--color-status-taken</div>
|
||||
<div class="swatch-hex">#9B6060</div>
|
||||
<div class="swatch-rule">„● [Name]"-Text, Fehler, Danger-Aktionen</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="swatch-color" style="background:#2A3B30"></div>
|
||||
<div class="swatch-info">
|
||||
<div class="swatch-name">Admin BG</div>
|
||||
<div class="swatch-token">--color-admin-bg</div>
|
||||
<div class="swatch-hex">#2A3B30</div>
|
||||
<div class="swatch-rule">Admin-Sidebar, Admin-Login-Hintergrund</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="token-table">
|
||||
<thead><tr><th>CSS Token</th><th>Wert</th><th>Tailwind-Klasse</th><th>Primärer Einsatz</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>--color-canvas</td><td><span class="dot" style="background:#F2EDE4"></span>#F2EDE4</td><td><code>bg-canvas</code></td><td>body background, Galerie-Hintergrund</td></tr>
|
||||
<tr><td>--color-surface</td><td><span class="dot" style="background:#fff;border:1px solid #ddd"></span>#FFFFFF</td><td><code>bg-surface</code></td><td>Karten, Modals, Inputs, Admin-Content</td></tr>
|
||||
<tr><td>--color-line</td><td><span class="dot" style="background:#E0D8CC"></span>#E0D8CC</td><td><code>border-line</code></td><td>Alle Rahmen und Trennlinien</td></tr>
|
||||
<tr><td>--color-primary</td><td><span class="dot" style="background:#5B7A66"></span>#5B7A66</td><td><code>bg-primary · text-primary</code></td><td>Navigationsleiste, Primär-Buttons, aktive Filter-Pills</td></tr>
|
||||
<tr><td>--color-primary-dark</td><td><span class="dot" style="background:#4A6855"></span>#4A6855</td><td><code>hover:bg-primary-dark</code></td><td>Hover-Zustand auf Primary-Elementen</td></tr>
|
||||
<tr><td>--color-accent</td><td><span class="dot" style="background:#C4874A"></span>#C4874A</td><td><code>bg-accent · text-accent</code></td><td>Admin-Sidebar aktiver Bereich, Thumbnail-Badge, sparsame Highlights</td></tr>
|
||||
<tr><td>--color-ink</td><td><span class="dot" style="background:#1C2820"></span>#1C2820</td><td><code>text-ink</code></td><td>Fließtext, Seiten-Titel, Artikel-Titel im Modal</td></tr>
|
||||
<tr><td>--color-ink-muted</td><td><span class="dot" style="background:#6B6050"></span>#6B6050</td><td><code>text-ink-muted</code></td><td>Kategorie-Labels, Platzhaltertext, Hinweise</td></tr>
|
||||
<tr><td>--color-status-free</td><td><span class="dot" style="background:#4A7C5C"></span>#4A7C5C</td><td><code>text-status-free</code></td><td>Statustext „✓ Frei", Badge-Text „✓ Meine Reservierung"</td></tr>
|
||||
<tr><td>--color-status-taken</td><td><span class="dot" style="background:#9B6060"></span>#9B6060</td><td><code>text-status-taken</code></td><td>Statustext „● [Name]", Danger-Buttons, Fehlertexte</td></tr>
|
||||
<tr><td>--color-admin-bg</td><td><span class="dot" style="background:#2A3B30"></span>#2A3B30</td><td><code>bg-admin</code></td><td>Admin-Sidebar, Admin-Login-Seite-Hintergrund</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3 style="font-family:'Lora',Georgia,serif;font-size:16px;font-weight:700;color:#1C2820;margin:28px 0 16px">Kontrast-Prüfung (WCAG 2.1)</h3>
|
||||
<div class="contrast-grid">
|
||||
<div class="contrast-card">
|
||||
<div style="font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:#888;margin-bottom:10px">Text auf Hintergrund</div>
|
||||
<div class="contrast-row">
|
||||
<div class="contrast-chip" style="background:#F2EDE4;color:#1C2820;font-size:11px">Aa</div>
|
||||
<div class="contrast-ratio">9.2:1</div>
|
||||
<span class="pass-aaa">AAA</span>
|
||||
<div class="contrast-desc">Ink auf Canvas — Haupttext</div>
|
||||
</div>
|
||||
<div class="contrast-row">
|
||||
<div class="contrast-chip" style="background:#fff;color:#1C2820;font-size:11px">Aa</div>
|
||||
<div class="contrast-ratio">12.6:1</div>
|
||||
<span class="pass-aaa">AAA</span>
|
||||
<div class="contrast-desc">Ink auf Surface — Karten</div>
|
||||
</div>
|
||||
<div class="contrast-row">
|
||||
<div class="contrast-chip" style="background:#F2EDE4;color:#6B6050;font-size:11px">Aa</div>
|
||||
<div class="contrast-ratio">5.1:1</div>
|
||||
<span class="pass-aa">AA</span>
|
||||
<div class="contrast-desc">Ink Muted auf Canvas — Labels</div>
|
||||
</div>
|
||||
<div class="contrast-row">
|
||||
<div class="contrast-chip" style="background:#5B7A66;color:#fff;font-size:11px">Aa</div>
|
||||
<div class="contrast-ratio">4.6:1</div>
|
||||
<span class="pass-aa">AA</span>
|
||||
<div class="contrast-desc">Weiß auf Primary — Nav, Buttons</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contrast-card">
|
||||
<div style="font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:#888;margin-bottom:10px">Status-Farben</div>
|
||||
<div class="contrast-row">
|
||||
<div class="contrast-chip" style="background:#fff;color:#4A7C5C;font-size:11px">✓ Frei</div>
|
||||
<div class="contrast-ratio">5.2:1</div>
|
||||
<span class="pass-aa">AA</span>
|
||||
<div class="contrast-desc">Status Free auf Surface</div>
|
||||
</div>
|
||||
<div class="contrast-row">
|
||||
<div class="contrast-chip" style="background:#F2EDE4;color:#4A7C5C;font-size:11px">✓ Frei</div>
|
||||
<div class="contrast-ratio">4.6:1</div>
|
||||
<span class="pass-aa">AA</span>
|
||||
<div class="contrast-desc">Status Free auf Canvas</div>
|
||||
</div>
|
||||
<div class="contrast-row">
|
||||
<div class="contrast-chip" style="background:#fff;color:#9B6060;font-size:11px">● Name</div>
|
||||
<div class="contrast-ratio">4.5:1</div>
|
||||
<span class="pass-aa">AA</span>
|
||||
<div class="contrast-desc">Status Taken auf Surface</div>
|
||||
</div>
|
||||
<div class="contrast-row">
|
||||
<div class="contrast-chip" style="background:#2A3B30;color:#fff;font-size:11px">Aa</div>
|
||||
<div class="contrast-ratio">11.8:1</div>
|
||||
<span class="pass-aaa">AAA</span>
|
||||
<div class="contrast-desc">Weiß auf Admin BG — Sidebar</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="do-dont">
|
||||
<div>
|
||||
<div class="dd-head do-head">✓ Tun</div>
|
||||
<div class="dd-body">
|
||||
<span class="rule-item do-item">Alle Farben über <code>bg-canvas</code>, <code>text-primary</code> etc. einsetzen</span>
|
||||
<span class="rule-item do-item">Status immer mit Icon + Text kommunizieren, nie Farbe allein</span>
|
||||
<span class="rule-item do-item">Accent (Amber) sparsam einsetzen — maximal 1–2 Elemente pro Screen</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="dd-head dont-head">✕ Nicht tun</div>
|
||||
<div class="dd-body">
|
||||
<span class="rule-item dont-item">Inline-Hex wie <code>style="color:#5B7A66"</code> im Markup</span>
|
||||
<span class="rule-item dont-item">Tailwind-Standard-Farben wie <code>text-green-600</code> für Status</span>
|
||||
<span class="rule-item dont-item">Decorative-Farben (#C4874A, #A8D5B8) als Body-Text auf weißem Hintergrund</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════ 02 — TYPOGRAFIE ════ -->
|
||||
<div class="section">
|
||||
<span class="section-kicker">02 — Typografie</span>
|
||||
<h2>Typografie</h2>
|
||||
<p class="section-desc">
|
||||
Zwei Schriftarten. <strong>Lora</strong> (Google Fonts, Serif) für alles Emotionale und Identitätsstiftende — App-Name, Artikel-Titel, Seiten-Überschriften.
|
||||
<strong>Inter</strong> (Google Fonts, Sans) für alle UI-Elemente — Labels, Buttons, Statustext, Fließtext.
|
||||
Minimale Font-Größe: 12 px. Body-Text: 16 px. Beide Schriften via <code><link></code> im <code><head></code> laden.
|
||||
</p>
|
||||
|
||||
<div class="type-specimen">
|
||||
<div class="type-meta">Lora 700 — App-Name, Seiten-Titel</div>
|
||||
<div style="font-family:'Lora',Georgia,serif;font-size:28px;font-weight:700;color:#1C2820;line-height:1.2">Erbstücke Wannsee</div>
|
||||
<div class="type-label">font-serif text-[28px] font-bold — nur Nav und Gate Screen</div>
|
||||
</div>
|
||||
|
||||
<div class="type-specimen">
|
||||
<div class="type-meta">Lora 700 — Artikel-Titel im Modal</div>
|
||||
<div style="font-family:'Lora',Georgia,serif;font-size:20px;font-weight:700;color:#1C2820;line-height:1.3">Schreibtisch aus Eichenholz</div>
|
||||
<div class="type-label">font-serif text-xl font-bold leading-snug</div>
|
||||
</div>
|
||||
|
||||
<div class="type-specimen">
|
||||
<div class="type-meta">Lora 600 — Karten-Titel (Liste)</div>
|
||||
<div style="font-family:'Lora',Georgia,serif;font-size:13px;font-weight:600;color:#1C2820;line-height:1.3">Goldbrosche mit Granat</div>
|
||||
<div class="type-label">font-serif text-[13px] font-semibold leading-snug</div>
|
||||
</div>
|
||||
|
||||
<div class="type-specimen">
|
||||
<div class="type-meta">Lora 400 italic — Notizen, Hinweise</div>
|
||||
<div style="font-family:'Lora',Georgia,serif;font-size:14px;font-style:italic;color:#6B6050;line-height:1.6">Kleine Schramme oben links, sonst sehr guter Zustand.</div>
|
||||
<div class="type-label">font-serif text-sm italic text-ink-muted leading-relaxed</div>
|
||||
</div>
|
||||
|
||||
<div class="type-specimen">
|
||||
<div class="type-meta">Inter 400 — Body / Fließtext</div>
|
||||
<div style="font-family:'Inter',sans-serif;font-size:16px;font-weight:400;color:#1C2820;line-height:1.6">Hier kannst du Gegenstände aus dem Nachlass reservieren. Gib deinen persönlichen Code ein:</div>
|
||||
<div class="type-label">font-sans text-base leading-relaxed — minimum 16px für Fließtext</div>
|
||||
</div>
|
||||
|
||||
<div class="type-specimen">
|
||||
<div class="type-meta">Inter 800 uppercase — Kategorie-Labels, Section-Kicker</div>
|
||||
<div style="display:flex;gap:20px;align-items:center;flex-wrap:wrap">
|
||||
<span style="font-family:'Inter',sans-serif;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.7px;color:#6B6050">Möbel</span>
|
||||
<span style="font-family:'Inter',sans-serif;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.7px;color:#6B6050">Schmuck</span>
|
||||
<span style="font-family:'Inter',sans-serif;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.7px;color:#6B6050">Bücher</span>
|
||||
</div>
|
||||
<div class="type-label">font-sans text-[10px] font-extrabold uppercase tracking-wide text-ink-muted</div>
|
||||
</div>
|
||||
|
||||
<div class="type-specimen">
|
||||
<div class="type-meta">Monospace System — Code-Strings</div>
|
||||
<div style="font-family:monospace;font-size:16px;font-weight:600;letter-spacing:4px;color:#1C2820">AB3K7MN2</div>
|
||||
<div class="type-label">font-mono text-base font-semibold tracking-[4px] uppercase — Gate Screen und Code-Verwaltung</div>
|
||||
</div>
|
||||
|
||||
<table class="token-table" style="margin-top:24px">
|
||||
<thead><tr><th>Rolle</th><th>Font / Gewicht</th><th>Größe</th><th>Tailwind-Klassen</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>App-Name (Nav)</td><td>Lora 700</td><td>14px</td><td><code>font-serif text-sm font-bold text-white</code></td></tr>
|
||||
<tr><td>Gate Screen Titel</td><td>Lora 700</td><td>20px</td><td><code>font-serif text-xl font-bold text-ink</code></td></tr>
|
||||
<tr><td>Admin Seiten-Titel</td><td>Lora 700</td><td>16px</td><td><code>font-serif text-base font-bold text-ink</code></td></tr>
|
||||
<tr><td>Artikel-Titel (Modal)</td><td>Lora 700</td><td>20px</td><td><code>font-serif text-xl font-bold text-ink leading-snug</code></td></tr>
|
||||
<tr><td>Karten-Titel (Liste)</td><td>Lora 600</td><td>13px</td><td><code>font-serif text-[13px] font-semibold text-ink leading-snug</code></td></tr>
|
||||
<tr><td>Notiz / kursiv</td><td>Lora 400 italic</td><td>14px</td><td><code>font-serif text-sm italic text-ink-muted leading-relaxed</code></td></tr>
|
||||
<tr><td>Body / Fließtext</td><td>Inter 400</td><td>16px</td><td><code>font-sans text-base leading-relaxed</code></td></tr>
|
||||
<tr><td>Kategorie-Label</td><td>Inter 800 caps</td><td>10px</td><td><code>font-sans text-[10px] font-extrabold uppercase tracking-wide text-ink-muted</code></td></tr>
|
||||
<tr><td>Status-Text</td><td>Inter 700</td><td>11–12px</td><td><code>font-sans text-[11px] font-bold</code></td></tr>
|
||||
<tr><td>Button-Text</td><td>Inter 700</td><td>13–14px</td><td><code>font-sans text-sm font-bold</code></td></tr>
|
||||
<tr><td>Admin-Label / Meta</td><td>Inter 800 caps</td><td>9px</td><td><code>font-sans text-[9px] font-extrabold uppercase tracking-widest text-ink-muted</code></td></tr>
|
||||
<tr><td>Code-String</td><td>Mono System 600</td><td>16px</td><td><code>font-mono text-base font-semibold tracking-[4px] uppercase</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ════ 03 — KERNKOMPONENTEN ════ -->
|
||||
<div class="section">
|
||||
<span class="section-kicker">03 — Komponenten</span>
|
||||
<h2>Kernkomponenten</h2>
|
||||
<p class="section-desc">Alle wiederverwendbaren UI-Bausteine. Jeder interaktive Bereich hat mindestens 44 px Höhe (WCAG 2.2 Touch Target). Fokus-Ringe immer sichtbar via <code>focus-visible</code>.</p>
|
||||
|
||||
<div class="comp-grid">
|
||||
|
||||
<div class="comp-box">
|
||||
<div class="comp-label">Buttons (min. 44 px Höhe)</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary">Reservieren</button>
|
||||
<button class="btn btn-accent">Speichern</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-outline">Aufheben</button>
|
||||
<button class="btn btn-danger">Code löschen</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-ghost">Abbrechen</button>
|
||||
<button class="btn btn-disabled" disabled>Nicht verfügbar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comp-box">
|
||||
<div class="comp-label">Status-Indikatoren</div>
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
<div>
|
||||
<div class="status-free">✓ Frei</div>
|
||||
<div style="font-size:10px;color:#999;margin-top:2px">Galerie-Karte + Modal — text-status-free</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="status-mine">✓ Meine Reservierung</div>
|
||||
<div style="font-size:10px;color:#999;margin-top:2px">Badge — bg-[#E8F5EC] border-[#A8D5B8]</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="status-taken">● Renate</div>
|
||||
<div style="font-size:10px;color:#999;margin-top:2px">Galerie-Karte + Modal — text-status-taken</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comp-box">
|
||||
<div class="comp-label">Kategorie-Chips</div>
|
||||
<div class="chip-row">
|
||||
<span class="chip chip-sage">Aktiv</span>
|
||||
<span class="chip chip-amber">Amber</span>
|
||||
<span class="chip chip-gray">Inaktiv</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#999;margin-top:4px;line-height:1.5">Sage = ausgewählte Kategorie. Grau = nicht ausgewählt.</div>
|
||||
</div>
|
||||
|
||||
<div class="comp-box">
|
||||
<div class="comp-label">Filter-Tab-Leiste (Galerie)</div>
|
||||
<div style="display:flex;gap:5px;overflow-x:auto;padding-bottom:4px">
|
||||
<button class="pill on">Alle</button>
|
||||
<button class="pill">Möbel</button>
|
||||
<button class="pill">Bücher</button>
|
||||
<button class="pill">Schmuck</button>
|
||||
<button class="pill">Werkzeug</button>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#999;margin-top:8px">Horizontal scrollbar bei Überlauf · min-height 32px</div>
|
||||
</div>
|
||||
|
||||
<div class="comp-box">
|
||||
<div class="comp-label">Galerie-Karte (Telefon — 1 Spalte)</div>
|
||||
<div class="gcard">
|
||||
<div class="gcard-img"></div>
|
||||
<div class="gcard-body">
|
||||
<div class="gcard-cat">Möbel</div>
|
||||
<div class="gcard-title">Schreibtisch Eiche</div>
|
||||
<div class="status-free" style="font-size:10px;margin-top:2px">✓ Frei</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gcard">
|
||||
<div class="gcard-img" style="background:#B8AA98"></div>
|
||||
<div class="gcard-body">
|
||||
<div class="gcard-cat">Schmuck</div>
|
||||
<div class="gcard-title">Goldbrosche</div>
|
||||
<div class="status-taken" style="font-size:10px;margin-top:2px">● Renate</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comp-box">
|
||||
<div class="comp-label">Galerie-Karte (Tablet — 2 Spalten)</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||
<div style="border:1px solid #E0D8CC;border-radius:8px;overflow:hidden;background:#fff">
|
||||
<div style="background:#C4B8A8;aspect-ratio:1"></div>
|
||||
<div style="padding:7px 9px">
|
||||
<div class="gcard-cat">Möbel</div>
|
||||
<div class="status-free" style="font-size:9px;margin-top:2px">✓ Frei</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border:1px solid #E0D8CC;border-radius:8px;overflow:hidden;background:#fff">
|
||||
<div style="background:#B8AA98;aspect-ratio:1"></div>
|
||||
<div style="padding:7px 9px">
|
||||
<div class="gcard-cat">Schmuck</div>
|
||||
<div class="status-taken" style="font-size:9px;margin-top:2px">● Renate</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comp-box">
|
||||
<div class="comp-label">Navigationsleiste (Familie)</div>
|
||||
<div class="app-nav">
|
||||
<span class="app-logo">Erbstücke Wannsee</span>
|
||||
<span class="app-user">Markus</span>
|
||||
</div>
|
||||
<div style="font-size:10px;color:#999;line-height:1.5">h-11 bg-primary sticky top-0 · App-Name links, Nutzername rechts</div>
|
||||
</div>
|
||||
|
||||
<div class="comp-box">
|
||||
<div class="comp-label">Fokus-Ring (Tastaturnavigation)</div>
|
||||
<button class="btn btn-primary focus-ex">Reservieren</button>
|
||||
<div style="margin-top:10px;font-family:monospace;font-size:10px;background:#F7F4EF;padding:7px 10px;border-radius:4px;color:#5B7A66;line-height:1.6">
|
||||
focus-visible:ring-2<br>
|
||||
focus-visible:ring-primary/45<br>
|
||||
focus-visible:ring-offset-2<br>
|
||||
outline-none
|
||||
</div>
|
||||
<div style="font-size:10px;color:#999;margin-top:6px">Gilt für alle fokussierbaren Elemente. <code>focus-visible</code> statt <code>focus</code> — kein Ring bei Mausklick.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="do-dont">
|
||||
<div>
|
||||
<div class="dd-head do-head">✓ Tun</div>
|
||||
<div class="dd-body">
|
||||
<span class="rule-item do-item">min-h-[44px] auf allen Buttons und interaktiven Elementen</span>
|
||||
<span class="rule-item do-item"><code>aria-label</code> auf allen Icon-only Buttons</span>
|
||||
<span class="rule-item do-item">Status immer mit Zeichen (✓, ●) + Text kommunizieren</span>
|
||||
<span class="rule-item do-item"><code>disabled</code>-Attribut setzen, nicht nur visuell dämpfen</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="dd-head dont-head">✕ Nicht tun</div>
|
||||
<div class="dd-body">
|
||||
<span class="rule-item dont-item"><code>outline: none</code> ohne Ersatz-Fokus-Ring</span>
|
||||
<span class="rule-item dont-item">Farbe als einziger Status-Indikator (Color-Blindness)</span>
|
||||
<span class="rule-item dont-item">Buttons unter 44px Höhe, besonders im Admin auf Mobilgeräten</span>
|
||||
<span class="rule-item dont-item">Placeholder-Text als Label-Ersatz für Formularfelder</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="nh">Tailwind-Konfiguration (tailwind.config)</div>
|
||||
<ul>
|
||||
<li>Alle Token in <code>theme.extend.colors</code>: <code>canvas: 'var(--color-canvas)'</code>, <code>primary: 'var(--color-primary)'</code> usw.</li>
|
||||
<li>CSS Custom Properties in <code>src/app.css</code> unter <code>:root</code> definieren</li>
|
||||
<li>Font-Familien: <code>fontFamily: { serif: ['Lora', 'Georgia', 'serif'], sans: ['Inter', 'system-ui', 'sans-serif'] }</code></li>
|
||||
<li>Google Fonts via <code><link></code> in <code>src/app.html</code>: Lora (400, 400i, 600, 700) + Inter (400, 500, 600, 700, 800)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /doc -->
|
||||
</body>
|
||||
</html>
|
||||
704
docs/superpowers/specs/2026-05-05-erbstuecke-wannsee-views.html
Normal file
704
docs/superpowers/specs/2026-05-05-erbstuecke-wannsee-views.html
Normal file
@@ -0,0 +1,704 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<title>Erbstücke Wannsee — View Specs</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;0,700;1,400&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:'Inter',system-ui,sans-serif;background:#EAE5DC;color:#1C2820;line-height:1.5;font-size:13px;padding:48px 32px 120px}
|
||||
.doc{max-width:1200px;margin:0 auto}
|
||||
|
||||
/* ── Masthead ── */
|
||||
.mh{padding-bottom:24px;border-bottom:3px solid #5B7A66;margin-bottom:56px}
|
||||
.mh .kicker{font-size:9px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:#C4874A}
|
||||
.mh h1{font-family:'Lora',Georgia,serif;font-size:30px;font-weight:700;color:#1C2820;letter-spacing:-.4px;margin-top:7px}
|
||||
.mh p{font-size:13.5px;color:#6B6050;max-width:820px;line-height:1.8;margin-top:12px}
|
||||
.byline{font-size:9px;color:#999;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;margin-top:16px}
|
||||
.tag-row{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
|
||||
.tag{background:#5B7A66;color:#fff;padding:3px 9px;border-radius:2px;font-size:8.5px;font-weight:800;letter-spacing:.8px;text-transform:uppercase}
|
||||
.tag.amber{background:#C4874A}.tag.outline{background:transparent;color:#5B7A66;border:1px solid #5B7A66}.tag.gray{background:#6B6050;color:#fff}
|
||||
|
||||
/* ── Section ── */
|
||||
.section{margin-bottom:80px}
|
||||
.section+.section{border-top:1px dashed #C8C0B4;padding-top:72px}
|
||||
.section-kicker{font-size:9px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:#C4874A;display:block;margin-bottom:6px}
|
||||
.section h2{font-family:'Lora',Georgia,serif;font-size:24px;font-weight:700;color:#1C2820;margin-bottom:10px}
|
||||
.section-desc{font-size:13px;color:#6B6050;line-height:1.75;max-width:780px;margin-bottom:28px}
|
||||
|
||||
/* ── Viewport grid ── */
|
||||
.vp-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:28px;align-items:start;margin-bottom:32px}
|
||||
.vp-label{font-size:9px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:#888;margin-bottom:8px}
|
||||
|
||||
/* ── Screen frame ── */
|
||||
.frame{border-radius:10px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,.12);border:1.5px solid #C8C0B4}
|
||||
.bar{height:20px;background:#C8C0B4;display:flex;align-items:center;padding:0 8px;gap:4px}
|
||||
.bar .d{width:6px;height:6px;border-radius:50%;background:rgba(0,0,0,.2)}
|
||||
|
||||
/* ── App tokens ── */
|
||||
:root{
|
||||
--cv:#F2EDE4;--sf:#fff;--ln:#E0D8CC;
|
||||
--pr:#5B7A66;--pd:#4A6855;--ac:#C4874A;
|
||||
--ink:#1C2820;--im:#6B6050;
|
||||
--fr:#4A7C5C;--tk:#9B6060;
|
||||
--ab:#2A3B30;
|
||||
}
|
||||
|
||||
/* ── App chrome ── */
|
||||
.app-nav{height:44px;background:var(--pr);display:flex;align-items:center;padding:0 14px;flex-shrink:0}
|
||||
.app-logo{font-family:'Lora',Georgia,serif;font-size:13px;font-weight:700;color:#fff}
|
||||
.app-user{margin-left:auto;font-size:10px;color:rgba(255,255,255,.75);font-weight:600}
|
||||
.filter-bar{background:var(--cv);border-bottom:1px solid var(--ln);padding:8px 10px;display:flex;gap:5px;overflow-x:auto;flex-shrink:0}
|
||||
.pill{background:transparent;color:var(--im);border:1.5px solid var(--ln);font-family:'Inter',sans-serif;font-size:10px;font-weight:700;padding:4px 10px;border-radius:20px;min-height:30px;white-space:nowrap;cursor:pointer}
|
||||
.pill.on{background:var(--pr);color:#fff;border-color:var(--pr)}
|
||||
.screen-body{background:var(--cv);padding:10px}
|
||||
|
||||
/* ── Gallery cards ── */
|
||||
.gcard{background:var(--sf);border:1px solid var(--ln);border-radius:8px;overflow:hidden;display:flex;margin-bottom:8px}
|
||||
.gcard-img{width:72px;height:72px;background:#C4B8A8;flex-shrink:0}
|
||||
.gcard-body{padding:10px 12px;display:flex;flex-direction:column;justify-content:center;gap:2px}
|
||||
.gcard-cat{font-size:9px;color:var(--im);font-weight:800;text-transform:uppercase;letter-spacing:.6px}
|
||||
.gcard-title{font-family:'Lora',Georgia,serif;font-size:12px;color:var(--ink);font-weight:600;line-height:1.3}
|
||||
.gcard-sq{background:var(--sf);border:1px solid var(--ln);border-radius:8px;overflow:hidden}
|
||||
.gcard-sq-img{background:#C4B8A8;aspect-ratio:1}
|
||||
.gcard-sq-body{padding:7px 9px}
|
||||
|
||||
/* ── Status ── */
|
||||
.sf{font-size:10px;font-weight:700;color:var(--fr)}
|
||||
.st{font-size:10px;font-weight:700;color:var(--tk)}
|
||||
.sm{font-size:9px;font-weight:700;background:#E8F5EC;color:#2E6645;padding:2px 8px;border-radius:10px;border:1px solid #A8D5B8;display:inline-block;margin-top:2px}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.btn{display:inline-flex;align-items:center;justify-content:center;font-family:'Inter',sans-serif;font-weight:700;border-radius:6px;cursor:pointer;border:none;min-height:44px;padding:0 20px;font-size:13px}
|
||||
.btn-prim{background:var(--pr);color:#fff;width:100%}
|
||||
.btn-danger{background:#FBF0F0;border:1.5px solid var(--tk);color:var(--tk);padding:0 19px;width:100%}
|
||||
.btn-dis{background:var(--cv);border:1.5px solid var(--ln);color:#AAA;cursor:not-allowed;width:100%}
|
||||
|
||||
/* ── Gate Screen ── */
|
||||
.gate-body{background:var(--cv);min-height:380px;display:flex;align-items:center;justify-content:center;padding:24px}
|
||||
.gate-card{background:var(--sf);border:1px solid var(--ln);border-radius:12px;padding:28px 20px;width:100%;max-width:300px;text-align:center}
|
||||
.gate-icon{width:52px;height:52px;border-radius:50%;background:#DFF0E6;display:flex;align-items:center;justify-content:center;margin:0 auto 14px;font-size:24px}
|
||||
.gate-title{font-family:'Lora',Georgia,serif;font-size:18px;font-weight:700;color:var(--ink);margin-bottom:6px}
|
||||
.gate-sub{font-size:12px;color:var(--im);line-height:1.6;margin-bottom:18px}
|
||||
.gate-input{width:100%;height:48px;border:1.5px solid var(--ln);border-radius:8px;background:#FAFAF7;font-family:monospace;font-size:16px;font-weight:600;text-align:center;letter-spacing:4px;color:#AAA;margin-bottom:10px;display:flex;align-items:center;justify-content:center}
|
||||
.gate-hint{font-size:10px;color:#AAA;line-height:1.5;margin-top:12px}
|
||||
|
||||
/* ── Modal ── */
|
||||
.modal-overlay{background:rgba(0,0,0,.45);display:flex;align-items:flex-end}
|
||||
.modal-sheet{background:var(--sf);border-radius:12px 12px 0 0;width:100%}
|
||||
.modal-handle{width:36px;height:4px;background:var(--ln);border-radius:2px;margin:10px auto 14px}
|
||||
.modal-gallery{background:#C4B8A8;aspect-ratio:4/3;position:relative}
|
||||
.modal-counter{position:absolute;bottom:8px;right:10px;background:rgba(0,0,0,.45);color:#fff;font-size:9px;font-weight:700;padding:2px 7px;border-radius:10px}
|
||||
.modal-body{padding:14px}
|
||||
.modal-cat{display:inline-block;font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.6px;background:#DFF0E6;color:#2E6645;padding:2px 9px;border-radius:10px;border:1px solid #A8D5B8;margin-bottom:6px}
|
||||
.modal-title{font-family:'Lora',Georgia,serif;font-size:17px;font-weight:700;color:var(--ink);line-height:1.3;margin-bottom:5px}
|
||||
.modal-note{font-size:12px;color:var(--im);font-style:italic;line-height:1.5;margin-bottom:12px}
|
||||
.modal-div{height:1px;background:var(--ln);margin-bottom:12px}
|
||||
.modal-status{display:flex;flex-direction:column;gap:7px}
|
||||
|
||||
/* ── Admin ── */
|
||||
.admin-layout{display:flex;min-height:420px}
|
||||
.admin-sidebar{width:155px;background:var(--ab);flex-shrink:0;display:flex;flex-direction:column;padding:12px 0}
|
||||
.admin-logo{font-family:'Lora',Georgia,serif;font-size:10px;font-weight:700;color:rgba(255,255,255,.9);padding:0 12px 12px;border-bottom:1px solid rgba(255,255,255,.1);margin-bottom:6px}
|
||||
.admin-logo span{display:block;font-family:'Inter',sans-serif;font-size:7px;font-weight:600;color:rgba(255,255,255,.4);letter-spacing:.5px;text-transform:uppercase;margin-bottom:1px}
|
||||
.slink{display:flex;align-items:center;gap:7px;padding:7px 12px;font-size:10px;font-weight:600;color:rgba(255,255,255,.45);border-left:3px solid transparent}
|
||||
.slink.on{background:rgba(255,255,255,.1);color:#fff;border-left-color:var(--ac)}
|
||||
.admin-content{flex:1;background:var(--cv);padding:14px;overflow:auto}
|
||||
.page-title{font-family:'Lora',Georgia,serif;font-size:14px;font-weight:700;color:var(--ink);margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
||||
.back-btn{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--im);border:1px solid var(--ln);border-radius:4px;padding:2px 7px}
|
||||
|
||||
/* ── Admin table ── */
|
||||
.atable{width:100%;border-collapse:collapse;background:var(--sf);border-radius:6px;overflow:hidden;border:1px solid var(--ln);font-size:11px}
|
||||
.atable th{text-align:left;padding:7px 9px;font-size:8.5px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:.5px;border-bottom:2px solid var(--ln);background:#F7F4EF}
|
||||
.atable td{padding:7px 9px;border-bottom:1px solid var(--ln);vertical-align:middle}
|
||||
.atable tr:last-child td{border-bottom:none}
|
||||
.thumb-sm{width:30px;height:30px;border-radius:4px;background:#C4B8A8}
|
||||
.code-mono{font-family:monospace;font-size:11px;font-weight:600;color:var(--pr);letter-spacing:2px}
|
||||
.act-btn{font-size:9px;font-weight:700;border-radius:4px;padding:3px 7px;cursor:pointer;border:none}
|
||||
.act-edit{color:var(--im);background:var(--cv);border:1px solid var(--ln)}
|
||||
.act-del{color:var(--tk);background:#FBF0F0;border:1px solid #E0C0C0}
|
||||
.act-link{color:var(--pr);background:#DFF0E6;border:1px solid #A8D5B8}
|
||||
|
||||
/* ── Dashboard stats ── */
|
||||
.stat-row{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;margin-bottom:12px}
|
||||
.stat-card{background:var(--sf);border:1px solid var(--ln);border-radius:8px;padding:9px;text-align:center}
|
||||
.stat-n{font-family:'Lora',Georgia,serif;font-size:20px;font-weight:700;color:var(--pr)}
|
||||
.stat-l{font-size:8.5px;font-weight:700;color:var(--im);text-transform:uppercase;letter-spacing:.4px;margin-top:1px}
|
||||
|
||||
/* ── Reservations ── */
|
||||
.res-head{background:var(--ab);color:#fff;padding:6px 9px;font-size:9.5px;font-weight:700;border-radius:4px 4px 0 0;margin-top:8px}
|
||||
.res-head:first-child{margin-top:0}
|
||||
.res-body{background:var(--sf);border:1px solid var(--ln);border-radius:0 0 4px 4px;margin-bottom:0}
|
||||
.res-item{display:flex;align-items:center;gap:7px;padding:6px 9px;border-bottom:1px solid var(--ln);font-size:10px}
|
||||
.res-item:last-child{border-bottom:none}
|
||||
.res-thumb{width:26px;height:26px;border-radius:3px;background:#C4B8A8;flex-shrink:0}
|
||||
.res-empty{color:rgba(255,255,255,.35);font-style:italic;padding:6px 9px;font-size:9px}
|
||||
|
||||
/* ── Add-item screen ── */
|
||||
.mobile-shell{background:var(--sf);display:flex;flex-direction:column;min-height:560px}
|
||||
.topbar{height:48px;background:var(--pr);display:flex;align-items:center;padding:0 14px;gap:10px;flex-shrink:0}
|
||||
.topbar-back{font-size:10px;font-weight:700;color:rgba(255,255,255,.8);cursor:pointer}
|
||||
.topbar-title{font-family:'Lora',Georgia,serif;font-size:13px;font-weight:700;color:#fff;flex:1;text-align:center}
|
||||
.topbar-save{font-size:10px;font-weight:800;color:#fff;background:rgba(255,255,255,.2);border:none;border-radius:6px;padding:5px 10px;cursor:pointer;min-height:30px}
|
||||
.topbar-save.off{opacity:.35;cursor:not-allowed}
|
||||
.scroll-body{flex:1;overflow-y:auto;background:var(--cv)}
|
||||
.cam-zone{padding:18px 14px;background:var(--cv);border-bottom:1px solid var(--ln)}
|
||||
.cam-btn{background:var(--pr);color:#fff;border:none;border-radius:10px;width:100%;padding:16px;font-family:'Lora',Georgia,serif;font-size:14px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:10px;min-height:60px}
|
||||
.cam-sub{font-size:10px;color:var(--im);text-align:center;margin-top:9px;line-height:1.5}
|
||||
.cam-link{color:var(--pr);text-decoration:underline;cursor:pointer}
|
||||
.strip-zone{padding:12px 14px;background:var(--sf);border-bottom:1px solid var(--ln)}
|
||||
.strip-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:9px}
|
||||
.strip-ttl{font-size:9px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:var(--im)}
|
||||
.strip-hint{font-size:8.5px;color:#AAA;font-style:italic}
|
||||
.strip-row{display:flex;gap:7px;overflow-x:auto}
|
||||
.sthumb{width:68px;height:68px;border-radius:8px;flex-shrink:0;position:relative}
|
||||
.sthumb .badge{position:absolute;top:4px;left:4px;background:var(--ac);color:#fff;font-size:7px;font-weight:800;padding:2px 5px;border-radius:3px}
|
||||
.sthumb .rm{position:absolute;top:4px;right:4px;background:rgba(0,0,0,.55);color:#fff;width:17px;height:17px;border-radius:50%;font-size:9px;font-weight:800;display:flex;align-items:center;justify-content:center;cursor:pointer}
|
||||
.st-add{width:68px;height:68px;border-radius:8px;border:2px dashed var(--ln);flex-shrink:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:var(--cv);cursor:pointer}
|
||||
.form-sec{padding:14px;display:flex;flex-direction:column;gap:12px;background:var(--sf)}
|
||||
.f-lbl{font-size:9.5px;font-weight:800;text-transform:uppercase;letter-spacing:.4px;color:var(--im);margin-bottom:4px;display:flex;align-items:center;gap:5px}
|
||||
.f-req{color:var(--tk);font-size:9px}
|
||||
.f-opt{font-size:9px;font-weight:400;color:#BBB;font-style:italic;letter-spacing:0;text-transform:none}
|
||||
.f-sel{width:100%;height:46px;border:1.5px solid var(--pr);border-radius:8px;background:var(--sf);font-size:13px;color:var(--ink);padding:0 12px;display:flex;align-items:center;justify-content:space-between}
|
||||
.f-in{width:100%;height:46px;border:1.5px solid var(--ln);border-radius:8px;background:#FAFAF7;font-size:13px;color:var(--ink);padding:0 12px;display:flex;align-items:center}
|
||||
.f-in.focus{border-color:var(--pr);box-shadow:0 0 0 3px rgba(91,122,102,.15)}
|
||||
.f-in.empty{color:#AAA;font-style:italic}
|
||||
.f-ta{width:100%;min-height:72px;border:1.5px solid var(--ln);border-radius:8px;background:#FAFAF7;font-size:13px;color:var(--im);padding:10px 12px;font-style:italic;display:flex;align-items:flex-start}
|
||||
.save-bar{background:var(--sf);border-top:1.5px solid var(--ln);padding:10px 14px;display:flex;gap:9px;flex-shrink:0;box-shadow:0 -2px 10px rgba(0,0,0,.06)}
|
||||
.s-cancel{flex:1;height:46px;border:1.5px solid var(--ln);border-radius:8px;background:transparent;font-family:'Inter',sans-serif;font-size:12px;font-weight:700;color:var(--im);cursor:pointer}
|
||||
.s-save{flex:2;height:46px;border:none;border-radius:8px;background:var(--pr);font-family:'Inter',sans-serif;font-size:13px;font-weight:700;color:#fff;cursor:pointer}
|
||||
.s-save.off{opacity:.4;cursor:not-allowed}
|
||||
|
||||
/* ── Impl table ── */
|
||||
.impl{background:#fff;border:1px solid #E0D8CC;border-radius:6px;padding:22px 26px;margin-top:0}
|
||||
.impl h3{font-size:9px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;color:#5B7A66;margin-bottom:14px}
|
||||
.impl-table{width:100%;border-collapse:collapse;font-size:11.5px}
|
||||
.impl-table th{text-align:left;font-size:9px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;color:#5B7A66;padding:7px 9px;background:#F7F4EF;border-bottom:2px solid #E0D8CC}
|
||||
.impl-table td{padding:8px 9px;border-bottom:1px solid #EDE8E0;vertical-align:top;line-height:1.5;color:#444}
|
||||
.impl-table td:first-child{font-weight:700;color:#1C2820;width:22%}
|
||||
.impl-table td code{font-family:monospace;font-size:10.5px;background:#F2EDE4;padding:1px 5px;border-radius:2px;color:#5B7A66}
|
||||
.impl-table td.px{color:#777;font-size:11px;width:9%}
|
||||
.impl-table td.note{color:#888;font-size:11px;font-style:italic}
|
||||
.notes{background:#F9F7F3;border-left:3px solid #C4874A;padding:14px 18px;border-radius:0 4px 4px 0;margin-top:20px}
|
||||
.notes .nh{font-size:9px;font-weight:800;letter-spacing:1px;text-transform:uppercase;color:#C4874A;margin-bottom:7px}
|
||||
.notes ul{list-style:none;display:flex;flex-direction:column;gap:6px}
|
||||
.notes li{font-size:12px;color:#333;padding-left:16px;position:relative;line-height:1.7}
|
||||
.notes li::before{content:"•";position:absolute;left:0;color:#C4874A;font-weight:800}
|
||||
.notes li code{font-family:monospace;font-size:11px;background:#F2EDE4;padding:1px 4px;border-radius:2px;color:#5B7A66}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="doc">
|
||||
|
||||
<!-- MASTHEAD -->
|
||||
<div class="mh">
|
||||
<div class="kicker">UI/UX Spec · Implementation-ready</div>
|
||||
<h1>Erbstücke Wannsee — Views</h1>
|
||||
<p>9 Views: Gate Screen · Galerie · Artikel-Modal · Admin Login · Admin Inventar · Artikel hinzufügen/bearbeiten · Codes · Reservierungen · Übersicht. Alle Mockups auf ~55 % skaliert. Impl-Ref-Tabellen enthalten exakte Tailwind-Klassen und Pixel-Werte.</p>
|
||||
<div class="byline">Leonie Voss · 2026-05-05 · Final · Verweis: 2026-05-05-erbstuecke-wannsee-design-system.html</div>
|
||||
<div class="tag-row">
|
||||
<span class="tag">9 views</span>
|
||||
<span class="tag amber">familie + admin</span>
|
||||
<span class="tag outline">320px+</span>
|
||||
<span class="tag outline">WCAG AA</span>
|
||||
<span class="tag gray">kein dark mode</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════ VIEW 01 — GATE ══ -->
|
||||
<div class="section">
|
||||
<span class="section-kicker">View 01 — Route: /</span>
|
||||
<h2>Gate Screen — Code-Eingabe</h2>
|
||||
<p class="section-desc">Einzige öffentlich sichtbare Seite. Kein Inhalt ohne gültigen Code. Der <code>?code=</code>-URL-Parameter wird serverseitig in <code>+page.server.ts</code> abgefangen — Cookie setzen → redirect <code>/galerie</code>. Manuelle Eingabe als Fallback.</p>
|
||||
|
||||
<div style="max-width:320px;margin-bottom:28px">
|
||||
<div class="vp-label">📱 Alle Viewports — zentrierte Karte</div>
|
||||
<div class="frame">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="gate-body">
|
||||
<div class="gate-card">
|
||||
<div class="gate-icon">🏡</div>
|
||||
<div class="gate-title">Erbstücke Wannsee</div>
|
||||
<div class="gate-sub">Gib deinen persönlichen Code ein, um die Artikel zu sehen:</div>
|
||||
<div class="gate-input">AB3K · · · ·</div>
|
||||
<button class="btn btn-prim" style="min-height:48px;font-size:14px">Weiter →</button>
|
||||
<div class="gate-hint">Noch kein Code? Wende dich an Marcel oder Tante.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="impl">
|
||||
<h3>Implementierungs-Referenz — Gate Screen</h3>
|
||||
<table class="impl-table">
|
||||
<thead><tr><th>Element</th><th>Tailwind-Klassen</th><th class="px">Px</th><th class="note">Notiz</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Seite</td><td><code>min-h-screen bg-canvas flex items-center justify-center p-6</code></td><td class="px">—</td><td class="note">Canvas-Hintergrund, vertikal zentriert</td></tr>
|
||||
<tr><td>Karte</td><td><code>bg-surface border border-line rounded-xl p-7 w-full max-w-sm text-center shadow-sm</code></td><td class="px">28px pad</td><td class="note">max-width 384px</td></tr>
|
||||
<tr><td>Icon</td><td><code>w-[52px] h-[52px] rounded-full bg-[#DFF0E6] flex items-center justify-center text-2xl mx-auto mb-3.5</code></td><td class="px">52px</td><td class="note">Dekorativ — aria-hidden</td></tr>
|
||||
<tr><td>Titel</td><td><code>font-serif text-xl font-bold text-ink mb-1.5</code></td><td class="px">20px/700</td><td class="note">Lora</td></tr>
|
||||
<tr><td>Subtext</td><td><code>text-sm text-ink-muted leading-relaxed mb-5</code></td><td class="px">14px</td><td class="note">Inter</td></tr>
|
||||
<tr><td>Code-Input</td><td><code>font-mono text-base font-semibold tracking-[4px] uppercase text-center w-full h-12 border-1.5 border-line rounded-lg bg-[#FAFAF7] mb-3 focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/30</code></td><td class="px">48px</td><td class="note">inputmode="text" autocomplete="off" spellcheck="false"</td></tr>
|
||||
<tr><td>Weiter-Button</td><td><code>bg-primary hover:bg-primary-dark text-white w-full min-h-[48px] rounded-lg font-bold text-sm</code></td><td class="px">48px</td><td class="note">SvelteKit Form Action — funktioniert ohne JS</td></tr>
|
||||
<tr><td>Fehler-Text</td><td><code>text-xs text-status-taken mt-2 flex items-center gap-1.5</code></td><td class="px">12px</td><td class="note">⚠ Icon + Text — kein Farbe allein</td></tr>
|
||||
<tr><td>Hinweis</td><td><code>text-[10px] text-ink-muted leading-relaxed mt-3</code></td><td class="px">10px</td><td class="note">—</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="notes">
|
||||
<div class="nh">Verhalten</div>
|
||||
<ul>
|
||||
<li>GET <code>/?code=AB3K7MN2</code> → Server liest <code>url.searchParams.get('code')</code>, prüft gegen DB, setzt HTTP-only Cookie <code>family_code</code>, leitet zu <code>/galerie</code></li>
|
||||
<li>Manuell falscher Code → Fehlermeldung: „Code nicht bekannt — bitte prüfe die Eingabe." mit ⚠-Icon</li>
|
||||
<li>Gültiger Cookie vorhanden → Redirect zu <code>/galerie</code> ohne Gate Screen zu zeigen (<code>hooks.server.ts</code>)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════ VIEW 02 — GALERIE ══ -->
|
||||
<div class="section">
|
||||
<span class="section-kicker">View 02 — Route: /galerie</span>
|
||||
<h2>Galerie — Artikel-Übersicht</h2>
|
||||
<p class="section-desc">Hauptansicht für Familienmitglieder. Telefon: 1-Spalte-Liste (horizontale Karten, Foto 72 px links). Tablet/Desktop: 2-Spalten-Grid (quadratische Karten). Kategorie-Filter als scrollbare Pill-Leiste. Kein Artikel-Titel in der Galerie — nur Kategorie + Status.</p>
|
||||
|
||||
<div class="vp-grid">
|
||||
<div>
|
||||
<div class="vp-label">📱 Telefon ≤ 767 px — Liste</div>
|
||||
<div class="frame">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="app-nav"><span class="app-logo">Erbstücke Wannsee</span><span class="app-user">Markus</span></div>
|
||||
<div class="filter-bar">
|
||||
<button class="pill on">Alle</button>
|
||||
<button class="pill">Möbel</button>
|
||||
<button class="pill">Bücher</button>
|
||||
<button class="pill">Schmuck</button>
|
||||
<button class="pill">Werkzeug</button>
|
||||
</div>
|
||||
<div class="screen-body">
|
||||
<div class="gcard"><div class="gcard-img"></div><div class="gcard-body"><div class="gcard-cat">Möbel</div><div class="gcard-title">Schreibtisch Eiche</div><div class="sf">✓ Frei</div></div></div>
|
||||
<div class="gcard"><div class="gcard-img" style="background:#B8AA98"></div><div class="gcard-body"><div class="gcard-cat">Schmuck</div><div class="gcard-title">Goldbrosche</div><div class="st">● Renate</div></div></div>
|
||||
<div class="gcard"><div class="gcard-img" style="background:#CBBFB0"></div><div class="gcard-body"><div class="gcard-cat">Bücher</div><div class="gcard-title">Goethe Gesamtausgabe</div><div class="sm">✓ Meine Reservierung</div></div></div>
|
||||
<div class="gcard"><div class="gcard-img" style="background:#C0B4A4"></div><div class="gcard-body"><div class="gcard-cat">Kunstwerke</div><div class="gcard-title">Aquarell Wannsee</div><div class="sf">✓ Frei</div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="vp-label">💻 Tablet ≥ 768 px — 2-Spalten-Grid</div>
|
||||
<div class="frame">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="app-nav"><span class="app-logo">Erbstücke Wannsee</span><span class="app-user">Markus</span></div>
|
||||
<div class="filter-bar">
|
||||
<button class="pill on">Alle</button>
|
||||
<button class="pill">Möbel</button>
|
||||
<button class="pill">Bücher</button>
|
||||
<button class="pill">Schmuck</button>
|
||||
</div>
|
||||
<div class="screen-body">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||
<div class="gcard-sq"><div class="gcard-sq-img"></div><div class="gcard-sq-body"><div class="gcard-cat" style="font-size:8px">Möbel</div><div class="sf" style="font-size:9px;margin-top:2px">✓ Frei</div></div></div>
|
||||
<div class="gcard-sq"><div class="gcard-sq-img" style="background:#B8AA98"></div><div class="gcard-sq-body"><div class="gcard-cat" style="font-size:8px">Schmuck</div><div class="st" style="font-size:9px;margin-top:2px">● Renate</div></div></div>
|
||||
<div class="gcard-sq"><div class="gcard-sq-img" style="background:#CBBFB0"></div><div class="gcard-sq-body"><div class="gcard-cat" style="font-size:8px">Bücher</div><div class="sm" style="font-size:8px;margin-top:2px">✓ Meine Res.</div></div></div>
|
||||
<div class="gcard-sq"><div class="gcard-sq-img" style="background:#C0B4A4"></div><div class="gcard-sq-body"><div class="gcard-cat" style="font-size:8px">Kunstwerke</div><div class="sf" style="font-size:9px;margin-top:2px">✓ Frei</div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="impl">
|
||||
<h3>Implementierungs-Referenz — Galerie</h3>
|
||||
<table class="impl-table">
|
||||
<thead><tr><th>Element</th><th>Tailwind-Klassen</th><th class="px">Px</th><th class="note">Notiz</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Nav</td><td><code>h-11 bg-primary sticky top-0 z-10 flex items-center px-3.5</code></td><td class="px">44px</td><td class="note">Sticky — immer sichtbar</td></tr>
|
||||
<tr><td>App-Logo</td><td><code>font-serif text-sm font-bold text-white</code></td><td class="px">14px/700</td><td class="note">Lora</td></tr>
|
||||
<tr><td>Nutzername</td><td><code>ml-auto text-[10px] font-semibold text-white/75</code></td><td class="px">10px</td><td class="note">„Angemeldet als: Markus" — Display-Name aus Cookie</td></tr>
|
||||
<tr><td>Filter-Leiste</td><td><code>bg-canvas border-b border-line px-2.5 py-2 flex gap-1.5 overflow-x-auto scrollbar-hide sticky top-11 z-10</code></td><td class="px">—</td><td class="note">Sticky unter Nav</td></tr>
|
||||
<tr><td>Pill aktiv</td><td><code>bg-primary text-white border-primary text-[10px] font-bold px-2.5 py-1.5 rounded-full min-h-[30px]</code></td><td class="px">30px min.</td><td class="note">—</td></tr>
|
||||
<tr><td>Pill inaktiv</td><td><code>bg-transparent text-ink-muted border-1.5 border-line text-[10px] font-semibold px-2.5 py-1.5 rounded-full min-h-[30px]</code></td><td class="px">—</td><td class="note">—</td></tr>
|
||||
<tr><td>Grid (Telefon)</td><td><code>flex flex-col gap-2 p-2.5</code></td><td class="px">≤ 767px</td><td class="note">1-Spalte-Liste</td></tr>
|
||||
<tr><td>Grid (Tablet+)</td><td><code>grid grid-cols-2 gap-2 p-2.5</code> via <code>md:grid md:grid-cols-2</code></td><td class="px">≥ 768px</td><td class="note">2-Spalten-Grid</td></tr>
|
||||
<tr><td>Karte (Liste)</td><td><code>bg-surface border border-line rounded-lg overflow-hidden flex cursor-pointer hover:shadow-sm transition-shadow</code></td><td class="px">—</td><td class="note">Klick öffnet Modal</td></tr>
|
||||
<tr><td>Karte Foto (Liste)</td><td><code>w-[72px] h-[72px] object-cover flex-shrink-0</code></td><td class="px">72×72</td><td class="note">alt="" für dekorative Fotos; aria-describedby auf Karte für SR</td></tr>
|
||||
<tr><td>Karte Foto (Grid)</td><td><code>aspect-square w-full object-cover</code></td><td class="px">quadratisch</td><td class="note">—</td></tr>
|
||||
<tr><td>Kategorie-Label</td><td><code>text-[9px] font-extrabold uppercase tracking-wider text-ink-muted</code></td><td class="px">9px</td><td class="note">—</td></tr>
|
||||
<tr><td>Status Frei</td><td><code>text-[10px] font-bold text-status-free</code></td><td class="px">10px</td><td class="note">„✓ Frei"</td></tr>
|
||||
<tr><td>Status Reserviert</td><td><code>text-[10px] font-bold text-status-taken</code></td><td class="px">10px</td><td class="note">„● [Display-Name]"</td></tr>
|
||||
<tr><td>Status Meins</td><td><code>text-[9px] font-bold bg-[#E8F5EC] text-[#2E6645] px-2 py-0.5 rounded-full border border-[#A8D5B8] mt-0.5 inline-block</code></td><td class="px">Badge</td><td class="note">„✓ Meine Reservierung"</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════ VIEW 03 — MODAL ══ -->
|
||||
<div class="section">
|
||||
<span class="section-kicker">View 03 — Route: /galerie (Modal über Galerie)</span>
|
||||
<h2>Artikel-Modal — 3 Zustände</h2>
|
||||
<p class="section-desc">Bottom Sheet öffnet sich beim Antippen einer Galerie-Karte. Foto-Galerie mit Wisch-Geste (touch-action: pan-x) und Seitenzähler. Reservierungsbereich passt sich an Status an. Schließen durch Tippen auf Overlay oder Handle-Swipe nach unten.</p>
|
||||
|
||||
<div class="vp-grid">
|
||||
<div>
|
||||
<div class="vp-label">Zustand A — Frei</div>
|
||||
<div class="frame">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="modal-overlay" style="min-height:340px;padding-top:50px">
|
||||
<div class="modal-sheet">
|
||||
<div class="modal-handle"></div>
|
||||
<div class="modal-gallery"><div class="modal-counter">1 / 3 →</div></div>
|
||||
<div class="modal-body">
|
||||
<span class="modal-cat">Möbel</span>
|
||||
<div class="modal-title">Schreibtisch aus Eichenholz</div>
|
||||
<div class="modal-note">Kleine Schramme oben links.</div>
|
||||
<div class="modal-div"></div>
|
||||
<div class="modal-status"><button class="btn btn-prim" style="min-height:48px;font-size:14px">Reservieren</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="vp-label">Zustand B — Meine Reservierung</div>
|
||||
<div class="frame">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="modal-overlay" style="min-height:340px;padding-top:50px">
|
||||
<div class="modal-sheet">
|
||||
<div class="modal-handle"></div>
|
||||
<div class="modal-gallery" style="background:#CBBFB0"></div>
|
||||
<div class="modal-body">
|
||||
<span class="modal-cat">Bücher</span>
|
||||
<div class="modal-title">Goethe Gesamtausgabe</div>
|
||||
<div class="modal-div"></div>
|
||||
<div class="modal-status">
|
||||
<div class="sm" style="font-size:12px;padding:7px 14px;border-radius:8px;text-align:center;margin-bottom:7px">✓ Meine Reservierung</div>
|
||||
<button class="btn btn-danger" style="min-height:44px;font-size:13px">Reservierung aufheben</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="vp-label">Zustand C — Fremde Reservierung</div>
|
||||
<div class="frame">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="modal-overlay" style="min-height:340px;padding-top:50px">
|
||||
<div class="modal-sheet">
|
||||
<div class="modal-handle"></div>
|
||||
<div class="modal-gallery" style="background:#B8AA98"></div>
|
||||
<div class="modal-body">
|
||||
<span class="modal-cat">Schmuck</span>
|
||||
<div class="modal-title">Goldbrosche mit Granat</div>
|
||||
<div class="modal-div"></div>
|
||||
<div class="modal-status">
|
||||
<div style="font-size:13px;font-weight:700;color:var(--tk);margin-bottom:7px">● Reserviert von Renate</div>
|
||||
<button class="btn btn-dis" disabled style="min-height:44px;font-size:13px">Nicht verfügbar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="impl">
|
||||
<h3>Implementierungs-Referenz — Artikel-Modal</h3>
|
||||
<table class="impl-table">
|
||||
<thead><tr><th>Element</th><th>Tailwind-Klassen</th><th class="px">Px</th><th class="note">Notiz</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Overlay</td><td><code>fixed inset-0 bg-black/45 flex items-end z-50</code></td><td class="px">—</td><td class="note">Klick schließt Modal · role="dialog" aria-modal="true"</td></tr>
|
||||
<tr><td>Sheet</td><td><code>bg-surface rounded-t-xl w-full max-h-[88dvh] overflow-y-auto</code></td><td class="px">88dvh max</td><td class="note">SvelteKit slide-Transition von unten</td></tr>
|
||||
<tr><td>Handle</td><td><code>w-9 h-1 bg-line rounded mx-auto mt-2.5 mb-3.5</code></td><td class="px">4px</td><td class="note">Swipe-Down schließt Modal</td></tr>
|
||||
<tr><td>Foto-Galerie</td><td><code>aspect-[4/3] w-full object-cover bg-canvas touch-pan-x overflow-hidden</code></td><td class="px">4:3</td><td class="note">Einfaches Swipe — kein JS-Framework nötig</td></tr>
|
||||
<tr><td>Foto-Zähler</td><td><code>absolute bottom-2 right-2.5 bg-black/45 text-white text-[9px] font-bold px-1.5 py-0.5 rounded-full</code></td><td class="px">—</td><td class="note">„1 / 3 →" — ausblenden bei nur 1 Foto</td></tr>
|
||||
<tr><td>Modal-Body</td><td><code>p-4</code></td><td class="px">16px</td><td class="note">—</td></tr>
|
||||
<tr><td>Kategorie-Badge</td><td><code>inline-block text-[9px] font-extrabold uppercase tracking-wider bg-[#DFF0E6] text-[#2E6645] px-2.5 py-0.5 rounded-full border border-[#A8D5B8] mb-1.5</code></td><td class="px">—</td><td class="note">—</td></tr>
|
||||
<tr><td>Artikel-Titel</td><td><code>font-serif text-[18px] font-bold text-ink leading-snug mb-1.5</code></td><td class="px">18px/700</td><td class="note">Lora</td></tr>
|
||||
<tr><td>Notiz</td><td><code>text-sm text-ink-muted italic leading-relaxed mb-3</code></td><td class="px">14px</td><td class="note">Nur rendern wenn <code>artikel.notiz</code> vorhanden</td></tr>
|
||||
<tr><td>Reservieren (A)</td><td><code>bg-primary hover:bg-primary-dark text-white w-full min-h-[48px] rounded-lg font-bold text-sm</code></td><td class="px">48px</td><td class="note">Form Action POST</td></tr>
|
||||
<tr><td>Aufheben (B)</td><td><code>bg-[#FBF0F0] border-1.5 border-status-taken text-status-taken w-full min-h-[44px] rounded-lg font-bold text-sm</code></td><td class="px">44px</td><td class="note">Nur wenn <code>reservierung.code_id === locals.familyCode.id</code></td></tr>
|
||||
<tr><td>Gesperrt (C)</td><td><code>bg-canvas border-1.5 border-line text-[#AAA] w-full min-h-[44px] rounded-lg font-bold text-sm cursor-not-allowed</code></td><td class="px">44px</td><td class="note">disabled-Attribut setzen</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="notes">
|
||||
<div class="nh">Reservierungslogik</div>
|
||||
<ul>
|
||||
<li>Zustand wird serverseitig bestimmt: <code>reservierungen</code>-Row existiert? → Taken. <code>code_id === locals.familyCode.id</code>? → Mine.</li>
|
||||
<li>POST Reservieren → UNIQUE-Constraint auf <code>reservierungen.artikel_id</code> verhindert Doppel-Reservierung atomar</li>
|
||||
<li>Constraint-Fehler → Galerie neu laden, Modal zeigt Zustand C</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════ VIEW 04 — ADMIN LOGIN ══ -->
|
||||
<div class="section">
|
||||
<span class="section-kicker">View 04 — Route: /admin/login</span>
|
||||
<h2>Admin — Login</h2>
|
||||
<p class="section-desc">Eigenständige Seite mit dunklem Hintergrund (Admin-BG). Vollständig getrennt von der Familien-Ansicht — kein Code-Zugang. Benutzername (Marcel / Renate / Berit) + Passwort. Kein User-Enumeration bei falschen Credentials.</p>
|
||||
|
||||
<div style="max-width:320px;margin-bottom:28px">
|
||||
<div class="frame">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div style="background:var(--ab);min-height:380px;display:flex;align-items:center;justify-content:center;padding:24px">
|
||||
<div style="background:var(--sf);border:1px solid var(--ln);border-radius:10px;padding:22px;width:100%">
|
||||
<div style="font-size:8px;font-weight:800;text-transform:uppercase;letter-spacing:1.5px;color:#AAA;margin-bottom:5px">Admin-Zugang</div>
|
||||
<div style="font-family:'Lora',Georgia,serif;font-size:17px;font-weight:700;color:var(--ink);margin-bottom:18px">Erbstücke Wannsee</div>
|
||||
<div style="margin-bottom:10px">
|
||||
<div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--im);margin-bottom:4px">Benutzername</div>
|
||||
<div style="height:40px;border:1.5px solid var(--ln);border-radius:6px;background:#FAFAF7;font-size:13px;color:#AAA;padding:0 10px;display:flex;align-items:center">Marcel</div>
|
||||
</div>
|
||||
<div style="margin-bottom:14px">
|
||||
<div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--im);margin-bottom:4px">Passwort</div>
|
||||
<div style="height:40px;border:1.5px solid var(--ln);border-radius:6px;background:#FAFAF7;font-size:13px;color:#AAA;padding:0 10px;display:flex;align-items:center">••••••••</div>
|
||||
</div>
|
||||
<button style="background:var(--ab);color:#fff;width:100%;min-height:44px;font-family:'Inter',sans-serif;font-size:12px;font-weight:700;border:none;border-radius:6px;cursor:pointer">Anmelden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="impl">
|
||||
<h3>Implementierungs-Referenz — Admin Login</h3>
|
||||
<table class="impl-table">
|
||||
<thead><tr><th>Element</th><th>Tailwind-Klassen</th><th class="px">Px</th><th class="note">Notiz</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Seite</td><td><code>min-h-screen bg-admin flex items-center justify-center p-6</code></td><td class="px">—</td><td class="note">Admin-Dunkelgrün als Background</td></tr>
|
||||
<tr><td>Karte</td><td><code>bg-surface border border-line rounded-xl p-[22px] w-full max-w-xs</code></td><td class="px">22px pad</td><td class="note">Kein Shadow — wirkt geerdet auf dunklem BG</td></tr>
|
||||
<tr><td>Form-Label</td><td><code>text-[9px] font-extrabold uppercase tracking-wide text-ink-muted mb-1 block</code></td><td class="px">9px</td><td class="note">Immer <code><label for="…"></code> verknüpfen</td></tr>
|
||||
<tr><td>Input</td><td><code>w-full h-10 border-1.5 border-line rounded-md bg-[#FAFAF7] px-2.5 text-[13px] focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/30</code></td><td class="px">40px</td><td class="note">—</td></tr>
|
||||
<tr><td>Anmelden-Button</td><td><code>bg-admin hover:bg-[#1E2C24] text-white w-full min-h-[44px] rounded-md font-bold text-[13px]</code></td><td class="px">44px</td><td class="note">bcrypt.compare → Cookie <code>admin_session</code></td></tr>
|
||||
<tr><td>Fehler</td><td><code>text-xs text-status-taken mt-2</code></td><td class="px">12px</td><td class="note">Identische Meldung für falschen User + falsches Passwort — kein User-Enumeration</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════ VIEWS 05–08 — ADMIN ══ -->
|
||||
<div class="section">
|
||||
<span class="section-kicker">Views 05–08 — Route: /admin/*</span>
|
||||
<h2>Admin-Bereich — Sidebar-Layout</h2>
|
||||
<p class="section-desc">Gemeinsames Layout-Gerüst für alle vier Admin-Bereiche. Desktop: feste Sidebar (176 px) links, Content-Bereich rechts. Mobil: Sidebar klappt zu Hamburger-Drawer. Amber-Linie markiert den aktiven Bereich.</p>
|
||||
|
||||
<!-- Inventar -->
|
||||
<div style="margin-bottom:36px">
|
||||
<div class="vp-label">View 05 — /admin/inventar</div>
|
||||
<div class="frame" style="max-width:680px">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="admin-layout">
|
||||
<div class="admin-sidebar">
|
||||
<div class="admin-logo"><span>Admin</span>Erbstücke Wannsee</div>
|
||||
<div class="slink on">📦 Inventar</div>
|
||||
<div class="slink">🔑 Codes</div>
|
||||
<div class="slink">📋 Reservierungen</div>
|
||||
<div class="slink">📊 Übersicht</div>
|
||||
</div>
|
||||
<div class="admin-content">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:11px">
|
||||
<div class="page-title" style="margin:0">Inventar</div>
|
||||
<button style="background:var(--ac);color:#fff;border:none;border-radius:5px;font-family:'Inter',sans-serif;font-size:10px;font-weight:700;padding:0 10px;min-height:32px;cursor:pointer">+ Hinzufügen</button>
|
||||
</div>
|
||||
<table class="atable">
|
||||
<thead><tr><th></th><th>Artikel</th><th>Kategorie</th><th>Status</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><div class="thumb-sm"></div></td><td style="font-weight:600;font-size:11px">Schreibtisch Eiche</td><td style="color:var(--im)">Möbel</td><td><span class="sf" style="font-size:9.5px">✓ Frei</span></td><td style="text-align:right;display:flex;gap:3px;justify-content:flex-end"><button class="act-btn act-edit">✎</button><button class="act-btn act-del">✕</button></td></tr>
|
||||
<tr><td><div class="thumb-sm" style="background:#B8AA98"></div></td><td style="font-weight:600;font-size:11px">Goldbrosche</td><td style="color:var(--im)">Schmuck</td><td><span class="st" style="font-size:9.5px">● Renate</span></td><td style="text-align:right;display:flex;gap:3px;justify-content:flex-end"><button class="act-btn act-edit">✎</button><button class="act-btn act-del" style="opacity:.35;cursor:not-allowed">✕</button></td></tr>
|
||||
<tr><td><div class="thumb-sm" style="background:#CBBFB0"></div></td><td style="font-weight:600;font-size:11px">Goethe Gesamtausgabe</td><td style="color:var(--im)">Bücher</td><td><span class="sm" style="font-size:8.5px">✓ Markus</span></td><td style="text-align:right;display:flex;gap:3px;justify-content:flex-end"><button class="act-btn act-edit">✎</button><button class="act-btn act-del" style="opacity:.35;cursor:not-allowed">✕</button></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="font-size:9px;color:#AAA;margin-top:6px">Löschen nur wenn nicht reserviert.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Artikel hinzufügen -->
|
||||
<div style="margin-bottom:36px">
|
||||
<div class="vp-label">View 05b — /admin/inventar/neu & /admin/inventar/[id]/bearbeiten — Vollbild Mobile</div>
|
||||
<div class="vp-grid" style="margin-bottom:0">
|
||||
<div>
|
||||
<div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#AAA;margin-bottom:6px">Zustand A — Kein Foto</div>
|
||||
<div class="frame" style="max-width:300px">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="mobile-shell">
|
||||
<div class="topbar"><div class="topbar-back">← Inventar</div><div class="topbar-title">Hinzufügen</div><button class="topbar-save off">Speichern</button></div>
|
||||
<div class="scroll-body">
|
||||
<div class="cam-zone">
|
||||
<button class="cam-btn"><span style="font-size:18px">📷</span> Foto aufnehmen</button>
|
||||
<div class="cam-sub">Öffnet die Kamera. Erstes Foto = Galerie-Thumbnail.<br><span class="cam-link">Oder Datei wählen</span></div>
|
||||
</div>
|
||||
<div class="form-sec" style="opacity:.4;pointer-events:none">
|
||||
<div class="field"><div class="f-lbl">Kategorie <span class="f-req">*</span></div><div class="f-in empty">Erst Foto aufnehmen…</div></div>
|
||||
<div class="field"><div class="f-lbl">Titel <span class="f-opt">(optional)</span></div><div class="f-in empty">Erst Foto aufnehmen…</div></div>
|
||||
<div class="field"><div class="f-lbl">Notiz <span class="f-opt">(optional)</span></div><div class="f-ta">Erst Foto aufnehmen…</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-bar"><button class="s-cancel">Abbrechen</button><button class="s-save off">Speichern</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#AAA;margin-bottom:6px">Zustand B — Mit Fotos</div>
|
||||
<div class="frame" style="max-width:300px">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="mobile-shell">
|
||||
<div class="topbar"><div class="topbar-back">← Inventar</div><div class="topbar-title">Hinzufügen</div><button class="topbar-save">Speichern</button></div>
|
||||
<div class="scroll-body">
|
||||
<div class="strip-zone">
|
||||
<div class="strip-hdr"><span class="strip-ttl">Fotos (3)</span><span class="strip-hint">Ziehen = sortieren</span></div>
|
||||
<div class="strip-row">
|
||||
<div class="sthumb" style="background:#C4B8A8"><div class="badge">Thumbnail</div><div class="rm">✕</div></div>
|
||||
<div class="sthumb" style="background:#B8AA98"><div class="rm">✕</div></div>
|
||||
<div class="sthumb" style="background:#CBBFB0"><div class="rm">✕</div></div>
|
||||
<div class="st-add"><span style="font-size:20px;color:var(--im);opacity:.5">📷</span><span style="font-size:7.5px;font-weight:700;color:var(--im);opacity:.5;text-transform:uppercase;letter-spacing:.3px">Mehr</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-sec">
|
||||
<div class="field"><div class="f-lbl">Kategorie <span class="f-req">*</span></div><div class="f-sel"><span>Möbel</span><span style="color:var(--im);font-size:10px">▾</span></div></div>
|
||||
<div class="field"><div class="f-lbl">Titel <span class="f-opt">(optional)</span></div><div class="f-in focus">Schreibtisch Eiche</div></div>
|
||||
<div class="field"><div class="f-lbl">Notiz <span class="f-opt">(optional)</span></div><div class="f-ta" style="color:var(--ink);font-style:normal">Kleine Schramme oben links.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-bar"><button class="s-cancel">Abbrechen</button><button class="s-save">Speichern</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Codes -->
|
||||
<div style="margin-bottom:36px">
|
||||
<div class="vp-label">View 06 — /admin/codes</div>
|
||||
<div class="frame" style="max-width:680px">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="admin-layout">
|
||||
<div class="admin-sidebar">
|
||||
<div class="admin-logo"><span>Admin</span>Erbstücke Wannsee</div>
|
||||
<div class="slink">📦 Inventar</div>
|
||||
<div class="slink on">🔑 Codes</div>
|
||||
<div class="slink">📋 Reservierungen</div>
|
||||
<div class="slink">📊 Übersicht</div>
|
||||
</div>
|
||||
<div class="admin-content">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:11px">
|
||||
<div class="page-title" style="margin:0">Zugangscodes</div>
|
||||
<button style="background:var(--ac);color:#fff;border:none;border-radius:5px;font-family:'Inter',sans-serif;font-size:10px;font-weight:700;padding:0 10px;min-height:32px;cursor:pointer">+ Neuer Code</button>
|
||||
</div>
|
||||
<table class="atable">
|
||||
<thead><tr><th>Name</th><th>Code</th><th>Res.</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="font-weight:700">Markus</td><td><span class="code-mono">AB3K7MN2</span></td><td style="color:var(--im)">3</td><td style="display:flex;gap:3px;justify-content:flex-end"><button class="act-btn act-link">🔗 Link</button><button class="act-btn act-del">✕</button></td></tr>
|
||||
<tr><td style="font-weight:700">Renate</td><td><span class="code-mono">XP9QRT4W</span></td><td style="color:var(--im)">1</td><td style="display:flex;gap:3px;justify-content:flex-end"><button class="act-btn act-link">🔗 Link</button><button class="act-btn act-del">✕</button></td></tr>
|
||||
<tr><td style="font-weight:700">Berit</td><td><span class="code-mono">LM2J6VH8</span></td><td style="color:#CCC;font-style:italic">0</td><td style="display:flex;gap:3px;justify-content:flex-end"><button class="act-btn act-link">🔗 Link</button><button class="act-btn act-del">✕</button></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reservierungen -->
|
||||
<div style="margin-bottom:36px">
|
||||
<div class="vp-label">View 07 — /admin/reservierungen</div>
|
||||
<div class="frame" style="max-width:680px">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="admin-layout">
|
||||
<div class="admin-sidebar">
|
||||
<div class="admin-logo"><span>Admin</span>Erbstücke Wannsee</div>
|
||||
<div class="slink">📦 Inventar</div>
|
||||
<div class="slink">🔑 Codes</div>
|
||||
<div class="slink on">📋 Reservierungen</div>
|
||||
<div class="slink">📊 Übersicht</div>
|
||||
</div>
|
||||
<div class="admin-content">
|
||||
<div class="page-title">Reservierungen</div>
|
||||
<div class="res-head">Markus — 3 Artikel</div>
|
||||
<div class="res-body">
|
||||
<div class="res-item"><div class="res-thumb"></div><div><div style="font-weight:600;font-size:10.5px">Schreibtisch Eiche</div><div style="font-size:8.5px;color:var(--im);text-transform:uppercase;font-weight:700;letter-spacing:.4px">Möbel</div></div></div>
|
||||
<div class="res-item"><div class="res-thumb" style="background:#CBBFB0"></div><div><div style="font-weight:600;font-size:10.5px">Goethe Gesamtausgabe</div><div style="font-size:8.5px;color:var(--im);text-transform:uppercase;font-weight:700;letter-spacing:.4px">Bücher</div></div></div>
|
||||
</div>
|
||||
<div class="res-head" style="margin-top:8px">Renate — 1 Artikel</div>
|
||||
<div class="res-body">
|
||||
<div class="res-item"><div class="res-thumb" style="background:#B8AA98"></div><div><div style="font-weight:600;font-size:10.5px">Goldbrosche mit Granat</div><div style="font-size:8.5px;color:var(--im);text-transform:uppercase;font-weight:700;letter-spacing:.4px">Schmuck</div></div></div>
|
||||
</div>
|
||||
<div class="res-head" style="margin-top:8px;opacity:.4">Berit — 0 Artikel</div>
|
||||
<div class="res-body" style="opacity:.4"><div class="res-item" style="font-style:italic;color:#AAA;font-size:10px">Noch keine Reservierungen</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Übersicht -->
|
||||
<div>
|
||||
<div class="vp-label">View 08 — /admin/uebersicht</div>
|
||||
<div class="frame" style="max-width:680px">
|
||||
<div class="bar"><div class="d"></div><div class="d"></div><div class="d"></div></div>
|
||||
<div class="admin-layout">
|
||||
<div class="admin-sidebar">
|
||||
<div class="admin-logo"><span>Admin</span>Erbstücke Wannsee</div>
|
||||
<div class="slink">📦 Inventar</div>
|
||||
<div class="slink">🔑 Codes</div>
|
||||
<div class="slink">📋 Reservierungen</div>
|
||||
<div class="slink on">📊 Übersicht</div>
|
||||
</div>
|
||||
<div class="admin-content">
|
||||
<div class="page-title">Übersicht</div>
|
||||
<div class="stat-row">
|
||||
<div class="stat-card"><div class="stat-n">47</div><div class="stat-l">Gesamt</div></div>
|
||||
<div class="stat-card"><div class="stat-n" style="color:var(--tk)">12</div><div class="stat-l">Reserviert</div></div>
|
||||
<div class="stat-card"><div class="stat-n" style="color:var(--fr)">35</div><div class="stat-l">Frei</div></div>
|
||||
</div>
|
||||
<table class="atable">
|
||||
<thead><tr><th>Kategorie</th><th>Gesamt</th><th>Reserviert</th><th>Frei</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td style="font-weight:600">Möbel</td><td>14</td><td style="color:var(--tk);font-weight:700">5</td><td style="color:var(--fr);font-weight:700">9</td></tr>
|
||||
<tr><td style="font-weight:600">Bücher</td><td>18</td><td style="color:var(--tk);font-weight:700">4</td><td style="color:var(--fr);font-weight:700">14</td></tr>
|
||||
<tr><td style="font-weight:600">Schmuck</td><td>7</td><td style="color:var(--tk);font-weight:700">2</td><td style="color:var(--fr);font-weight:700">5</td></tr>
|
||||
<tr><td style="font-weight:600">Kunstwerke</td><td>4</td><td style="color:var(--tk);font-weight:700">1</td><td style="color:var(--fr);font-weight:700">3</td></tr>
|
||||
<tr><td style="font-weight:600">Küche</td><td>4</td><td style="color:#CCC">0</td><td style="color:var(--fr);font-weight:700">4</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="impl" style="margin-top:32px">
|
||||
<h3>Implementierungs-Referenz — Admin-Layout & Artikel hinzufügen</h3>
|
||||
<table class="impl-table">
|
||||
<thead><tr><th>Element</th><th>Tailwind-Klassen</th><th class="px">Px</th><th class="note">Notiz</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Outer Layout</td><td><code>flex min-h-screen</code></td><td class="px">—</td><td class="note">+layout.svelte unter /admin/</td></tr>
|
||||
<tr><td>Sidebar Desktop</td><td><code>w-44 bg-admin flex-shrink-0 hidden md:flex flex-col py-3</code></td><td class="px">176px</td><td class="note">Dunkelgrün, ab md: sichtbar</td></tr>
|
||||
<tr><td>Sidebar Mobil</td><td>Overlay-Drawer — Toggle via Svelte <code>$state(false)</code></td><td class="px">—</td><td class="note">Hamburger-Icon in Mobile-TopBar</td></tr>
|
||||
<tr><td>Sidebar-Link aktiv</td><td><code>flex items-center gap-2 px-3 py-1.5 text-[10px] font-semibold text-white bg-white/10 border-l-[3px] border-accent</code></td><td class="px">—</td><td class="note">Amber-Linie links</td></tr>
|
||||
<tr><td>Sidebar-Link inaktiv</td><td><code>flex items-center gap-2 px-3 py-1.5 text-[10px] font-semibold text-white/45 border-l-[3px] border-transparent hover:text-white/75</code></td><td class="px">—</td><td class="note">—</td></tr>
|
||||
<tr><td>Content-Bereich</td><td><code>flex-1 bg-canvas p-3.5 overflow-auto</code></td><td class="px">14px</td><td class="note">—</td></tr>
|
||||
<tr><td>Stat-Karten-Grid</td><td><code>grid grid-cols-3 gap-1.5 mb-3</code></td><td class="px">—</td><td class="note">—</td></tr>
|
||||
<tr><td>Kamera-Button</td><td><code>font-serif text-sm font-bold bg-primary text-white w-full min-h-[60px] rounded-xl flex items-center justify-center gap-2.5</code></td><td class="px">60px</td><td class="note">Triggert <code><input capture="environment"></code></td></tr>
|
||||
<tr><td>Thumbnail-Strip</td><td><code>flex gap-1.5 overflow-x-auto pb-0.5</code></td><td class="px">—</td><td class="note">Drag & Drop für Reihenfolge</td></tr>
|
||||
<tr><td>Thumbnail</td><td><code>w-[68px] h-[68px] rounded-lg object-cover flex-shrink-0 relative cursor-grab</code></td><td class="px">68×68</td><td class="note">—</td></tr>
|
||||
<tr><td>Thumbnail Badge</td><td><code>absolute top-1 left-1 bg-accent text-white text-[7px] font-extrabold px-1.5 py-0.5 rounded-sm</code></td><td class="px">—</td><td class="note">Text: „Thumbnail" — nur auf Index 0</td></tr>
|
||||
<tr><td>Kategorie (native select)</td><td><code>w-full h-[46px] border-1.5 border-primary rounded-lg px-3 text-[13px] text-ink bg-surface appearance-none</code></td><td class="px">46px</td><td class="note">Native <code><select></code> für Mobile-Kompatibilität</td></tr>
|
||||
<tr><td>Sticky Save Bar</td><td><code>bg-surface border-t border-line p-2.5 flex gap-2 shadow-[0_-2px_10px_rgba(0,0,0,.06)] flex-shrink-0</code></td><td class="px">—</td><td class="note">Sticky unten — immer erreichbar</td></tr>
|
||||
<tr><td>Code löschen (Bestätigung)</td><td>Native <code>confirm()</code> oder Svelte Dialog-Komponente</td><td class="px">—</td><td class="note">Warnung: „Alle Reservierungen werden gelöscht"</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="notes">
|
||||
<div class="nh">Admin — Implementierungshinweise</div>
|
||||
<ul>
|
||||
<li>Sidebar-Navigation: <code>+layout.svelte</code> unter <code>src/routes/admin/</code> — rendert Sidebar + <code><slot /></code></li>
|
||||
<li>Auth-Guard: <code>+layout.server.ts</code> prüft <code>locals.admin</code> → redirect zu <code>/admin/login</code> bei fehlendem Cookie</li>
|
||||
<li>Artikel-Form: gleiche Svelte-Komponente für /neu und /[id]/bearbeiten — Edit füllt Props vor</li>
|
||||
<li>Foto-Upload: verstecktes <code><input type="file" accept="image/*" capture="environment" multiple></code> — Kamera-Button triggert via <code>.click()</code></li>
|
||||
<li>Fotos als <code>createObjectURL()</code>-Preview im Strip, Upload erst beim Speichern via Multipart Form Action → sharp → WebP</li>
|
||||
<li>Code „🔗 Link"-Button: <code>navigator.clipboard.writeText(url)</code> mit visueller Bestätigung (Button-Text → „Kopiert ✓")</li>
|
||||
<li>Löschen von Codes: Bestätigungs-Dialog mit expliziter Warnung über kaskadierte Reservierungs-Löschung</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /doc -->
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user