Shuffle/docs/design-system.md
cschlaefke 25212929ce
All checks were successful
CI / flutter (pull_request) Successful in 1m46s
CI / web-build (pull_request) Successful in 59s
CI / supabase (pull_request) Successful in 12s
CI / supabase-tests (pull_request) Successful in 31s
CI / e2e (pull_request) Successful in 2m54s
feat(catalog): review fixes for catalog search
Word limit and no-retry state for rejected input, nested /catalog route for
system back, logger filter for supabase libs, more mutation-proof tests,
doc fix, umlaut-aware term sorting, thousands separators. Refs #3

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

8.9 KiB
Raw Blame History

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), 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 ein Tab-Stopp mit Fokusring (3 dp): Tastaturnutzer durchlaufen so die Liste, was sie scrollt und das Nachladen auslöst (die Liste wird lazy gebaut). Im Screenreader-Durchgang bewerten, ob die vielen Tab-Stopps stören.
  • 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.