diff --git a/docs/anleitung/bilder/schluessel-detail-desktop.png b/docs/anleitung/bilder/schluessel-detail-desktop.png new file mode 100644 index 0000000..e085b4e Binary files /dev/null and b/docs/anleitung/bilder/schluessel-detail-desktop.png differ diff --git a/docs/anleitung/bilder/schluessel-detail-mobil.png b/docs/anleitung/bilder/schluessel-detail-mobil.png new file mode 100644 index 0000000..719f4ad Binary files /dev/null and b/docs/anleitung/bilder/schluessel-detail-mobil.png differ diff --git a/docs/anleitung/bilder/uebersicht-desktop.png b/docs/anleitung/bilder/uebersicht-desktop.png new file mode 100644 index 0000000..d58e294 Binary files /dev/null and b/docs/anleitung/bilder/uebersicht-desktop.png differ diff --git a/docs/anleitung/bilder/uebersicht-mobil.png b/docs/anleitung/bilder/uebersicht-mobil.png new file mode 100644 index 0000000..8ad67ab Binary files /dev/null and b/docs/anleitung/bilder/uebersicht-mobil.png differ diff --git a/docs/issues/002-design-layout.md b/docs/issues/002-design-layout.md index 7768dfe..ea2e2ed 100644 --- a/docs/issues/002-design-layout.md +++ b/docs/issues/002-design-layout.md @@ -1,25 +1,51 @@ # #002 Design-System und Layout -**Status:** offen +**Status:** erledigt **Meilenstein:** v0.1.0 **Labels:** ui -**Geschätzt:** 4 h **Tatsächlich:** – +**Geschätzt:** 4 h **Tatsächlich:** 0,3 h (09:07–09:22) ## Ziel Design laut Vorgabe (Tresor, Graphit + Messing): Tokens, Schriften, Logo, Favicon/PWA-Icons, Grundlayout mit Seitenleiste. ## Aufgaben -- [ ] Design-Tokens als CSS-Variablen + Tailwind-Theme -- [ ] Space Grotesk, IBM Plex Sans, JetBrains Mono selbst gehostet (`@fontsource`) -- [ ] `public/brand/` mit `logo.svg`, `logo-light.svg`, `logo-mono.svg`, `favicon.svg`; PNG-Icons + Manifest per Skript -- [ ] Seitenleiste (Logo, Navigation, Benutzer unten), einklappbar auf schmalen Bildschirmen -- [ ] Grundkomponenten: Karte, Button (primär/sekundär/Gefahr), Badge, Statuspunkt, Tabelle, Diff-Box, Kennzahl-Kachel +- [x] Design-Tokens als CSS-Variablen + Tailwind-Theme +- [x] Space Grotesk, IBM Plex Sans, JetBrains Mono selbst gehostet (`@fontsource`) +- [x] `public/brand/` mit `logo.svg`, `logo-light.svg`, `logo-mono.svg`, `favicon.svg`; PNG-Icons + Manifest per Skript +- [x] Seitenleiste (Logo, Navigation, Benutzer unten), einklappbar auf schmalen Bildschirmen +- [x] Grundkomponenten: Karte, Button (primär/sekundär/Gefahr), Badge, Statuspunkt, Tabelle, Diff-Box, Kennzahl-Kachel ## Akzeptanzkriterien -- [ ] Kontrast ≥ 4,5:1, Fokusrahmen in Messing -- [ ] Kein horizontales Scrollen bei 390 px -- [ ] Klickflächen ≥ 44 px +- [x] Kontrast ≥ 4,5:1, Fokusrahmen in Messing +- [x] Kein horizontales Scrollen bei 390 px +- [x] Klickflächen ≥ 44 px ## Abhängigkeiten #001 ## Notizen / Entscheidungen +- **Tokens:** CSS-Variablen in `:root`, an Tailwind per `@theme inline` gereicht (`bg-surface`, + `text-muted`, `border-border-strong`, `rounded-karte` …). So nutzen Inline-SVGs und Utilities + dieselbe Quelle. Nur dunkler Modus. +- **Schriften:** nur Latin-Ausschnitt und benutzte Gewichte, importiert in `src/app/layout.tsx`. + Zeichen außerhalb (z. B. „→“) fallen auf die Systemschrift zurück – unauffällig. +- **Kontrast** (gemessen): `--text-muted` auf `--surface` 5,8:1, auf `--raised` 5,3:1; + Messing auf `--accent-soft` 7,2:1; Koralle auf `--danger-soft` 5,8:1; Text auf Messing-Button 10,3:1. +- **Icons:** `pnpm icons` (`scripts/icons.mjs`) erzeugt `public/icons/*`, `src/app/favicon.ico` + (16/32/48, PNG-in-ICO), `src/app/icon.svg` und `src/app/apple-icon.png`; Ergebnisse sind + eingecheckt, damit Build und Laufzeitabbild kein sharp brauchen. App-Icons auf `#0D1013` mit + Rand (Logo 72 %), deshalb ohne `maskable`. +- **Aktiver Pfad:** Ein Layout kennt den Pfad nicht. `Huelle` nimmt `aktiverPfad` optional; + ohne Angabe ermittelt die Client-`Navigation` ihn per `usePathname()`. +- **Benutzer:** `src/app/(app)/huellen-benutzer.ts` → `huellenBenutzerErmitteln()` liefert bis + #004 einen festen Beispielbenutzer (TODO-Kommentar). +- **Mobil:** Unter 1024 px Kopfleiste mit Menü-Button, Leiste als Overlay (280 px) mit eigenem + Schließen-Knopf; Escape, Klick auf Abdunkler oder auf einen Link schließt. Geschlossen ist die + Leiste `invisible` und damit nicht per Tab erreichbar. +- **Desktop:** Seitenleiste als eigene Flex-Spalte, Inhalt darin `sticky` – Hintergrund und Rahmen + laufen auch bei langen Seiten (und in Full-Page-Screenshots) durch. +- **Tabellen** scrollen im eigenen Container; der Container ist `relative`, sonst entkommen + `sr-only`-Texte dem Scrollbereich und verbreitern die Seite (bei 390 px gefunden). +- **Prototyp:** Beispieldaten in `src/beispiel/` (Bezugszeit `BEISPIEL_JETZT`), Seiten sichtbar + als „Beispieldaten“ markiert. Platzhalterseiten nennen Phase und Issue. +- **Screenshots:** `pnpm exec tsx scripts/screenshots.ts` (ENV `SSHKV_URL`, `SSHKV_BILDER`, + `SSHKV_SEITEN`) → `docs/anleitung/bilder/`; meldet horizontalen Überlauf mit Exit-Code 1. diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts new file mode 100644 index 0000000..f9808b4 --- /dev/null +++ b/scripts/screenshots.ts @@ -0,0 +1,82 @@ +/** + * Screenshots der Oberfläche für Anleitung und Design-Abnahme (#002). + * + * pnpm build && pnpm start # erstes Terminal + * pnpm exec tsx scripts/screenshots.ts # zweites Terminal + * + * Umgebung: + * SSHKV_URL Basis-URL (Standard http://localhost:3400) + * SSHKV_BILDER Zielordner (Standard docs/anleitung/bilder) + * SSHKV_SEITEN Kommagetrennte Auswahl der Seitennamen (Standard: alle) + * + * Prüft nebenbei, dass keine Seite horizontal scrollt (Design-Vorgabe ab 390 px), und + * endet dann mit Fehlercode – so taugt das Skript auch als schneller Layout-Test. + */ +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { chromium } from "@playwright/test"; + +const BASIS = (process.env["SSHKV_URL"] ?? "http://localhost:3400").replace(/\/$/, ""); +const ZIEL = process.env["SSHKV_BILDER"] ?? join(process.cwd(), "docs/anleitung/bilder"); + +const SEITEN = [ + { name: "uebersicht", pfad: "/" }, + { name: "schluessel-detail", pfad: "/schluessel/beispiel" }, +]; + +const ANSICHTEN = [ + { name: "desktop", width: 1440, height: 900 }, + { name: "mobil", width: 390, height: 844 }, +]; + +async function main() { + const auswahl = process.env["SSHKV_SEITEN"]?.split(",").map((s) => s.trim()); + const seiten = auswahl ? SEITEN.filter((s) => auswahl.includes(s.name)) : SEITEN; + await mkdir(ZIEL, { recursive: true }); + + const browser = await chromium.launch(); + const fehler: string[] = []; + try { + for (const ansicht of ANSICHTEN) { + const kontext = await browser.newContext({ + viewport: { width: ansicht.width, height: ansicht.height }, + deviceScaleFactor: 1, + locale: "de-DE", + timezoneId: "Europe/Berlin", + colorScheme: "dark", + }); + const seite = await kontext.newPage(); + for (const s of seiten) { + const antwort = await seite.goto(`${BASIS}${s.pfad}`, { waitUntil: "networkidle" }); + if (!antwort?.ok()) { + fehler.push(`${s.pfad}: HTTP ${antwort?.status() ?? "keine Antwort"}`); + continue; + } + // Erst nach dem Laden der selbst gehosteten Schriften fotografieren, sonst + // stehen Fallback-Schriften im Bild. + await seite.evaluate(() => document.fonts.ready); + + const ueberlauf = await seite.evaluate( + () => document.documentElement.scrollWidth - document.documentElement.clientWidth, + ); + if (ueberlauf > 0) { + fehler.push(`${s.pfad} @ ${ansicht.width}px: ${ueberlauf}px horizontaler Überlauf`); + } + + const datei = join(ZIEL, `${s.name}-${ansicht.name}.png`); + await seite.screenshot({ path: datei, fullPage: true }); + console.log(`✓ ${datei}`); + } + await kontext.close(); + } + } finally { + await browser.close(); + } + + if (fehler.length > 0) { + console.error(`\n${fehler.length} Problem(e):\n- ${fehler.join("\n- ")}`); + process.exitCode = 1; + } +} + +await main();