docs(anleitung): Screenshot-Skript und Bilder des Prototyps (#002)

scripts/screenshots.ts nimmt Uebersicht und Schluesseldetail bei
1440x900 und 390x844 auf und meldet nebenbei horizontalen Ueberlauf,
damit es fuer die Anleitung (#029) und als schneller Layout-Test taugt.
Issue #002 ist damit erledigt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 09:21:02 +02:00
co-authored by Claude Opus 5.5
parent 445f5c5ebd
commit 4af07e9553
6 changed files with 118 additions and 10 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

+36 -10
View File
@@ -1,25 +1,51 @@
# #002 Design-System und Layout # #002 Design-System und Layout
**Status:** offen **Status:** erledigt
**Meilenstein:** v0.1.0 **Meilenstein:** v0.1.0
**Labels:** ui **Labels:** ui
**Geschätzt:** 4 h **Tatsächlich:** – **Geschätzt:** 4 h **Tatsächlich:** 0,3 h (09:07–09:22)
## Ziel ## Ziel
Design laut Vorgabe (Tresor, Graphit + Messing): Tokens, Schriften, Logo, Favicon/PWA-Icons, Grundlayout mit Seitenleiste. Design laut Vorgabe (Tresor, Graphit + Messing): Tokens, Schriften, Logo, Favicon/PWA-Icons, Grundlayout mit Seitenleiste.
## Aufgaben ## Aufgaben
- [ ] Design-Tokens als CSS-Variablen + Tailwind-Theme - [x] Design-Tokens als CSS-Variablen + Tailwind-Theme
- [ ] Space Grotesk, IBM Plex Sans, JetBrains Mono selbst gehostet (`@fontsource`) - [x] 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 - [x] `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 - [x] 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] Grundkomponenten: Karte, Button (primär/sekundär/Gefahr), Badge, Statuspunkt, Tabelle, Diff-Box, Kennzahl-Kachel
## Akzeptanzkriterien ## Akzeptanzkriterien
- [ ] Kontrast ≥ 4,5:1, Fokusrahmen in Messing - [x] Kontrast ≥ 4,5:1, Fokusrahmen in Messing
- [ ] Kein horizontales Scrollen bei 390 px - [x] Kein horizontales Scrollen bei 390 px
- [ ] Klickflächen ≥ 44 px - [x] Klickflächen ≥ 44 px
## Abhängigkeiten ## Abhängigkeiten
#001 #001
## Notizen / Entscheidungen ## 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.
+82
View File
@@ -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();