8.7 KiB
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()undAppTheme.dark();ShuffleAppnutztThemeMode.system. - Farben:
ColorScheme.fromSeedmit Seed#4F46E5(Indigo). Einzige Abweichung:outlineist etwas dunkler (hell#6B6A7A) bzw. heller (dunkel#9A99AB), damit Feldrahmen mindestens 3:1 erreichen. - Keine Farbwerte in Widgets: immer
Theme.of(context).colorSchemeundtextThemeverwenden.
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 aufcompactverkleinern. - Tastaturfokus (Web/Desktop): Textfelder, Outlined-/Elevated-/Text-Buttons zeigen einen 3 dp breiten Rahmen in
primary; der FilledButton (Füllungprimary) nutzt stattdessenonSurface, weilprimaryauf der Füllung unsichtbar wäre (Kontrast zu Fläche und Füllung >= 3:1, im Test geprüft). - Icons ohne Text brauchen
tooltipbzw.Semantics(label: ...); Bilder ein Label oderexcludeSemantics. - 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:
GameCoverklein (Platzhalter), Titel (2 Zeilen), „auch: …“, Jahr · Verlage (zwei, sonst „+n“), Kurzangaben; ein zusammengefasstes Semantics-Label. OhneonTapist 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.