Shuffle/docs/design-system.md
cschlaefke 80b548819b docs(design): list components and catalog search behavior
Refs #3

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 01:18:23 +01:00

100 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design-System (Theme-Basis, F6 / Issue #31)
Stand: Basis + Formular-Komponenten (U1). Weitere Komponenten (Spiel-Kachel, Listen, Leer-/Ladezustände) folgen.
Code: `app/lib/core/theme/` (`app_theme.dart`, `app_spacing.dart`), `app/lib/core/widgets/`; Tests: `app/test/core/theme/`, `app/test/core/widgets/`.
## Theme
- Material 3, `AppTheme.light()` und `AppTheme.dark()`; `ShuffleApp` nutzt `ThemeMode.system`.
- Farben: `ColorScheme.fromSeed` mit Seed `#4F46E5` (Indigo). Einzige Abweichung: `outline` ist
etwas dunkler (hell `#6B6A7A`) bzw. heller (dunkel `#9A99AB`), damit Feldrahmen mindestens 3:1 erreichen.
- Keine Farbwerte in Widgets: immer `Theme.of(context).colorScheme` und `textTheme` verwenden.
## Kontraste (WCAG 2.1 AA)
Geprüft im Test `app_theme_test.dart` für Hell und Dunkel. Text mindestens 4.5:1, Bedienelemente
(Rahmen, Fokusring, Fehlerrahmen) mindestens 3:1.
| Paar | Hell | Dunkel |
|---|---|---|
| onSurface / surface | 16.3 | 14.4 |
| onSurfaceVariant / surface | 8.9 | 10.9 |
| onSurfaceVariant / Feldfüllung | 7.2 | 7.3 |
| onPrimary / primary (Buttons) | 6.5 | 7.7 |
| Fehlertext (error) / Feldfüllung | 5.0 | 7.3 |
| SnackBar-Text / SnackBar-Fläche | 11.5 | 10.1 |
| SnackBar-Aktion / SnackBar-Fläche | 7.7 | 5.0 |
| Feldrahmen (outline) / Feldfüllung | 4.1 | 4.4 |
| Fokusring (primary) / Feldfüllung | 5.0 | 7.2 |
Regeln: Informationen nie nur über Farbe vermitteln (Fehler = Text + Rahmen + Icon). Neue Farbpaare
müssen in den Test aufgenommen werden. Deaktivierte Elemente sind von der Kontrastpflicht ausgenommen.
## Touch-Ziele und Fokus
- Mindestgröße 48 x 48 dp für alle Buttons und Icon-Buttons (`minimumSize`), `MaterialTapTargetSize.padded`,
`VisualDensity.standard`. Nicht auf `compact` verkleinern.
- Tastaturfokus (Web/Desktop): Textfelder, Outlined-/Elevated-/Text-Buttons zeigen einen 3 dp breiten Rahmen in `primary`; der FilledButton (Füllung `primary`) nutzt stattdessen `onSurface`, weil `primary` auf der Füllung unsichtbar wäre (Kontrast zu Fläche und Füllung >= 3:1, im Test geprüft).
- Icons ohne Text brauchen `tooltip` bzw. `Semantics(label: ...)`; Bilder ein Label oder `excludeSemantics`.
- Text muss bis 200 % Skalierung ohne Abschneiden funktionieren (kein fixes Höhen-Layout für Text).
## Typografie
Material-2021-Skala (`textTheme`), Farbe `onSurface`. Überschriften `headline*`/`title*`, Fließtext `bodyLarge/Medium`,
Hinweise `bodySmall` (`onSurfaceVariant`). Schriftgrößen nie hart codieren.
## Abstände, Radien, Breakpoints (`app_spacing.dart`)
| Token | Wert (dp) |
|---|---|
| `AppSpacing` xxs / xs / sm / md / lg / xl / xxl / xxxl | 2 / 4 / 8 / 12 / 16 / 24 / 32 / 48 |
| `AppRadius` sm / md / lg / xl | 8 / 12 / 16 / 28 |
| Breakpoints (`AppWidthClass`) | kompakt < 600, mittel 600-1023, groß >= 1024 |
| `AppBreakpoints.maxContentWidth` | 560 (allgemeine Formulare/Text auf breiten Bildschirmen begrenzen) |
| `AppBreakpoints.authCardMaxWidth` | 480 (zentrierte Auth-Karte ab 600 dp, Spec #1 § 6) |
`AppSpacing.pagePadding(width)` liefert den Seitenrand je Breitenklasse (16 / 24 / 32). Web darf nicht
wie eine gestreckte Handy-App wirken: Inhalt zentrieren und auf `maxContentWidth` begrenzen.
## Komponenten-Themes in der Basis
FilledButton, ElevatedButton, OutlinedButton, TextButton, IconButton, Checkbox, InputDecoration
(gefüllt, Rahmen 1 dp, Fokus 3 dp, Fehler 2 dp), Card, AppBar, Divider, SnackBar (schwebend, mit Schließen-Icon).
## Formular-Komponenten (`app/lib/core/widgets/`, U1)
Gemeinsame Regeln: Alle Texte (Labels, Fehler, Semantics-Labels) kommen als Parameter von außen (l10n), nie
hart codiert. Fehler = Icon + Text + Fehlerrahmen, nie nur Farbe. Fehlertexte sind Live-Regionen (werden von
Screenreadern angesagt). Layout bricht bei 200 % Textskalierung um statt abzuschneiden. Alle Komponenten sind
in Hell und Dunkel gegen `androidTapTargetGuideline`, `labeledTapTargetGuideline` und `textContrastGuideline`
getestet; Tests liegen in `app/test/core/widgets/` und dienen als lauffähige Vorschau.
| Komponente | Zweck und API (Kurzform) |
|---|---|
| `AppTextField` | `label` (sichtbar über dem Feld, umbrechend, auch Semantics-Label), `helperText`, `errorText` (ersetzt Hilfetext), `controller`, `focusNode`, `keyboardType`, `textInputAction`, `autofillHints`, `maxLength` + `showCounter` (weiche Grenze: Eingabe wird nicht abgeschnitten, damit der Fehler am Feld erscheint), `counterText` (eigener Zählertext, wenn die Zähleinheit vom Standard abweicht, z. B. getrimmte Codepoints beim Anzeigenamen), `inputFormatters`, `enabled`, `onChanged`, `onSubmitted`. |
| `AppPasswordField` | Wie `AppTextField`, dazu `showPasswordLabel` / `hidePasswordLabel` (Pflicht; Tooltip + Semantics des 48-dp-Schalters), `ruleHint` (Regelhinweis, solange kein Fehler), `isNewPassword` (Autofill `newPassword` statt `password`), `autofillHints` zum Überschreiben. Startet verdeckt, Autokorrektur und Vorschläge aus. |
| `AppCheckboxField` | `AppCheckboxField(value, onChanged, label)` bzw. `.rich(segments: [AppLabelSegment.text(..), AppLabelSegment.link(text, onTap)])`, `errorText`. Ganze Zeile (Checkbox + Label) ist Touch-Ziel >= 48 dp. Links sind per Tastatur fokussierbar und für Screenreader als benutzerdefinierte Aktionen verfügbar; Semantics-Knoten meldet Zustand und den vollen Labeltext. |
| `AppPrimaryButton` | `label`, `onPressed`, `isLoading` (deaktiviert + Spinner, kein Doppelsenden), `loadingLabel` (gesprochener Text im Ladezustand), `icon`, `expand` (Standard: volle Breite). |
| `AppFormScaffold` | `child`, `appBar`, `maxWidth` (Standard `AppBreakpoints.authCardMaxWidth`). Kompakt: Vollbild, scrollbar; ab 600 dp zentrierte Karte. Tastatur schiebt den Inhalt hoch (Scaffold-Resize), Ziehen schließt die Tastatur. |
| `AppBanner` | `type` (`AppBannerType.info` / `error`), `message`, `typeLabel` (gesprochenes Präfix, z. B. „Fehler“), optional `actionLabel` + `onAction` (48-dp-TextButton), `actionEnabled` (Standard `true`; `false` lässt die Aktion sichtbar, aber deaktiviert, z. B. während eine Anfrage läuft; Semantics melden sie als nicht aktiviert, der Screenreader-Fokus geht nicht verloren). Live-Region mit „typeLabel: message“. |
| `AppFieldError` | Interner Baustein: Fehlerzeile mit Icon als Live-Region, von Textfeld und Checkbox genutzt. |
## Listen-Komponenten (`app/lib/core/widgets/`, #3 KAT-01)
Für jede Liste in R1 (Zustände Krit. 1–3): Leer- und Fehlerzustand sowie Listenfuß zum Nachladen. Gleiche Regeln wie bei den
Formular-Komponenten (Texte von außen, Tests gegen die Guidelines in Hell und Dunkel, Umbruch statt Abschneiden bei 200 %).
| Komponente | Zweck und API (Kurzform) |
|---|---|
| `AppStateMessage` | Leer-/Fehlerzustand: `icon` (dekorativ, für Screenreader ausgeblendet), `title`, optional `message`, `actions` (`AppStateAction(label, onPressed, key)`; die erste Aktion ist tonal hervorgehoben, weitere umrandet; alle 48 dp). Titel und Hinweis sind eine Live-Region. |
| `AppListFooter` | Fuß einer seitenweise geladenen Liste: `isLoading` (beschrifteter Fortschrittskreis), `hasMore` (Schaltfläche „Weitere laden“, per Tastatur und Screenreader erreichbar, optional `loadMoreFocusNode`), `errorMessage` + Wiederholen (bereits geladene Einträge bleiben). Vorrang: Fehler vor Laden vor „Weitere laden“. Automatisches Nachladen ist Sache des Aufrufers; die Schaltfläche bleibt der Weg für Tastatur und Screenreader. |
**Katalogsuche (`features/catalog/presentation/search/`)** – Aufbau und Verhalten, Entscheidungen der Umsetzung (Spec #3 § 5.6):
- Kompakt (< 600 dp) und mittel (600–1023 dp): Suchfeld, darunter die Schaltfläche „Filter (n)“ mit Bottom Sheet (Änderungen wirken sofort, „Fertig“ schließt); ab 1024 dp feste Seitenleiste (320 dp) links, Trefferliste rechts mit höchstens 720 dp Breite.
- Einfachauswahl (Spielerzahl, Dauer, Alter) als beschriftete Auswahlfelder mit „Beliebig“; Genre und Mechanik als Filter-Chips, nach lokalisiertem Namen sortiert. Fehler beim Laden der Begriffe betrifft nur diese beiden Bereiche („Erneut versuchen“).
- Trefferkachel: `GameCover` klein (Platzhalter), Titel (2 Zeilen), „auch: …“, Jahr · Verlage (zwei, sonst „+n“), Kurzangaben; ein zusammengefasstes Semantics-Label. Ohne `onTap` ist die Kachel kein Button, aber per Code fokussierbar (Fokusring 3 dp, kein Tab-Stopp).
- Trefferzahl über der Liste; Ansage der Trefferzahl 600 ms nach Abschluss der letzten Suche (`SemanticsService.sendAnnouncement`), nie während des Tippens.
- Nach „Weitere laden“ (bzw. „Erneut versuchen“ im Listenfuß) springt der Fokus auf die erste neu geladene Kachel. Automatisches Nachladen, wenn weniger als etwa fünf Kacheln unterhalb des sichtbaren Bereichs liegen; nach einem Fehler nur noch über die Schaltfläche (keine Wiederholungsschleife).
Auth-Screens: Layout-Skizzen und Textvorschläge siehe `docs/design/auth-screens.md`.