Design System — Komponentenspezifikation

Kanonische, plattformunabhängige Spezifikation aller Komponenten — wächst über die Iterationen hinweg (nichts wird gelöscht, nur ergänzt). Verbindlich für die plattformübergreifende Implementierung (Android · iOS · Web). Der jeweilige Pro-Iteration-Lieferumfang steht in iteration-1.html (Home + Burger-Menü) und iteration-2.html (Lesepläne-Komponenten). Die Referenz-Implementierung für Android befindet sich in packages/android.

Zielgruppe
Engineers (plattformübergreifend), Designer zur Verifikation des Kontrakts, QA, agentische Implementierungs-Tools.
Status
v1 — übergabebereit · 16 Komponenten · 21 Abschnitte · 250 Token-Pfade katalogisiert.
Begleitdokumente
index.html (Engineering-Referenz, generierte Tabellen) · handover.json (maschinenlesbare Zusammenfassung)
Konventionen
dp / sp · ms · cubic-bezier · DTCG-Token-Pfade.

Aufbau dieses Dokuments

Jede Komponente unten ist nach einem einheitlichen Kontrakt dokumentiert:

  1. Überblick — was die Komponente ist und warum sie im Erlebnis existiert.
  2. Visuelle Spezifikation — Dimensionen, Aufbau (Layer-Stack), Tokens, die ihr Erscheinungsbild bestimmen.
  3. Zustände — jeder visuelle Zustand mit Auslöser und Austrittsbedingung.
  4. Verhalten — Zustandsautomat, Lebenszyklus, Seiteneffekte, Datenabhängigkeiten.
  5. Bewegung — Dauern, Easings, Choreografie. Immer in Millisekunden + cubic-bezier angegeben.
  6. Interaktion & UX — Eingabebehandlung (Drücken, Ziehen, Wischen, Tastatur, Fokus), Gesten, Scrollen.
  7. Barrierefreiheit — semantische Rolle, Labels, Fokusreihenfolge, Mindest-Trefferflächen, Verhalten bei reduzierter Bewegung.
  8. Verwendete Tokens — jeder DTCG-Token-Pfad, den die Komponente liest, nach Ebene.
  9. Implementierungsreferenz — Android-spezifische Hinweise; iOS und Web ergänzen hier eigene Hinweise, sobald sie eingeführt werden.

Konventionen:

  • Größen werden in dichteunabhängigen Pixeln angegeben (dp = Android / iOS-Punkt @1x / CSS px / Flutter logical pixel — sie sind für das Layout austauschbar).
  • Schriftgrößen werden in skalierbaren Pixeln angegeben (sp auf Android, äquivalent zu Dynamic Type auf iOS, rem im Web). Beide Plattformen müssen die Schriftgrößen-Einstellung des Nutzers respektieren.
  • Farben und Deckkraftwerte sind Tokens, niemals Hex-Literale. Token-Pfade folgen der vierstufigen Hierarchie primitives → semantic → component → views.
  • Zeit wird in Millisekunden angegeben. Easings werden benannt oder als cubic-bezier(x1, y1, x2, y2) angegeben.
  • "Container" bezeichnet eine generische Layout-Box (Android Box / iOS UIView / Web div). "Stack" bezeichnet ein vertikales oder horizontales Layout (Column/Row / VStack/HStack / flex).

Token-System

Tokens werden in tokens/{primitive,semantic,component,view}/*.json im W3C-DTCG-Format definiert. Die einzige Quelle der Wahrheit ist die Design-Tokens-API unter /api/v1/tokens; die Plattformen laden den konsolidierten Payload zur Laufzeit und legen ihn über ihre gebündelten Standardwerte.

EbeneZweckBeispiel
PrimitiveRohwerte: Farben, Abstände, Schriftstärken, Deckkraft-Stufen.primitives.opacity.scale80 = 0.8
SemanticTheme-bewusste Bedeutung: dunkler/heller Text, Rahmen, Oberflächen.semantic.dark.text.primary = #FFFFFF
ComponentKomposition pro Komponente.component.heroVideo.tabletTitleFontSize = 26
ViewKomposition pro Bildschirm (bindet Primitives + Semantic an Oberflächen eines spezifischen Bildschirms).views.home.modalScrim.alpha = 0.8

Die Implementierung muss Tokens aus der API lesen; jeder für den Nutzer sichtbare Wert, der kein Token ist, gilt als Bug.

Token-Pfad-Konvention. In diesem Dokument folgen Pfade innerhalb von <code>-Tags der API-Benennungskonvention. Von den 250 genannten eindeutigen Pfaden werden 157 (63%) heute vollständig über die API aufgelöst; 70 (28%) existieren als Kotlin-Modell-Defaults und warten auf API-Promotion; 23 (9%) sind reine Spezifikations-Vorschläge, die während der plattformübergreifenden Implementierung formalisiert werden. Siehe Abschnitt Token-Referenz am Ende für die vollständige Statustabelle — vor der Implementierung lesen.

Plattform-Hinweise

Android. Referenz-Implementierung. Verfügbar in packages/android. Verwendet Jetpack Compose mit Material 3. Die Referenz ist verbindlich für das Verhalten — wenn diese Spezifikation und die Android-Quelle abweichen, gilt die Spezifikation, aber die Android-Quelle ist die nächstgelegene existierende Antwort.
iOS. Implementierung ausstehend. Ziel: SwiftUI. Material-3-äquivalente Semantik über die SwiftUI-Material-Bibliothek oder ein dünnes Shim adoptieren; keine Material-spezifische Ikonografie importieren.
Web. Implementierung ausstehend. Ziel: React + ein token-getriebenes CSS-Variablen-System. Das HTML-Referenzdokument index.html zeigt eine Möglichkeit, component-tokens.css zu konsumieren.

Glossar

In diesem Dokument verwendete Begriffe — plattformunabhängig definiert.

BegriffBedeutung
dpDichteunabhängiges Pixel. Ein dp = ein logisches Pixel auf einem "Baseline"-Bildschirm mit 160 dpi. Äquivalent zu: Android dp, iOS-Punkt @ 1×, CSS px, Flutter logical pixel.
spSkalierbares Pixel. Wie dp, skaliert aber mit der vom Nutzer bevorzugten Schriftgröße. Für jede Schriftgröße verwenden. iOS Dynamic Type und CSS rem erfüllen dieselbe Rolle.
ContainerEine generische Layout-Box, die weitere Elemente enthält. Entspricht Android Box, iOS UIView / SwiftUI VStack-Wrapper, Web div.
StackEine lineare Anordnung von Kindelementen, entweder vertikal oder horizontal. Entspricht Compose Column/Row, SwiftUI VStack/HStack, CSS flex-direction: column/row.
Press-FeedbackDie transiente visuelle Bestätigung, wenn ein Nutzer ein interaktives Element antippt. Android = Ripple, iOS = Highlight/Scale, Web = Active-State.
ScrimEine halbtransparente Überlagerung hinter einer modalen Oberfläche, die den darunterliegenden Inhalt abdunkelt und visuell trennt. Immer antippbar zum Schließen.
SlotEin benannter Bereich innerhalb einer Eltern-Komponente, in den ein Kind eingefügt wird. Das Eltern-Element gibt Geometrie und Styling des Slots vor; das Kind füllt ihn. Äquivalent zu Compose-Composable-Content-Lambdas, SwiftUI @ViewBuilder-Parametern, Web-Slots / React-Children.
ChromeWiederverwendbare visuelle Umrandung um Inhalt (Rahmen, Eckenradius, Schatten, Badge-Slot) — die Komponente dekoriert, ohne den Inhalt vorzugeben.
SettleDer Endzustand nach einem Wisch- oder Fling-Gesten, wenn der scrollbare Bereich an einer stabilen Seitenposition zur Ruhe kommt.
TrefferflächeDer transparente antippbare Bereich um ein visuelles Icon oder einen Button. Muss gemäß Barrierefreiheits-Baseline ≥ 48dp × 48dp sein, unabhängig von der sichtbaren Größe.
BP / BreakpointViewport-Größenschwelle, die den Layout-Modus umschaltet. Iteration 1 hat zwei aktive: mobile (kürzeste Seite < 600dp) und tablet (≥ 600dp). Siehe Breakpoints.
DTCGW3C Design Tokens Community Group-Format. Die JSON-Struktur, in der Tokens unter tokens/{primitive,semantic,component,view}/*.json verfasst werden. Spezifikation: tr.designtokens.org/format.
TokenEin benannter Designwert (Farbe, Dimension, Deckkraft, Dauer, …), der in DTCG verfasst, von der Design-Tokens-API ausgeliefert und zur Laufzeit von der Plattform konsumiert wird. Die vier Ebenen (Primitive · Semantic · Component · View) komponieren in Richtung spezifischerer Bedeutungen.
Vorab-PaletteLaufzeitanalyse eines Bildes, um eine dominante "Basis"-Farbe und eine "Akzent"-Farbe abzuleiten, die zum Einfärben der Hero-Gradients und des umgebenden Bildschirmhintergrunds verwendet werden. Die Android-Referenz verwendet den palette=json-Endpoint von Imgix oder als Fallback die androidx.palette-Bibliothek.
SkeletonEin Platzhalter-Visual, das angezeigt wird, während der echte Inhalt lädt. Gleiche Form wie die finale Karte / Reihe, gefüllt mit einer kontrastarmen Farbe, die dezent pulsieren darf.
Composition PayloadServer-getriebenes JSON, das die Struktur eines Bildschirms beschreibt: welche Abschnitte vorhanden sind, deren Reihenfolge und welcher Inhalt jeden füllt. Der Home-Bildschirm liest dies, um zu bestimmen, welche RowSections in welcher Reihenfolge gerendert werden.

Theming — Kontrakt für hellen & dunklen Modus

Die App unterstützt zwei Farbschemata. Jede Komponente muss in beiden korrekt funktionieren. Die meisten Oberflächen wechseln automatisch über semantische Tokens — Designer und Implementierer sollten KEIN modus-spezifisches CSS direkt im Komponenten-Code schreiben.

Wie die Modi verdrahtet sind

  • System-Ebene: Der aktive Modus wird durch die System-Theme-Präferenz des Nutzers bestimmt (Android System-Theme / iOS UITraitCollection / Web prefers-color-scheme).
  • Semantic-Ebene: Der Großteil der Token-Auflösung liegt in semantic.{dark,light}.* — jede sichtbare Farbe stammt von dort. Komponenten lesen semantic.{mode}.text.primary, background.canvas usw., und die Laufzeit wählt den korrekten Sub-Tree basierend auf dem aktiven Modus.
  • Component-/View-Ebene: Manche Komponenten haben strukturelle Unterschiede zwischen den Modi (nicht nur Farbwechsel) — z.B. erscheint der Schlagschatten der Hero-Karte nur im dunklen Modus. Diese strukturellen Regeln sind im jeweiligen Komponenten-Abschnitt unten explizit dokumentiert.

Strukturelle Unterschiede (Zusammenfassung pro Komponente)

KomponenteHeller ModusDunkler Modus
TopBarStatusleiste = dunkle Icons auf hell. Hintergrund der Leiste blendet zu semantic.light.background.surface (Pfirsich).Statusleiste = helle Icons auf dunkel. Hintergrund der Leiste blendet zu semantic.dark.background.canvas (Tiefseegrün).
HeroCarouselKein Schlagschatten am Hero-Kartenkörper. Rahmenfarbe aus Light-Mode-Semantic.Schlagschatten bei heroCarousel.elevation = 16dp. Rahmenfarbe aus Dark-Mode-Semantic.
HeroVideoCardUnterer Gradient = 7 Stufen (hellere Rampe). Light-Mode-Textfarben.Unterer Gradient = 8 Stufen (dichtere Rampe für Textlesbarkeit auf Foto). Dark-Mode-Textfarben.
ButtonGroupSectionPrimärer Button = vollflächige Oberfläche, KI-Variante verwendet Light-Mode-Akzent-Gradient.Gleiche Komponente, Dark-Mode-Akzent-Gradient.
RowSectionTiteltext aus semantic.light.text.primary.Titeltext aus semantic.dark.text.primary.
CardPosterRahmen- + Schattenwerte aus Light-Mode-Tokens. Skeleton-Füllung verwendet helle bgColor.Dasselbe mit Dark-Mode-Tokens.
MenuDrawerPanel-Hintergrund semantic.light.background.canvas × α 0.85 (Mobile) bzw. × α 0.92 (Tablet-Panel) + Backdrop-Blur (24dp, Android 12+). Zeilen-Hintergrund Color.Transparent — Zeilen erben vom Panel.Panel semantic.dark.background.canvas × α 0.85 / 0.92 + Backdrop-Blur. Zeilen-Hintergrund Color.Transparent. Press-Feedback ist die einzige zeilenspezifische visuelle Antwort.
Home-BildschirmCanvas = semantic.light.background.surface (Pfirsich). Gradient mischt Hero-Farbe über Pfirsich.Canvas = semantic.dark.background.canvas (Tiefseegrün). Gradient mischt Hero-Farbe über Tiefseegrün.

Jede Zeile gegen den Abschnitt der jeweiligen Komponente unten verifizieren — jeder strukturelle Unterschied wird auch im Audit-Notizen-Block der Komponente erwähnt.

Was (noch) NICHT im Umfang ist

Disziplin beim Umfang ist wichtig. Bisher ausgeliefert: Iteration 1 — Home-Bildschirm + Burger-Menü (iteration-1.html); Iteration 2 — die Lesepläne-Komponenten (HeroGenericCard, HeroPosterCard, CardLandscape, CardAvatar + voller ButtonGroupSection-Umfang; iteration-2.html) sowie der Design-Sync vom 2026-06-10 (festes Scrim-Band der Hero-Karten, Seitenhintergrund vereinheitlicht auf bg.canvas mit nahtloser Gradient-Konvergenz, API-Felder appearance.backgroundColor*, Badge-Textfarbe per Luminanz, Split-Karten-Verankerung).

Implementiert + im Showcase, Doku-Abschnitte ausstehend: CardOverlay und CardSplit sind in der App (Lesepläne-Showcase) und in Figma (Seiten i2 · Card · Overlay/Split) ausgeliefert, haben aber noch keine eigenen Komponenten-Abschnitte in diesem Dokument — geplant für Iteration 3. Inzwischen geliefert und daher aus der Liste entfernt: Pull-to-Refresh (CollectionScreen rendert PullToRefreshBox) · der Lesepläne-Tab als Komponenten-Showcase.

Die folgenden Bildschirme und Features bleiben auch nach Iteration 2 explizit außerhalb des Umfangs und sollen jetzt nicht gebaut oder spezifiziert werden, selbst wenn die Android-Quelle Hinweise darauf enthält. Eine künftige Iteration wird jeden Punkt adressieren.

  • Andere Bildschirme als Home + Burger-Menü: Bibellese-Ansicht, Live-TV, TV-Programm, Konto, Suchergebnisse, Detailansichten (Serie / Video / Playlist), Vollbild-Videoplayer. Die Bottom-Nav-Ziele für diese Tabs verweisen irgendwohin, aber die Zielbildschirme sind nicht im Umfang. (Der Lesepläne-Tab beherbergt derzeit den Iteration-2-Komponenten-Showcase — das eigentliche Lesepläne-Produkt bleibt außerhalb des Umfangs.)
  • Authentifizierungs-Flows: Der "Anmelden"-Button im Drawer ist sichtbar, aber für den Implementierer eine No-Op-Verdrahtung — die tatsächlichen Anmeldebildschirme sind eine separate Spezifikation.
  • Bildschirmübergreifende Routen-Transitions: Wenn der Nutzer eine Karte → Detail antippt, die Plattform-Standardanimation verwenden. Eine dedizierte Motion-Spezifikation folgt später (Shared-Axis / Fade / Push, je nach Plattform-Konvention).
  • Internationalisierung über Deutsch hinaus. UI-Text ist in v1 ausschließlich deutsch; künftige Spezifikationen führen strings.json-Keys für den Rest ein.
  • Theming über Hell / Dunkel hinaus. Kein Akzentfarben-Picker, keine benutzerdefinierten Themes, keine nutzerspezifische Palette. Das System unterstützt sie auf Token-API-Ebene (modus-bewusst), aber die UI legt sie nicht offen.
  • Offline-/Cache-Inhalte. Karten rendern Skeleton- und Fehlerzustände, aber es gibt keinen lokalen Storage-Fallback und kein "Für später gespeichert"-Feature in dieser Phase.
  • Analytics, Crash-Reporting, Telemetrie. Der Implementierer sollte das Standard-SDK der Plattform verdrahten, aber es sind keine spezifischen Events spezifiziert.
  • Live-Updates über EPG-Polling hinaus. HeroLiveStreamSlide pollt EPG; keine andere Komponente empfängt Push-Updates.
  • Benutzerdefiniertes Fokus-Management im Web über den Plattform-Standardring + die Modal-Falle für MenuDrawer hinaus. Die aktuellen Iterationen erfordern keine Tastenkürzel.

Changelog

Änderungen an Spezifikation, Token-API und Referenz-Implementierung — adressiert u. a. den BT-3900 Token-Drift-Report (04.06.2026). Einträge, die das Rendering von Iteration-1-Komponenten (Home) verändern, sind mit [Iteration 1] markiert.

2026-06-11

  • [Iteration 1] API: heroVideo.titleFontSize — Platzhalter behoben (Report-Punkt 1). Die API liefert jetzt die echte absteigende Skala 28/24/22/20/18sp (Schwellen <20/<30/<40/<50/≥50 Zeichen, Zeilenhöhe = Größe+2) statt flat 28. Der Token ist runtime-gemergt (⚙️ API) — Clients können den gebündelten Fallback weiter nutzen, beide Quellen stimmen jetzt überein.
  • Doku: Geist-Token mobileEyebrowFontSize entfernt (Report-Punkt 2). Die Eyebrow-Größe kommt — wie im Code — aus heroVideo.tabletDescFontSize (14sp, bewusst auch auf Mobile). Kein neuer API-Token nötig.
  • Doku an API angeglichen (Report-Punkt 3): paddingDefault-Tabelle jetzt 16dp (mobil) / 20dp (tablet) statt pauschal 24dp · surfaceRaised-Replica + Beschriftung auf #252F35 korrigiert (vorher „Neutral700 #1A2733", ein Hex außerhalb jeder Palette).
  • [Iteration 1] Proto: globales Text-Letterspacing entfernt (Report-Punkt 4). Die Material3-Typography der Referenz-App setzte (M3-Default) letterSpacing = 0.5sp auf bodyLarge/labelSmall — nicht durch die Spec gedeckt. Jetzt 0sp; der frühere ~8px-Versatz gegenüber der BTV-App entfällt.
  • ⚠️ cardSplit.titleFontSize/Weight — API-Korrektur 14→16 (betrifft Report-Zeile „RowSection-Poster-Fallback"): Der API-Wert 14/SemiBold war selbst Drift — die Referenz-Implementierung (Kotlin-Truth) rendert 16sp/SemiBold/1.2; API + Doku wurden am 10.06. darauf korrigiert (Drift-Gate verifiziert). Bitte den BTV-Fix „16/Medium → 14/SemiBold" auf 16/SemiBold nachziehen — zur Laufzeit liefert die API den Wert bereits.
  • [Iteration 1] Badges + Fortschrittsbalken auf allen Kartenvarianten: CardPoster und CardLandscape rendern jetzt das Status-/Kategorie-Badge (tagVariant/tag/contentType, oben links) nativ; CardTop10 reicht Badge + Progress an die innere Karte durch (manuelles Overlay entfernt).
  • Tablet-Hero: Scrim-Breite modusabhängigtabletGradientWidthFraction hell 0.6 / dunkel 0.5 (per-Mode in API + gemergt; vorher nur gebündelt). Begründung: dunkler Text auf hellem Scrim verliert nahe der Ausblendung schneller an Lesbarkeit.

2026-06-10

  • [Iteration 1] Hero-Scrim: festes Band statt proportionalem Gradient (HeroVideoCard + HeroGenericCard, mobil). Volle Deckkraft am Anker contentTopPadding(140) + scrimFadeShift(52) = 192dp (Baseline der ersten Headline-Zeile), Rampe scrimFadeHeight(240dp) nach oben, darunter Vollton — unabhängig von der Texthöhe. Neue ⚙️-API-Token scrimFadeShift/scrimFadeHeight; Stop-Kurven (band-relativ) in heroVideo.gradient.stops dokumentiert. Vorher skalierten die Stop-Anteile mit der Boxhöhe — bei viel Text saßen Eyebrow/Titel auf halbtransparentem Verlauf.
  • [Iteration 1] Seitenhintergrund vereinheitlicht auf bg.canvas (beide Modi; hell #F8F9FA, dunkel #080D16): Basis, TopBar-Fade-Ziel und Gradient-Konvergenzpunkt sind identisch; der Seiten-Gradient konvergiert exakt (6. Stop = Canvas, vorher 24 % Rest-Tint = sichtbare Naht). Per-Page-Override via appearance.backgroundColorDark/Light (neu in der Page-API); appearance.gradientColor* hat jetzt Vorrang vor der extrahierten Hero-Farbe.
  • [Iteration 1] Badge-Textfarbe per Luminanz (BtvBadge): weiß auf dunklen/transluzenten, #1A1A1A auf hellen Hintergründen (Schwelle 0.179). Neuer Token badge.textColorOnLight.
  • [Iteration 1] Buttons brechen nie um (maxLines = 1 + Ellipsis, alle ProtoBibleButton-Varianten); auf Tablet-Heroes darf der CTA über die Inhaltsspalte hinaus bis zur Kartenbreite wachsen.
  • Tablet-Portrait-Zeilenlimits reduziert: heroVideo.maxLines Portrait 3/4 → 2/3 (crowded 2/2); heroFeatured erhält eigene Portrait-Limits (2/3). ⚙️ API.
  • Split-Karte: Titel oben ohne Eyebrow — Info-Bereich behält die feste Höhe, Eyebrow+Titel oben verankert, Footer unten; ohne Eyebrow rückt der Titel nach oben statt eine Leerzeile zu reservieren.
  • Karten-Token an Kotlin-Truth angeglichen (Drift seit 092): imageOnly/overlay.cornerRadius 16→12 · overlay.titleLineHeight 1.07→1.2143 · split.titleFontSize 14→16 · split.titleLineHeight 1.07→1.2. Drift-Gate (d2-card) aktualisiert und grün.
  • Pull-to-Refresh auf Collection-Screens ausgeliefert (PullToRefreshBox) — aus „Nicht im Umfang" entfernt.
  • Token-Pipeline-Hinweis (Report-Punkt 5, teilweise): diverse 🔧-Kotlin-Pfade wurden in die ⚙️ API überführt (Scrim-Maße, Hero-Zeilenlimits inkl. heroVideo.maxLines.* + heroFeatured.tabletPortrait*, badge.textColorOnLight, Tablet-Layout-Fraktionen). Noch offen u. a.: Kern-Button-Farben, Hero-Schriftfamilien, MenuDrawer-Werte, broadcast.live/focusRing — Migrations-Backlog für Iteration 3.

Jeder Eintrag ist im Repo nachvollziehbar (Branch iteration-2, Commits vom 10.–11.06.2026); Token-Werte gegen die Live-API prüfbar (/api/v1/tokens). Figma (Datei JSmrFLCfLv8vTVAZVgEwVl, Seiten i2 ·) wurde synchron aktualisiert inkl. Spec-Karten je Komponente.

Implementierungs-Schnellstart

Eine komprimierte Checkliste für einen Entwickler (oder Agenten), der auf einer Nicht-Android-Plattform kalt startet. Jeder Schritt verweist auf den ausführlichen Abschnitt.

  1. Token-API-Client verdrahten. Beim App-Start /api/v1/tokens abrufen. Antwort cachen und beim App-Foreground erneut laden. Die vier Ebenen auf Plattformtypen abbilden (z.B. Swift-Structs / TypeScript-Interfaces). Fallback auf gebündelte Defaults, falls das Netzwerk nicht erreichbar ist — siehe Android-resolveMediathekTokens für das Merge-Muster.
  2. Zuerst die übergreifenden Kontrakte implementieren. Bewegungssprache (Dauernskala + 6 Easings), Barrierefreiheits-Baseline (48dp-Trefferflächen, Fokusringe, reduzierte Bewegung), Breakpoints (mobile / tablet auf Root-Ebene berechnet, per Kontext propagiert).
  3. Dann Leaf-Komponenten bottom-up bauen. Mit den kleinsten beginnen: CardPoster. Sein visueller Kontrakt ist in sich geschlossen, der einzige Seiteneffekt ist der Tap-to-Detail-Callback. (Andere Kartenvarianten wie CardLandscape / CardTop10 sind im Android-Quellcode für andere Views vorhanden, aber außerhalb des Iteration-1-Home-Umfangs.)
  4. Dann die Abschnitts-Container. RowSection (rendert eine horizontal scrollende Reihe von Karten) · ButtonGroupSection. RowSection steuert die Karten-Varianten-Dispatch aus Serverdaten.
  5. Dann der Hero-Stack. HeroCardShell-Chrome-Wrapper · die vier Slide-Varianten · HeroCarousel-Außenhülle (verwaltet Auto-Advance, Wischen, Farbauflösung aus der Composition-API) · PageIndicator.
  6. Dann die bildschirmebene Oberflächen. TopBar · BottomNavigation · Home-Bildschirm-Orchestrator, der alles komponiert.
  7. Schließlich das Overlay. MenuDrawer — das einzige Modal im Umfang von Iteration 1. Fokus-Falle und reduzierte Bewegung korrekt umsetzen.
  8. Validieren. Den Barrierefreiheits-Audit der Plattform ausführen (axe im Web, Accessibility Inspector auf iOS, Espresso Accessibility auf Android). Eigene Renderings gegen die in jedem Komponenten-Abschnitt eingebetteten Android-Referenzscreenshots vergleichen.
Agenten- / autonomes Tooling. Eine maschinenlesbare Zusammenfassung dieses Kontrakts wird unter handover.json veröffentlicht — Komponenten, Klassifikation, Bewegungsskala, Breakpoints, Barrierefreiheits-Baseline, Token-Abdeckung, Governance-Gates. Damit lassen sich automatisierte Implementierungen oder Compliance-Prüfungen bootstrappen. Abschnitts-Anker zur Nachvollziehbarkeit zitieren (z.B. handover.html#hero-carousel).

Komponentenmatrix

Komponenten über die bisherigen Iterationen hinweg. Die Spalte Iter. nennt, in welcher Iteration die Komponente in den Lieferumfang kam: 1 = Home + Burger-Menü (iteration-1.html), 2 = Lesepläne-Komponenten (iteration-2.html). Die Spezifikation ist kanonisch und wächst — die Matrix ergänzt sich pro Iteration.

KomponenteKlasseIter.BP-bewusstAnimiertAbschnitt
Home-BildschirmComposite (Orchestrator)1Scroll, Hintergrund-Transition
TopBarSticky1Scroll-Fade
HeroCarouselComposite · wischbar (inkl. Shell + Indicator)1Auto-Advance · Drag
HeroVideoCardHero-Karte · Video1Bild-Fade
HeroPosterCardHero-Karte · image-only (2-Asset)2Bild-Fade
HeroGenericCardHero-Karte · custom URL2Bild-Fade · Press
ButtonGroupSectionSection (Single-AI · Multi · Overflow)1 · 2Press-Feedback
RowSectionSection · horizontales Scrollen1horizontales Scrollen
CardPosterRow-Item · 9:161Bild-Fade · Press
CardLandscapeRow-Item · 16:9 image-only (S/M/L)2Bild-Fade · Press
CardAvatarRow-Item · Kreis + Label2Press
MenuDrawerModal · Overlay1Slide · Scrim-Fade

12 Komponenten + 1 Home-Orchestrator über Iteration 1 + 2. Jeder numerische Wert in diesem Dokument stammt aus der tatsächlichen Android-Implementierung — File:Line-Zitate erscheinen neben jedem Token, sodass jeder Wert in Sekunden gegen packages/android verifizierbar ist.

Maschinenlesbare Zusammenfassung. Ein strukturierter JSON-Snapshot dieser Matrix plus Bewegungsskala, Breakpoints, Barrierefreiheits-Baseline und Token-Abdeckungszahlen wird neben dem Dokument unter handover.json veröffentlicht — nützlich für automatisiertes Tooling, agenten-getriebene Implementierung und CI-Checks, die den Spezifikations-Kontrakt programmatisch konsumieren müssen.

Home-Bildschirm — Layout & Verhalten

Umfang Iteration 1 Composite

Der Home-Bildschirm ("Mediathek") ist die primäre Oberfläche zur Inhaltsentdeckung der App. Er besteht aus einem vertikalen Scroll von eigenständigen Inhaltsblöcken: einer fixierten Top-Leiste, einem wischbaren Hero-Karussell, einem CTA-Streifen "Die Bibel erkunden" über die volle Breite und einem unbestimmten Stapel horizontal scrollender Inhaltszeilen, die von der Page-Composition-API geladen werden.

Oberfläche
Vollbild.
Hintergrund
semantic.{mode}.background.canvas
Scroll
Vertikal, einspaltig, volle Breite.
Design
Übernimmt Hell oder Dunkel gemäß Nutzerpräferenz. Hero-Blöcke überschreiben lokal basierend auf Slide-Inhalt.

Layout-Struktur

Der Home-Bildschirm ("Mediathek") ist ein vertikaler Scroll eigenständiger Inhaltsblöcke, orchestriert von CollectionScreen (dem server-getriebenen Home-Composer). Er hat drei Verantwortlichkeiten, die die im Umfang befindlichen Komponenten nicht selbst übernehmen: (1) Abschnittskomposition + Abstände, (2) die Sticky-Top-Leiste mit scroll-getriebenem Hintergrund-Fade, und (3) die Hintergrund-Transition — ein Vollbild-Gradient, der die Farbe des aktiven Hero-Slides aufnimmt und sie über die gesamte Canvas blutet.

Mobil — vertikale Komposition Alle Abschnittsabstände aus views.home.layout.* Tokens
canvas · effectiveBaseColor ↧ Hintergrund-Gradient · 5 Stufen · oben = Hero-Slide-Hintergrund über Canvas gemischt TopBar · sticky · Scroll-Fade-Hintergrund contentPaddingTop {Mobile/Tablet} HeroCarousel + HeroVideoCard sectionGap {Mobile/Tablet} ButtonGroupSection sectionGap RowSection · CardPoster-Reihe RowSection · …N weitere, server-getrieben contentPaddingBottom 1 2 3 4 5 6

Callout-Legende

  1. Canvas — Basis-HintergrundVollbild-Hintergrund = pageBackgroundColor: semantic.{mode}.background.canvas in beiden Modi (hell #F8F9FA / dunkel #080D16). Die Page-API kann pro Seite per appearance.backgroundColorLight/Dark übersteuern. Der Gradient-Bleed (Punkt 2) zeichnet darüber.CollectionScreen.kt:98–104, 152–155
  2. Hintergrund-Gradient-Bleed (die "Hintergrund-Transition")Vertikaler Gradient mit 6 Stufen, gezeichnet von GradientBackground von y=0 bis zur Bildschirmhöhe via ColorTransformations.pageGradientStops. Obere Farbe = Mischung der gradientSourceColor über canvas bei α 0.8. Stop-Anteile: 0.0 → obere Farbe · headerStopFraction → obere Farbe · 0.3 → α 0.68 · 0.5 → α 0.48 · 0.75 → α 0.24 · 1.0 → canvas exakt (α 0) — der Gradient konvergiert nahtlos in den Seitenhintergrund (identisch zur color-engine computeGradientStops). Dies ist es, was dem Home seine hero-getriebene Stimmung verleiht.CollectionScreen.kt:315–343 (GradientBackground) · ColorTransformations.kt:35 (pageGradientStops)
  3. HeroCarousel-BlockOberster Inhaltsblock. Sein onBackgroundColorChange-Callback aktualisiert den heroBackgroundColorArgb-State auf dem Home. Die Gradient-Quellfarbe wird per resolvePageGradientSource aufgelöst: API-appearance.gradientColorLight/Dark gewinnt, extrahierte Hero-Farbe ist Fallback, sonst Canvas.CollectionScreen.kt:107–110, 133–140, 345–351
  4. AbschnittsabstandVertikaler Abstand zwischen allen Abschnitten · views.home.layout.sectionGapMobile = 24dp · sectionGapTablet = 32dp · getrieben von verticalArrangement = Arrangement.spacedBy(section.verticalSpacing) in der LazyColumnCollectionRenderer.kt:54 · views/home.json (Spec 083)
  5. ButtonGroupSectionCTA-Streifen über volle Breite. Siehe ButtonGroupSection.
  6. RowSection × N — server-getriebenAnzahl der Reihen + ihre Kartentypen kommen aus der Composition-API. Iteration 1 rendert nur POSTER_PORTRAIT-Reihen. Siehe RowSection.

Hintergrund-Transition — formaler Kontrakt

Der Hintergrund des Home "folgt" dem aktiven Hero-Slide. Dies ist die wichtigste visuelle Signatur des Designs; sie verbindet Hero → Home-Canvas zu einer kohärenten Oberfläche. Der Implementierer muss diesen Kontrakt exakt reproduzieren.

PhaseWas passiertQuelle
Initiales LadenHome wird gerendert. Wenn der erste Hero-Slide eine gradientColor-Hex-Angabe in seinem Composition-Payload trägt, wird einmal eine initiale Farbe emittiert — der Hintergrund-Gradient übernimmt diese Farbe. Andernfalls bleibt die Canvas-Farbe als Basis.HeroCarousel.kt:176–199
Slide settledHeroCarousel emittiert onBackgroundColorChange(currentPageBgColor) über seinen Callback. Home speichert ihn als heroBackgroundColorArgb-State. Die nächste Recomposition berechnet gradientSourceColor = heroBackgroundColor neu. GradientBackground zeichnet mit der neuen oberen Farbe neu.CollectionScreen.kt:100–103, 132–139, 263–266
Nutzer zieht KarussellHeroCarousel emittiert kontinuierlich während des Ziehens — die Farbe wird interpoliert lerp(fromBgColor, toBgColor, dragProgress). Der Hintergrund-Gradient des Home aktualisiert sich in Echtzeit und morpht zwischen den Stimmungen der zwei Slides.HeroCarousel.kt:137–159
Seitenwechsel (Auto-Advance)Wie bei "Slide settled" — der Emit erfolgt auf der neuen currentRealPage, nachdem die Auto-Advance-Animation abgeschlossen ist.HeroCarousel.kt:165–174
Nutzer scrollt nach untenDer TopBar-Hintergrund blendet ein (Alpha 0 → 1 über 300 px Scroll). Die Headline-Opazität bleibt bei 1. Der Gradient-Bleed (unter der TopBar) bleibt sichtbar, bis der Nutzer darüber hinausscrollt — es gibt keinen separaten scroll-getriebenen Fade auf dem Gradient selbst.CollectionScreen.kt:141–152
Theme-Wechsel (Dunkel ↔ Hell)Sowohl baseDarkColor als auch canvasBg werden neu berechnet (heller Modus verwendet background.surface, dunkler verwendet background.canvas). Der Gradient wird neu gemischt.CollectionScreen.kt:95–97, 327

Hintergrund-Gradient — 6 Stufen, vollständig spezifiziert

Stop-AnteilFarbeHinweise
0.0blendOver(gradientColor, canvas, α 0.8)Oberer Bildschirmrand — stärkster Hero-Farbeinfluss.
headerStopFractionblendOver(gradientColor, canvas, α 0.8)Gleiche Farbe wie 0.0 — hält den oberen Farbton flach unter der TopBar. headerStopFraction = stickyHeaderHeight / screenHeight.
0.3blendOver(gradientColor, canvas, α 0.68) = 0.8 × 0.85Beginnt zur Canvas auszublenden.
0.5blendOver(gradientColor, canvas, α 0.48) = 0.8 × 0.6Mitte des Bildschirms — halb gemischt.
0.75blendOver(gradientColor, canvas, α 0.24) = 0.8 × 0.3Letzter getönter Stop.
1.0canvas (exakt, α 0)Unten — konvergiert exakt in den Seitenhintergrund, keine Naht.

ColorTransformations.kt:35 (pageGradientStops) · blendOver(source, destination, sourceAlpha) in ColorTransformations · blendAlpha = 0.8f macht den oberen Bereich "dominant hero, aber nicht reines Hero" · Stop-Kurve identisch zur color-engine (lib/palette.js computeGradientStops), die die per-Item-Paletten der Page-API erzeugt.

Verhalten

Abschnittskomposition

Der Server liefert eine collection.sections-Liste. CollectionRenderer rendert sie in Reihenfolge mit LazyColumn und verticalArrangement = Arrangement.spacedBy(section.verticalSpacing). Jeder Abschnitt ist eines von: HeroCarouselSection, ButtonGroupSection, RowSection (POSTER_PORTRAIT im Umfang von Iteration 1). Außerhalb des Umfangs liegende Abschnittstypen (FEATURED, TOP10, AVATAR) sind aktuell im Quellcode vorhanden, werden aber im Iteration-1-Home nicht gerendert.

Produktiver Home-Payload (Iteration 1, live)

Live-Beispiel aus https://page-api.beta.bibeltv.de/v1/page/home (Stand Mai 2026) — die Iteration-1-Produktions-Payload trägt genau diese 5 Komponenten in dieser Reihenfolge:

  1. slider_hero mit 2 card_hero_media-Slides (→ HeroCarousel mit HeroVideoCard)
  2. group_button mit 1 Button "Die Bibel erkunden" (→ ButtonGroupSection im Single-Item-AI-CTA-Modus)
  3. slider mit 3 card_poster_small-Karten (→ RowSection mit CardPoster)
  4. slider mit 3 card_poster_medium-Karten
  5. slider mit 3 card_poster_small-Karten

Was das für Iteration 1 bedeutet: die Agentur muss nur die zwei Karten-Typen card_hero_media und card_poster_* ausliefern, plus die Single-Item-AI-CTA-Variante der ButtonGroupSection. Andere Karten-Typen (verse, featured, livestream, landscape, top10, avatar) existieren im Composition-Modell und im Android-Renderer, werden aber von dieser Payload nicht angefordert und sind explizit verschoben. Der Multi-Item-/Overflow-Branch der ButtonGroupSection muss trotzdem implementiert werden — er ist der dokumentierte Vertrag und kann in einer zukünftigen Payload-Variante aktiviert werden ohne Code-Änderung.

Sticky Header

Die TopBar wird in einer separaten Box bei zIndex(10f) gerendert, NICHT innerhalb der LazyColumn. Das bedeutet, sie bleibt am oberen Rand geklebt, auch wenn der Inhalt darunter scrollt. Ihr Hintergrund ist der scroll-getriebene Fade (siehe TopBar).

Statusleiste

Die System-Statusleiste wird per window.statusBarColor = TRANSPARENT auf transparent gezwungen; isAppearanceLightStatusBars spiegelt das aktuelle Design, sodass die System-Icons lesbar bleiben. Padding für die Höhe der Statusleiste wird innerhalb der TopBar-Komponente hinzugefügt (nicht als Window-Inset auf der Bildschirm-Wurzel), sodass der Gradient-Bleed dahinter durchgreift.

Pull to Refresh

Iteration 1: ja, Pull-to-Refresh ist über PullToRefreshBox verdrahtet. Löst onRefresh aus, das den Composition-Payload erneut lädt. Spinner-Farbe folgt dem Design.CollectionScreen.kt:160–164

Dimensionen

EigenschaftMobilTabletToken
Horizontales Seitenpadding16dp32dpviews.home.layout.contentPaddingHorizontal{Mobile,Tablet}
Oberes Inhalts-Padding8dp16dpviews.home.layout.contentPaddingTop{Mobile,Tablet}
Abschnittsabstand (zwischen Abschnitten)24dp32dpviews.home.layout.sectionGap{Mobile,Tablet}
Unteres Inhalts-Padding24dp32dpviews.home.layout.contentPaddingBottom{Mobile,Tablet}
Karten-Abstand innerhalb Reihe16dp16dpviews.home.layout.rowCardSpacing{Mobile,Tablet}
Höhe des Sticky-HeadersstatusBar + 56dpstatusBar + 56dpcollectionView.headerHeightDp + System-Inset
Hintergrund-Gradient — Blend-Alpha0.80.8collectionView.gradientBlendAlpha

Barrierefreiheit

  • Der Home ist eine vertikale Scroll-Region — Hilfstechnologien kündigen jeden Abschnitt nacheinander an.
  • Der Hintergrund-Gradient ist rein dekorativ — er trägt keine semantische Bedeutung.
  • Pull-to-Refresh kündigt "Aktualisieren" beim Auslösen und "Aktualisiert" bei Erfolg an.
  • Die Statusleiste verwendet das passende Erscheinungsbild (helle Icons im dunklen Design, dunkle Icons im hellen Design) für die Lesbarkeit.

Audit-Notizen

Keine Regressionen erkannt. Alle Abschnittsabstände werden über views.home.layout.*-Tokens gelesen (Spec 083). Der Hintergrund-Gradient verwendet das tokenisierte collectionView.gradientBlendAlpha = 0.8f. Die 5 Stop-Anteile des Gradients (0.0, headerStopFraction, 0.3, 0.5, 1.0) und die kumulativen Alpha-Multiplikatoren (×0.85, ×0.6, ×0.3) sind Laufzeit-Layoutkonstanten — sie sind zurecht nicht tokenisiert, da sie die Kurvenform definieren, nicht einen Designwert.

Ein Hinweis für Implementierer: Die Hintergrund-Gradient-Logik liegt in einem privaten Composable innerhalb CollectionScreen.kt. Falls iOS/Web die Formel über die Design-Tokens-API teilen möchten, könnten die 5 Stop-Anteile zu views.home.backgroundGradient.stops (ein Array) befördert werden. Für Parität nicht erforderlich — die Mathematik ist klein genug, um sie neu zu implementieren.

Verhalten

Initiales Laden

Bei der ersten Navigation zum Home: Page-Composition-Payload laden, TopBar sofort rendern (Skeleton-Hero + Skeleton-Reihen darunter), dann Inhalt rendern, sobald er aufgelöst wird. Das Hero-Karussell beginnt erst mit dem Auto-Advance, nachdem das erste Slide-Bild aufgelöst ist.

Scroll-Verhalten

Vertikales Scrollen blendet die Deckkraft des TopBar-Logos von 1 → 0 über die ersten component.btvTopBar.scrollFadeDistanceDp = 120dp des Scrolls aus und lässt die Cast- und Konto-Icons vollständig sichtbar. Die Hintergrundfarbe der Leiste blendet im selben Bereich von transparent zu semantic.{mode}.background.canvas.

Tab-Reta

Das Antippen des Home-Tabs, während Home bereits aktiv ist, scrollt den Inhalt mit der plattformüblichen Smooth-Scroll-Animation (≤300ms) nach oben.

Pull to Refresh

Seit Iteration 2 implementiert: PullToRefreshBox (Material 3) umschließt den Inhalt; die Geste löst onRefresh aus und der Indikator verschwindet, sobald der Page-Payload nicht mehr im Loading-Zustand ist.CollectionScreen.kt:157–166

Leere / Fehlerzustände

Wenn der Page-Composition-Payload fehlschlägt: TopBar + zentrierter Text "Inhalt konnte nicht geladen werden" mit einem Wiederholungs-Button rendern. Keine Skeletons.

Composition-API-Kontrakt

Der Bildschirm konsumiert den Page-Payload (GET /api/v1/pages/{slug}PlaylistCollection):

FeldTypBedeutung
idStringSeiten-Slug (z. B. home, mediathek, leseplan).
titleString?Optionaler Seitentitel.
sectionsList<Section>Servergesteuerte Abschnittssequenz; pro Abschnitt siehe den jeweiligen Komponenten-Kontrakt.
appearance.gradientColorDark / gradientColorLightString? (Hex)Quellfarbe des Hintergrund-Gradient-Bleeds pro Modus. Hat Vorrang vor der extrahierten Hero-Farbe (resolvePageGradientSource); fehlt der Wert, ist die extrahierte Hero-Farbe der Fallback, sonst Canvas.
appearance.backgroundColorDark / backgroundColorLightString? (Hex)Seitenhintergrund pro Modus (Basis, TopBar-Fade-Ziel und Gradient-Konvergenzpunkt zugleich). Fallback: Token bg.canvas (#080D16 / #F8F9FA). Vom Server aus PAGE_BACKGROUND (assemblyService) befüllt — muss mit dem Token und der color-engine-Canvas übereinstimmen.

PlaylistCollectionModels.kt (CollectionAppearance) · CollectionScreen.kt:98–110, 133–140 · playlist-api/src/types/playlist.ts

Referenz

Home mobile, dark
Mobil · Dunkel · Scrollposition 0
Home mobile, light
Mobil · Hell · Scrollposition 0
Home tablet, dark
Tablet · Dunkel · Scrollposition 0
Home tablet, light
Tablet · Hell · Scrollposition 0

TopBar

Umfang Iteration 1 Sticky

Persistente Leiste, die oben am Home- (und anderen Top-Level-)Bildschirmen verankert ist. Beherbergt das Marken-Logo, einen Chromecast-Trigger und den Konto-/Burger-Button, der den MenuDrawer öffnet.

Visuelle Spezifikation

Aufbau

Sticky-Leiste, oben am Home-Bildschirm verankert. Beherbergt das Marken-Logo (links), Chromecast-Trigger (rechts) und das Konto-/Burger-Icon, das den MenuDrawer öffnet. Der Hintergrund blendet aus transparent ein, sobald der Nutzer die Seite darunter scrollt.

Mobil & Tablet — gleiche Struktur, breakpoint-bewusstes Padding Leistenhöhe 56dp · Logohöhe 22dp
Statusleisten-Inset · vom Gerät gemeldet · transparent Header-Container · Höhe 56dp · scroll-getriebener bg-Fade · zIndex 10 bibel APP (Wortmarke) 22dp Höhe [cast] [person] ↓ Scroll-Fortschritt α = 0 → 1 über 300 px Scroll 1 2 3 4 5 6

Callout-Legende

  1. Header-Container — sticky, scroll-getriebener HintergrundHöhe: collectionView.headerHeightDp = 56dp · linkes Padding 16dp (gapDefault) · rechtes Padding 8dp (gap2xs) · zIndex collectionView.headerZIndex = 10CollectionScreen.kt:282–283 · MediathekTokenModels.kt CollectionViewTokens
  2. Statusleisten-Inset — system-gestelltTransparenter Bereich über der Leiste = WindowInsets.statusBars.calculateTopPadding() · Statusleisten-Hintergrund transparent gezeichnet, sodass der Gradient/Scroll-Fade der Leiste darunter durchgreiftCollectionScreen.kt:105, 119–122 · Statusleiste im SideEffect transparent gesetzt
  3. Marken-Logo (BtvLogo) — linksHöhe PrimitiveSpaceTokens.Space5_5 = 22dp · Textfarbe semantic.{mode}.text.primary · Akzent (Stern) ist dynamisch — kommt vom composition-gelieferten Akzent des aktiven Hero-SlidesCollectionScreen.kt:287–291 · BtvLogo.kt
  4. Cast-Icon (Chromecast-Trigger)MaterialSymbols.Cast · 24dp-Icon-Glyph innerhalb 48dp-IconButton-Trefferfläche · Tint semantic.{mode}.text.primary · Content-Description "Streamen"CollectionScreen.kt:296–302
  5. Konto-/Menü-Icon — öffnet Burger-DrawerMaterialSymbols.PersonText · 24dp-Glyph in 48dp-IconButton · gleicher Tint · Content-Description "Konto" · onClick = onBurgerMenuOpen ruft den MenuDrawer aufCollectionScreen.kt:303–309
  6. Scroll-getriebener Hintergrund-FadeHintergrund-Alpha der Leiste = (scrollOffset / 300f).coerceIn(0, 1), wobei scrollOffset = listState.firstVisibleItemScrollOffset + listState.firstVisibleItemIndex × 500f. Aufgelöste Farbe = semantic.{mode}.background.canvas bei diesem Alpha.CollectionScreen.kt:141–152 · headerFadeScrollDivisor = 300f und scrollOffsetItemMultiplier = 500f sind beide @TokenExempt(behavioral) in CollectionViewTokens

Dimensionen — alle Tokens, keine Inline-Werte

EigenschaftWertTokenQuelle
Leistenhöhe56dpcomponent.{mode}.collectionView.headerHeightDpMediathekTokenModels.kt
Logohöhe22dpcomponent.{mode}.collectionView.headerLogoHeightDp (oder Äquivalent primitives.spacing.Space5_5)MediathekTokenModels.kt
Linkes Padding16dpcomponent.{mode}.collectionView.headerLeftPadding = semantic.spacing.{bp}.gapDefaultCollectionScreen.kt:283
Rechtes Padding8dpcomponent.{mode}.collectionView.headerRightPadding = semantic.spacing.{bp}.gap2xsCollectionScreen.kt:283
Action-Icon-Abstand0dp (Icons berühren sich)Sentinel-Null, exception-getaggtCollectionScreen.kt:293
Icon-Trefferfläche48dp × 48dpMaterial 3 IconButton-Default · erfüllt a11y-Minimuma11y-Baseline
z-index10component.{mode}.collectionView.headerZIndexCollectionScreen.kt:274
Scroll-Fade-Divisor300 (px)component.{mode}.collectionView.headerFadeScrollDivisor · TokenExempt(behavioral)CollectionScreen.kt:147

Zustände

Standard (oben im Scroll)
Hintergrund der Leiste vollständig transparent · Logo und Icons vollständig sichtbar.
Gescrollt
Hintergrund-Alpha steigt linear mit dem Scroll. Bei scrollOffset ≥ 300 ist der Hintergrund vollständig undurchsichtig semantic.{mode}.background.canvas.
Gedrückt (Icon)
Standard-Material-3-IconButton-Ripple innerhalb einer 48dp-kreisförmigen Trefferfläche · Tint semantic.{mode}.text.primary bei primitives.opacity.scale15.
Deaktiviert (Cast)
Wenn kein Cast-Gerät verfügbar · Icon-Alpha sinkt auf primitives.opacity.scale40, IconButton nicht klickbar.

Verhalten

  • Logo-Akzent — wird pro aktivem Hero-Slide über den Color-Emit-Callback des Karussells neu berechnet. Der Akzent wird als logoAccentColor-Parameter an BtvLogo übergeben.
  • Logo-Tap — in der aktuellen Home-Implementierung nicht an einen Callback gebunden (das Logo ist im Home-Tab dekorativ).
  • Cast-Tap — aktuell No-Op (onClick = { }) — vom Implementierer an den Cast-Picker der Plattform anzubinden.
  • Menü-Tap — ruft onBurgerMenuOpen auf, das der bildschirmebenen Orchestrator an den MenuDrawer weiterleitet.
  • Statusleisten-Behandlungwindow.statusBarColor wird in einem SideEffect auf transparent gezwungen; isAppearanceLightStatusBars = isLightMode, sodass die System-Icons zum Design passen.

Barrierefreiheit

  • Beide Action-Icons haben explizite contentDescription ("Streamen", "Konto"). Deutsch ist die einzige Sprache in Iteration 1.
  • Trefferflächen sind ≥ 48dp dank des Standard-Material-3-IconButton.
  • Der Scroll-Fade der Leiste hat keinen Einfluss auf die Barrierefreiheit — Inhalt ist immer erreichbar; nur die visuelle Hintergrund-Opazität ändert sich.
  • Fokusreihenfolge: Logo → Cast → Konto/Menü.

Audit-Notizen

Keine Regressionen erkannt. Alle Dimensionen sind token-getrieben; die beiden Verhaltenskonstanten (300f Scroll-Fade-Divisor, 500f scrollOffsetItemMultiplier) sind korrekt als @TokenExempt(behavioral) getaggt, da es Math-Konstanten sind, keine Designwerte.

Offene Lücke: Das onClick des Cast-Icons ist eine No-Op — der Implementierer muss den Cast-Picker der Plattform verdrahten (Android MediaRouter / iOS AVRoutePickerView / Web TBD).

Zustände

Standard
Logo bei voller Deckkraft; Hintergrund der Leiste transparent.
Gescrollt
Logo-Deckkraft proportional zu min(1, scrollY / 120dp), linear auf 1 - x abgebildet. Hintergrund-Deckkraft der Leiste entspricht.
Gedrückt (Icon)
Jedes Icon zeigt ein kreisförmiges Press-Feedback bei opacity.scale15, getintet mit semantic.{mode}.text.primary, ≤56dp Durchmesser.
Deaktiviert
Cast-Icon, wenn kein Cast-Gerät verfügbar: opacity.scale40, kein Press-Feedback.

Verhalten

  • Logo-Tap — navigiert zur Wurzel des Home-Tabs. Wenn bereits auf Home, scrollt nach oben.
  • Cast-Tap — öffnet den Standard-Cast-Picker der Plattform. iOS: AVRoutePickerView. Android: Cast-SDK MediaRouteButton. Web: in Iteration 1 nicht implementiert.
  • Konto-/Menü-Tap — öffnet den MenuDrawer. Mobil: Vollbild-Overlay schiebt von rechts ein. Tablet: Seitenpanel schiebt mit Scrim von rechts ein.

Bewegung

AuslöserEigenschaftDauerEasing
ScrollLogo-Deckkraft, Leisten-Hintergrund-Deckkraftkontinuierlich (1:1 mit Scroll)linear
Icon-DruckPress-Feedback-Deckkraft120ms in / 200ms outease-out
MenuDrawer öffnensiehe MenuDrawer

Interaktion & UX

  • Die Leiste ist nicht erhöht: Sie rendert keinen Schatten. Visuelle Trennung entsteht ausschließlich durch den Hintergrund-Fade.
  • Die Leiste muss lesbar bleiben, wenn der Inhalt darunter hell ist; deshalb bleiben die Icons während des Scrollens bei voller Deckkraft.
  • Long-Press, Doppeltipp und Rechtsklick haben in Iteration 1 kein definiertes Verhalten.

Barrierefreiheit

  • Logo: Rolle image; Label "ProtoBible" (ohne Punkt). Antippbar, daher zusätzlich Rolle button.
  • Cast: Rolle button; Label "Cast" (Deutsch: "Übertragen"); Zustand "verfügbar" / "nicht verfügbar".
  • Konto/Menü: Rolle button; Label "Menü öffnen" / "Menü schließen" je nach Drawer-Zustand; Zustandsänderungen werden angekündigt.
  • Fokusreihenfolge: Logo → (Cast) → Menü. Plattform-Standard-Fokusring verwenden (semantic.focusRing.{bp}.*).
  • Der scroll-getriebene Logo-Fade beeinflusst die Sichtbarkeit für Screen Reader nicht — das Logo bleibt in jeder Scrollposition im Accessibility-Tree.

Verwendete Tokens

PfadEbene
semantic.{mode}.background.canvassemantic
semantic.{mode}.text.primarysemantic
semantic.icons.{bp}.mdsemantic
semantic.focusRing.{bp}.*semantic
component.btvTopBar.horizontalPaddingMobilecomponent
component.btvTopBar.horizontalPaddingTabletcomponent
component.btvTopBar.verticalPaddingMobilecomponent
component.btvTopBar.verticalPaddingTabletcomponent
component.btvTopBar.scrollFadeDistanceDpcomponent
component.btvTopBar.actionSpacingcomponent
component.btvLogo.heightMobilecomponent
component.btvLogo.heightTabletcomponent
primitives.opacity.scale15primitive
primitives.opacity.scale40primitive

Referenz

Mobile · TopBar (light + dark variants on the home tab)
Mobil · TopBar (helle und dunkle Variante im Home-Tab)

Implementierungsreferenz

Android · packages/android/app/src/main/java/com/protobible/android/ui/components/MediaCard.kt (TopBar enthalten), packages/android/app/src/main/java/com/protobible/android/navigation/MainNavigation.kt. Der scroll-getriebene Fade wird im Modifier.onScrollChange des Eltern-Bildschirms berechnet und als State-Wert propagiert.

Composition-API-Kontrakt

Keiner. Die TopBar ist rein client-seitig (Logo, Suche, Menü-Trigger) und konsumiert keine Felder aus dem Composition-Payload.

HeroVideoCard

Umfang Iteration 1 Hero-Slide-Variante Bildgeführt

Ein bildgeführter Hero-Slide, der innerhalb der HeroCardShell gerendert wird. Stellt einen einzelnen redaktionellen Videoinhalt (oder eine Playlist) dar mit Cover-Bild, optionalem Kategorie-Badge, Eyebrow, Titel, optionaler Beschreibung, primärem CTA ("Jetzt ansehen") und einem optionalen sekundären CTA. Der Slide passt sich zwischen einem Portrait-Layout für Mobile (Bild füllt die Karte; Text liegt über einem vertikalen Verlauf am unteren Rand) und einem Landscape-Layout für Tablet (Bild rechts; Text-und-CTA-Spalte links, getrennt durch einen horizontalen Verlauf, der das Bild in eine durch die Komposition gelieferte Basisfarbe überblendet) an.

Visuelle Spezifikation

Aufbau

  1. 1. Shell — bereitgestellt durch HeroCardShell. Abgerundete Ecken, Rahmen, durch die Komposition gelieferte Basisfarbe für den Verlauf, optionales onClick-Press-Feedback. Alle nachfolgenden Kinder werden innerhalb der Shell gestapelt.
  2. 2. Cover-Bildebene — füllt die Shell auf Mobile (full bleed); füllt den rechten Anteil component.heroCarousel.tabletImageWidthFraction auf Tablet. Zentriert beschnitten auf das Portrait- oder Landscape-Seitenverhältnis des Slides. Quelle: über imgix bereitgestellte URL oder Plattform-Bildressource. Lädt mit Cross-Fade-In (siehe Motion).
  3. 3. Gradient-Overlay — liegt über dem Bild. Mobile: vertikaler Verlauf am unteren Rand verankert, geht von transparent oben in die durch die Komposition gelieferte Basisfarbe unten über. Tablet: horizontaler Verlauf am linken Rand verankert, geht von der Basisfarbe an der führenden Kante zu transparent an der nachfolgenden Kante über.
  4. 4. BtvBadge — API-getriebene Kategorie-Pille — optional. Oben links. Pille, bei der sowohl Farbe als auch Text aus der Composition-Payload stammen (slide.category) — kein festes Enum. Die Beispiele "Video / Serie / Live" im Dokument sind illustrative Payloads, keine erlaubten Werte. Versteckt sich selbst, wenn der Text null/leer ist. Tokens aus component.badge.*; oberhalb des Verlaufs gerendert. Padding innerhalb der Shell: semantic.spacing.{bp}.paddingDefault = 24dp.
  5. 5. Content-Stack — vertikaler Stack innerhalb der Verlaufsregion.
    1. 5a. Optionaler Eyebrow (Untertitel), einzeilig, sekundäre Textfarbe.
    2. 5b. Titel, primäre Textfarbe, bis zu N Zeilen (Token tabletTitleMaxLines) mit Ellipsis.
    3. 5c. Optionale Beschreibung (oder EPG-Kurztext, falls verfügbar), sekundäre Textfarbe, bis zu N Zeilen mit Ellipsis.
    4. 5d. Primärer CTA — MediathekButton, Typ Primary, Größe Small, vorangestelltes Icon play_arrow.
    5. 5e. Optionaler sekundärer CTA — MediathekButton, Typ Secondary, Größe Small.

Abmessungen

EigenschaftMobileTabletToken
Karten-Seitenverhältnis0.75 (3:4 Portrait, füllt Shell)0.394 (≈2.54:1 Landscape)component.heroCarousel.aspectRatioMobile / aspectRatioTablet
Seitenverhältnis Cover-Bild15:16 (imgix-Anfrage)16:9 (imgix-Anfrage)
Breitenanteil Cover-Bild1.0tabletImageWidthFractioncomponent.heroCarousel.tabletImageWidthFraction
Breitenanteil Content-Spalte1.0tabletContentWidthFractioncomponent.heroCarousel.tabletContentWidthFraction
Breitenanteil Verlaufsregion1.0 (vertikal)tabletGradientWidthFraction (horizontal)component.heroCarousel.tabletGradientWidthFraction
Content Top-Padding (oberhalb der Verlauf-Textregion)140dpcomponent.{mode}.heroVideo.contentTopPadding
Content seitliches Padding16dp20dpsemantic.spacing.{bp}.paddingDefault (mobile 16 · tablet 20 · desktop 24)
Content unteres Padding16dp20dpsemantic.spacing.{bp}.paddingDefault (mobile 16 · tablet 20 · desktop 24)
Eyebrow Schriftgröße12sp14spcomponent.{mode}.heroVideo.mobileDescFontSize / tabletDescFontSize
Titel Schriftgröße16sp26spcomponent.{mode}.heroVideo.mobileTitleFontSize / tabletTitleFontSize
Titel Zeilenhöhe1.15× Schriftgröße (Standard)28spcomponent.{mode}.heroVideo.tabletTitleLineHeight
Titel SchriftstärkeSemiBold (Mobile)Medium (Tablet)component.{mode}.heroVideo.mobileTitleFontWeight / tabletTitleFontWeight
Beschreibung Schriftgröße12sp14spcomponent.{mode}.heroVideo.mobileDescFontSize / tabletDescFontSize
Beschreibung Zeilenhöhe18spcomponent.{mode}.heroVideo.tabletDescLineHeight
Beschreibung SchriftstärkeNormalMediumcomponent.{mode}.heroVideo.mobileDescFontWeight / tabletDescFontWeight
CTA primärer Icon-Größe14dp14dpcomponent.{mode}.heroVideo.iconSize
Abstand Eyebrow → Titel4dp4dpsemantic.spacing.{bp}.gap4xs / gap3xs
Abstand Titel → Beschreibung4dp4dpsemantic.spacing.{bp}.gap4xs / gap2xs
Abstand Content → primärer CTA12dpsemantic.spacing.{bp}.gapSm
Abstand primärer → sekundärer CTA8dp8dpsemantic.spacing.{bp}.gap2xs

Mobile Gradient-Overlay (vertikal, am unteren Rand verankert)

Acht Stops im Dark Mode (gerendert wenn isLightMode = false); sieben Stops im Light Mode. Die Basisfarbe C ist die durch die Composition-API in der Slide-Payload gelieferte Verlaufsbasis.

PositionDark Mode (8 Stops)Light Mode (7 Stops)
0%transparenttransparent
8%C @ alpha 0.15 primitives.opacity.scale15
15%C @ alpha 0.05 primitives.opacity.scale5
22%C @ alpha 0.45 primitives.opacity.scale45
35%C @ alpha 0.50 = primitives.opacity.scale50
38%C @ alpha 0.72 primitives.opacity.scale70 (gerundet)
50%C @ alpha 0.82 primitives.opacity.scale80 (gerundet)
52%C @ alpha 0.90 primitives.opacity.scale90
60%C @ alpha 0.95 primitives.opacity.scale95
62%C @ alpha 0.97 primitives.opacity.scale95 (gerundet)
68%C (deckend)
70%C (deckend)
100%C (deckend)C (deckend)

Jede Zeile mit Alpha < 1.0 ist die durch die Komposition gelieferte Basisfarbe multipliziert mit der angegebenen Opazität. Die Opazitätswerte sind auf die primitives.opacity.*-Skala gemappt.

Tablet Gradient-Overlay (horizontal, am führenden Rand verankert, 4 effektive Stops + Wiederholungen)

PositionFarbe
0%C (deckend)
60%C (deckend, Plateau)
70%C @ alpha 0.75 primitives.opacity.scale75
80%C @ alpha 0.50 = primitives.opacity.scale50
90%C @ alpha 0.25 = primitives.opacity.scale25
100%transparent

Hit-Targets

Die gesamte Shell ist ein Hit-Target (vollständiges Slide-Tippen = onClick). Jedes interaktive Kind darin (primärer CTA, sekundärer CTA, Kategorie-Badge sofern mit Aktion belegt) muss mindestens 48dp × 48dp erreichen. Polstern Sie kleinere Steuerelemente transparent auf, um das Minimum zu erreichen.

Aufbau

Wireframe-Darstellung der HeroVideoCard in den tatsächlichen Proportionen. Mobile (3:4 Portrait) oben, Tablet (~2.54:1 Landscape, Split-Panel-Layout) unten. Die Nummern verweisen auf die Legende rechts; jeder genannte Wert hat eine file:line-Referenz auf den Android-Quellcode.

Mobile 3:4 Portrait · aspectRatioMobile = 0.75 (B/H)
Bildebene (full-bleed) durch Komposition gelieferte Bild-URL {slide.category} ↓ Gradient-Overlay (8 Stops, ↦ durch Komposition gelieferte Basis) Eyebrow · "Serie / Sendungsname" Titel (bis zu 3 Zeilen) Beschreibung (mit Ellipsis) [play_arrow] Jetzt ansehen 1 2 3 4 5 6 7 8

Mobile Callout-Legende

  1. Äußerer Container — Karten-ChromeEckenradius 24dp HeroCarouselTokens.cornerRadius · 1dp eingerückter Rahmen borderColor + borderWidth = 1.dp · Schlagschatten 16dp nur im Dark Mode elevation = 16.dpMediathekTokenModels.kt:99–102
  2. Bildebene — Full-bleed-CoverAuf Container-Radius beschnitten · Skeleton-Füllung cardPoster.bgColor während des Ladens · Bild-URL aus HeroSlide.thumbnail in der Composition-PayloadHeroVideoCard.kt · HeroSharedContent.kt
  3. BtvBadge — API-getriebene Kategorie-PilleWiederverwendbares Primitive BtvBadge(color, text, modifier). Sowohl color (semantischer Schlüssel wie "live.default" oder ein Hex-String) ALS AUCH text stammen aus der Composition-Payload (slide.category) — KEIN fest codiertes Label. Fällt auf badgeTokens.defaultBackgroundColor zurück, wenn die Farbe null ist; versteckt sich vollständig, wenn der Text null/leer ist. Der Text wird in uppercase() gerendert. Oben links, eingerückt um paddingDefault = 16dp. Die Textfarbe wird per Luminanz gegen die aufgelöste Hintergrundfarbe gewählt (Schwelle ≈ 0.179, der Punkt gleicher WCAG-Kontraste für Weiß/Schwarz): dunkle/transluzente Hintergründe → badge.textColor (weiß), helle → badge.textColorOnLight (#1A1A1A). Alle Abmessungen aus component.badge.*.BtvBadge.kt (badgeTextColorFor) · HeroVideoCard.kt
  4. Gradient-Overlay — am unteren Rand verankert, mehrstufig vertikalTransparent (oben) → durch Komposition gelieferte Basisfarbe (unten). Mobile = 8 Stops im Dark Mode (7 im Light Mode). Opazitäten aus den primitives.opacity.scale*-Stufen. Höhe der Verlaufsregion = Kartenhöhe − heroVideo.contentTopPadding = 140.dpMediathekTokenModels.kt:114 (contentTopPadding)
  5. Eyebrow — Serie / SendungsnameEinzeilig · heroCarousel.eyebrowMaxLines = 1 · Diagramm Display-Familie · Schriftstärke Medium · Größe aus heroVideo.tabletDescFontSize (= 14sp, bewusst auch auf Mobile — ein separater mobileEyebrowFontSize-Token existiert nicht)HeroSharedContent.kt (RegularHeroContent eyebrow)
  6. Titel — bis zu 2 Zeilen (Mobile)Mobile titleMaxLines = 2 · Schriftstärke Medium · mit Ellipsis. Die Schriftgröße ist dynamisch abhängig von der Titel-Zeichenlänge (siehe Tabelle "Dynamische Titel-Schriftgröße" unterhalb der Diagramme).Kanonisch: Commit 4b17f706 (smart line limits). Der aktuelle Quellcode ist auf statische 3 Zeilen REGREDIERT (HeroSharedContent.kt:544) — muss auf die dynamische Regel wiederhergestellt werden.
  7. Beschreibung — bis zu 3 Zeilen (Mobile)Mobile descriptionMaxLines = 3 · Inter Body-Familie · mit EllipsisKanonisch: Commit 4b17f706 · HeroSharedContent.kt:561 setzt derzeit statisch 3, was Mobile entspricht, aber auf Tablet falsch ist — Tablet sollte sich gemäß der untenstehenden Tabelle anpassen.
  8. Primärer CTA "Jetzt ansehen"Small Primary Button mit vorangestelltem Play-Icon · bezogen von component.protoBibleButton.primary.* · Hit-Target ≥ 48 × 48 dp durch transparente AufpolsterungProtoBibleButton.kt
Tablet ~2.54 : 1 Landscape · aspectRatioTablet = 0.394 (gespeichert als H/B, angewendet als 1f / 0.394)
Bildebene · tabletImageWidthFraction = 0.7 am nachfolgenden Rand verankert Gradient-Overlay · 50% Breite · deckend LINKS → transparent RECHTS Content-Panel · tabletContentWidthFraction = 0.4 {slide.category} Eyebrow Titel (bis zu 3 Zeilen auf Tablet) Beschreibung (bis zu 5 Zeilen auf Tablet) [play_arrow] Jetzt ansehen 1 2 3 4 5 6 7 8 9

Tablet Callout-Legende

  1. Äußerer Container — Karten-ChromeDieselben Shell-Tokens wie Mobile · Seitenverhältnis abgeleitet aus aspectRatioTablet = 0.394 gespeichert als H/B; angewendet als 1f / 0.394 ≈ 2.5381 : 1 LandscapeMediathekTokenModels.kt:86
  2. Bildebene — am nachfolgenden Rand verankertBreitenanteil tabletImageWidthFraction = 0.7 · Motiv so komponiert, dass das visuelle Zentrum innerhalb des nicht ausgeblendeten Bereichs liegtMediathekTokenModels.kt:111
  3. Gradient-Overlay — horizontal, blendet Bild in Canvas-Farbe überBreitenanteil tabletGradientWidthFraction = 0.5 · Rampe vom deckenden Bild (rechts) zur Canvas-Farbe (links)MediathekTokenModels.kt:112
  4. Content-Panel — am führenden Rand verankert über Canvas-Color-BleedBreitenanteil tabletContentWidthFraction = 0.4 · gepolstert mit paddingDefault = 16dp · Text bleibt lesbar, da er über dem ausgeblendeten Teil des Verlaufs liegtMediathekTokenModels.kt:113
  5. BtvBadge — API-getriebene Kategorie-PilleDasselbe Primitive BtvBadge(color, text) wie auf Mobile. color + text stammen beide aus slide.category in der Composition-Payload — niemals fest codiert (die Beispiele "VIDEO" / "SERIES" / "LIVE" in diesem Dokument sind Platzhalter, keine Enum-Werte). Versteckt sich, wenn der Text null/leer ist. Tokens aus component.badge.*. Oben links der Karte, eingerückt um paddingDefault = 16dp.BtvBadge.kt:100–128 · HeroVideoCard.kt:318
  6. Eyebrow — Serie / SendungsnameEinzeilig · heroVideo.eyebrowMaxLines = 1 · Diagramm Display-FamilieMediathekTokenModels.kt:283 (eyebrowMaxLines)
  7. Titel — dynamische maximale Zeilenanzahl (Tablet)Landscape: max. 2 Zeilen. Portrait: normalerweise 3 Zeilen, 2 Zeilen wenn sowohl Kategorie-Pille als auch Untertitel vorhanden sind ("crowded top"). Schriftgröße ebenfalls dynamisch nach Titellänge (siehe Tabelle unten).Kanonisch: Commit 4b17f706 · HeroVideoCardTablet verwendet statisch tabletTitleMaxLines = 3 — REGRESSION; wiederherstellen.
  8. Beschreibung — dynamische maximale Zeilenanzahl (Tablet)Landscape: max. 5 Zeilen. Portrait: normalerweise 4 Zeilen, 3 Zeilen bei "crowded top". Inter Body-Familie · mit Ellipsis.Kanonisch: Commit 4b17f706 · HeroVideoCardTablet verwendet statisch tabletDescMaxLines = 5 unabhängig von der Ausrichtung — REGRESSION; wiederherstellen.
  9. Primärer CTA "Jetzt ansehen"Dieselbe Small-Primary-Komponente wie auf Mobile · am unteren Ende des Content-Stacks · Hit-Target ≥ 48 × 48 dpProtoBibleButton.kt
Dynamische Regeln — Titel-Schriftgröße & maximale Zeilenanzahl Kanonisch: Commit 4b17f706 · "smart tablet line limits + dynamic font sizing"

Zwei adaptive Regeln steuern den Text der HeroVideoCard. Beide sind für das Redesign erforderlich — der aktuelle Android-Quellcode ist regrediert und muss aus dem kanonischen Commit wiederhergestellt werden.

1. Titel-Schriftgröße — abhängig von der Titel-Zeichenlänge

Titellänge (Zeichen)SchriftgrößeZeilenhöhe
< 2028sp30sp
< 3024sp26sp
< 4022sp24sp
< 5020sp22sp
≥ 5018sp20sp

Formel: lineHeight = fontSize + 2 immer. fontSize wird pro Slide via remember(variant.title) { … } neu berechnet, sodass sie für einen gegebenen Titel stabil ist.

2. Maximale Zeilenanzahl — abhängig von Breakpoint, Ausrichtung und "crowded top"

ModushasCrowdedTopTitel max. ZeilenBeschreibung max. Zeilen
Mobile(irrelevant)23
Tablet Portraitnein23
Tablet Portraitja22
Tablet Landscape(irrelevant)35

hasCrowdedTop = slide.category != null && !slide.subtitle.isNullOrEmpty()

Tablet Portrait wurde 2026-06-10 reduziert (vorher 3/4 bzw. crowded 2/3): die schmale Inhaltsspalte plus größere Buttons machten die Karte zu textlastig. Wenn die Karte sowohl eine Kategorie-Pille ALS AUCH eine Eyebrow-Zeile hat (crowded), verliert die Beschreibung eine weitere Zeile. Werte: component.heroVideo.maxLines.* (API-übersteuerbar).

Fallback-Zustand Wenn die Bild-URL leer ist oder das Laden fehlschlägt

HeroVideoCard hat keinen expliziten Fallback bei Bildfehler: Wenn das Bild leer ist oder fehlschlägt, rendert das AsyncImage nichts und Verlaufsregion + Content-Stack werden weiterhin über die Hintergrundfarbe der Karte gerendert. Das Ergebnis ist eine "Karte nur mit Verlauf und Text", visuell ähnlich zur Gradient-only-Variante der HeroBibleVerseCard (außerhalb des Umfangs von Iteration 1), aber ohne expliziten Akzentfüllung. Wenn ein korrekter Poster-Karten-Fallback (Verlauf mit zentriertem Titel) auf dem Hero gewünscht ist, fragen Sie nach, bevor Sie ihn implementieren — er ist nicht Teil des aktuellen Vertrags.

HeroVideoCard.kt:94–111 — kein onError-Handler ist am AsyncImage angehängt.

Zustände

Default
Cover-Bild sichtbar, Verlauf + Content-Stack zusammengesetzt. Primärer CTA in voller Deckkraft.
Bild wird geladen
Bildebene wird mit Alpha 0 gerendert; sobald dekodiert, blendet sie über die Cross-Fade-Dauer auf 1 über. Die Basisfarbe des Verlaufs der Shell ist unter dem Alpha-0-Bild sichtbar, sodass der Slide nie "leer" ist — es ist die durch die Komposition gelieferte Basisfarbe, mit dem Content-Stack bereits lesbar darüber.
Bildfehler / fehlende URL
Bildebene bleibt bei Alpha 0; Verlaufsbasisfarbe füllt die Shell; Content-Stack und Kategorie-Badge bleiben lesbar. Keine Anzeige eines defekten Bildes.
Pressed (gesamter Slide)
Press-Feedback-Overlay der Shell — siehe HeroCardShell.
Pressed (CTA)
Gemäß MediathekButton — typischerweise ein tonales Overlay bei primitives.opacity.scale15.
Disabled CTA
In Iteration 1 nicht verwendet — der primäre CTA ist immer aktiviert, wenn vorhanden.
Focus (Tastatur / D-Pad)
Äußerer Fokusring von der Shell gerendert bei semantic.focusRing.{bp}.*. CTAs erhalten ihren eigenen inneren Fokusring, wenn sie über die Tastatur erreicht werden.

Verhalten

Auswahl Mobile vs. Tablet

Das Layout wechselt basierend auf dem aktiven Device-Mode-Signal (LocalDeviceMode). Mobile rendert HeroVideoCardMobile (Bild full-bleed, vertikaler Verlauf unten). Tablet rendert HeroVideoCardTablet (Bild rechts, horizontaler Verlauf, Content-Spalte links). Der Breakpoint wird zentral gesetzt — siehe Breakpoints.

Lebenszyklus des Cover-Bildes

1) Beim ersten Composition: Erstellen einer imgix-Anfrage für die thumbnail-URL des Slides mit dem Modus-spezifischen Seitenverhältnis (Mobile 15:16, Breite 600; Tablet 16:9, Breite 1200) und Fokuspunkt Y = 0.5. 2) Beginn des asynchronen Dekodierens. 3) Bei Erfolg: Cross-Fade-Einblenden der Bildebene. 4) Bei Fehler: Ebene transparent lassen; Verlaufsbasisfarbe scheint durch. Wenn der Slide eine integrierte Bildressource trägt (imageResId), wird sie direkt ohne imgix-Transformation gezeichnet.

Vorgelieferte Palette → Verlaufsbasisfarbe

Die Basisfarbe C, die von beiden Gradient-Overlays verwendet wird, wird in dieser Prioritätsreihenfolge aufgelöst (HeroColorResolver): (1) vorab durch die Komposition gelieferte Basis aus der preExtractedColors-Variante (Light oder Dark) der Slide-Payload; (2) wahrnehmungsbasierte vorgelieferte Palette aus dem Live-Bild (URL oder Ressource); (3) der statische gradientColor-Hex des Slides; (4) die Hintergrundfarbe der Seite als letzter Fallback. Der Resolver veröffentlicht außerdem eine Logo-Akzentfarbe nach oben über onColorsExtracted(backgroundGradient, logoAccent), sodass die übergeordnete Bildschirmebene das TopBar-Logo passend einfärben kann.

Eyebrow / Titel / Beschreibung Rendering

Eyebrow ist der Untertitel des Slides. Title ist der Titel des Slides. Die Beschreibung bevorzugt epgKurz (kurze EPG-Zeile), wenn verfügbar, andernfalls fällt sie auf die description des Slides zurück. Jedes Element wird ausgelassen, wenn null/leer. Alle drei respektieren ihre Token-maximalen Zeilen und verwenden Ellipsis-Overflow.

CTA-Aufruf

Tipp auf den primären CTA ruft die ctaAction des Slides auf. Tipp auf den sekundären CTA ruft secondaryCtaAction auf. Tipp auf den ganzen Slide ruft das optionale onClick auf, das vom übergeordneten Karussell registriert wurde; dies spiegelt im Allgemeinen die primäre CTA-Aktion für Slides ohne ein separates "Zur Detailseite navigieren"-Ziel.

Light- vs. Dark-Modus

Der Slide liest isLightMode vom Karussell. Textfarben, Gradient-Stop-Schema und der visuelle Stil des CTA wechseln alle anhand dieses Flags. Beide Modi verwenden dieselbe durch die Komposition gelieferte Basisfarbe C, aber der Dark-Mode-Verlauf rampt früher hoch (8 Stops), um den Text gegen typischerweise hellere Cover-Fotografie lesbar zu halten; Light-Mode verwendet 7 Stops mit einer späteren, aggressiveren Rampe.

Motion

AuslöserEigenschaftDauerEasing
Bild dekodiertAlpha der Bildebene 0 → 1200ms (Plattform-Standard-Cross-Fade)ease-out
Slide-DruckPress-Feedback der Shellsiehe HeroCardShellsiehe HeroCardShell
CTA-DruckOpazität des tonalen Overlays120ms ein / 200ms ausease-out
Karussell Auto-Advancesiehe HeroCarousel
Modus-Wechsel (Light/Dark)Verlauf-Stops, Textfarbensofort (Re-Composition, kein Tween)

Interaktion & UX

  • Tipp auf den ganzen Slide ist die primäre Affordance auf Mobile; auf Tablet ist der sichtbare primäre CTA das prominentere Ziel, aber Tipp auf den ganzen Slide funktioniert weiterhin.
  • Long-Press, Doppel-Tipp, Rechtsklick: kein definiertes Verhalten in Iteration 1.
  • Während das Karussell automatisch weiterläuft, pausiert eine aktive Berührung (laufender Drag) auf diesem Slide das Auto-Advance — siehe HeroCarousel.
  • Das Kategorie-Badge ist in Iteration 1 dekorativ (nicht interaktiv), muss aber dennoch im Accessibility-Tree vorhanden sein.
  • Beschreibungstext ist rein informativ — niemals der alleinige Träger einer Aktion.

Barrierefreiheit

  • Slide-Container: role button; zugänglicher Name = Titel; zugängliche Beschreibung = "{eyebrow}. {description}", wenn diese existieren.
  • Cover-Bild: role image; Alt-Text = Titel (dekorative Rolle wird in den übergeordneten Button zusammengeführt).
  • Kategorie-Badge: role text; wird als Teil des zugänglichen Namens des Slides angekündigt, wenn vorhanden (z. B. "Video. Titel. Beschreibung.").
  • Primärer CTA: role button; Label aus ctaLabel. Sekundärer CTA: dasselbe, Label aus secondaryCtaLabel.
  • Fokus-Reihenfolge innerhalb des Slides: Slide → primärer CTA → sekundärer CTA. Der primäre CTA muss über Tastatur / D-Pad erreichbar bleiben, auch wenn der ganze Slide tippbar ist.
  • Reduzierte Bewegung: Bild-Cross-Fade überspringen — Bild sofort beim Dekodieren mit Alpha 1 rendern.
  • Textfarben werden aus der Palette abgeleitet, sodass Kontrast nicht statisch erzwungen wird; der Resolver tendiert zu semantic.{mode}.text.onColor für primären Text und einer mit scale75 abgeschwächten Kopie für sekundären Text. Implementierungen müssen pro Slide verifizieren, dass der aufgelöste primäre Text gegenüber dem End-Stop des Verlaufs WCAG AA (≥4.5:1) erfüllt.

Genutzte Tokens

PfadTier
semantic.{mode}.background.canvassemantic
semantic.{mode}.text.primarysemantic
semantic.{mode}.text.onColorsemantic
semantic.{mode}.brand.primarysemantic
semantic.{mode}.brand.accentsemantic
semantic.{mode}.border.subtlesemantic
semantic.spacing.{bp}.paddingDefaultsemantic
semantic.spacing.{bp}.gap2xssemantic
semantic.spacing.{bp}.gap3xssemantic
semantic.spacing.{bp}.gap4xssemantic
semantic.spacing.{bp}.gapSmsemantic
semantic.focusRing.{bp}.*semantic
component.{mode}.heroCarousel.aspectRatioMobilecomponent
component.{mode}.heroCarousel.aspectRatioTabletcomponent
component.{mode}.heroCarousel.tabletImageWidthFractioncomponent
component.{mode}.heroCarousel.tabletGradientWidthFractioncomponent
component.{mode}.heroCarousel.tabletContentWidthFractioncomponent
component.{mode}.heroCarousel.cornerRadiuscomponent
component.{mode}.heroCarousel.borderColorcomponent
component.{mode}.heroCarousel.borderWidthcomponent
component.{mode}.heroCarousel.bodyFontFamilycomponent
component.{mode}.heroCarousel.displayFontFamilycomponent
component.{mode}.heroCarousel.eyebrowMaxLinescomponent
component.{mode}.heroCarousel.titleMaxLinescomponent
component.{mode}.heroCarousel.descriptionMaxLinescomponent
component.{mode}.heroVideo.contentTopPaddingcomponent
component.{mode}.heroVideo.mobileTitleFontSizecomponent
component.{mode}.heroVideo.mobileTitleFontWeightcomponent
component.{mode}.heroVideo.mobileDescFontSizecomponent
component.{mode}.heroVideo.mobileDescFontWeightcomponent
component.{mode}.heroVideo.tabletTitleFontSizecomponent
component.{mode}.heroVideo.tabletTitleLineHeightcomponent
component.{mode}.heroVideo.tabletTitleFontWeightcomponent
component.{mode}.heroVideo.tabletTitleMaxLinescomponent
component.{mode}.heroVideo.tabletDescFontSizecomponent
component.{mode}.heroVideo.tabletDescLineHeightcomponent
component.{mode}.heroVideo.tabletDescFontWeightcomponent
component.{mode}.heroVideo.tabletDescMaxLinescomponent
component.{mode}.heroVideo.eyebrowMaxLinescomponent
component.{mode}.heroVideo.iconSizecomponent
component.{mode}.heroVideo.displayFontFamilycomponent
component.{mode}.heroVideo.bodyFontFamilycomponent
primitives.opacity.scale25primitive
primitives.opacity.scale50primitive
primitives.opacity.scale75primitive
primitives.opacity.scale15primitive

Referenz

Mobile · Dark — full-bleed image with bottom gradient
Mobile · Dark — Full-bleed-Bild mit Verlauf am unteren Rand
Mobile · Light
Mobile · Light
Tablet · Dark — split-pane image left, content right
Tablet · Dark — Split-Pane Bild links, Inhalt rechts
Tablet · Light
Tablet · Light

Implementierungsreferenz

Android · packages/android/app/src/main/java/com/protobible/android/ui/components/hero/cards/HeroVideoCard.kt. Mobile-Layout in HeroVideoCardMobile; Tablet-Layout in HeroVideoCardTablet. Gemeinsamer Inhalt wird über RegularHeroContent in HeroSharedContent.kt gerendert. Vorgelieferte Palette ist über rememberHeroColors in HeroColorResolver.kt verdrahtet; Bildtransformationen über ImgixUtils.getHeroImageUrl. CTA-Buttons verwenden MediathekButton (type = Primary/Secondary, size = Small).

Composition-API-Vertrag

Annotationen verfasst vom API-Engineer. Jedes Feld der HeroCard_Video / HeroCarouselContainer-Payload (badge, sponsor, eyebrow, headline, description, button, image, color) ist im darunterliegenden Source-of-Truth-Diagramm dokumentiert. Click-Ziel wird stets aus objectType + objectId aufgelöst; Farben stammen aus dem Design-Tokens-System je Theme.

Composition API annotations for HeroCard_Video / HeroCarouselContainer — badge, sponsor, eyebrow, headline, description, button, image, and color fields
HeroCard_Video Composition-API-Annotationen (deutsch). Click-Ziel aufgelöst über objectType + objectId; Button-Farben über Design Tokens. Quelle: Pencil-Board des API-Engineers.

API-Felder (1:1 zu CompositionModels.kt)

Diese Tabelle ist die Vereinigung der Annotationen des API-Engineers und des tatsächlich bereits in packages/android/app/src/main/java/com/protobible/android/data/models/CompositionModels.kt implementierten Wire-Vertrags. Feldtypen und Required-/Optional-Flags spiegeln das Modell wider — die Agentur soll dies exakt abbilden. Wo sich die verbale Beschreibung des API-Engineers und das implementierte Modell unterscheiden, wird die Differenz explizit benannt.

Feld in CompositionCardTyp (gemäß CompositionModels.kt)Hinweise
type CardType-Enum → Wire-Wert "card_hero_media" "Grundsätzlich type: 'card_hero_media', unabhängig vom Inhalt (ob Video oder Serie)." Ein API-Typ für jede Hero-Medien-Karte, unabhängig davon, ob der zugrundeliegende Inhalt ein Video oder eine Serie ist. Definiert bei CompositionModels.kt:95 (@SerialName("card_hero_media") CARD_HERO_MEDIA).
objectType + objectId ObjectType-Enum (video · series · verse · playlist · generic) + String? Löst das Tap-Ziel für die gesamte Karte UND den Button auf. CompositionModels.kt:72–73, 102–108.
badge CompositionBadge? { displayText: String, backgroundColor: String, textColor: String } Wird nur gerendert, wenn das gesamte badge-Objekt vorhanden ist. Wenn die API auf dieser Karte kein Badge möchte, wird die Eigenschaft vollständig weggelassen (null); wenn sie gesendet wird, sind alle drei Felder erforderlich.
"displayText für den Text, der angezeigt werden soll. Ein etwaiges permanentes Uppercasing hat Clientseitig zu erfolgen. backgroundColor für die Hintergrundfarbe mit AlphaKanal optional. textColor für die Textfarbe." Client führt Uppercasing aus (Android erledigt dies bei BtvBadge.kt:119 über text.uppercase()). Hex-Strings mit optionalem Alphakanal — geparst über android.graphics.Color.parseColor (CompositionModels.kt:159–160). Modelldefinition bei CompositionModels.kt:114–119.
imageUrl String? "Falls keine imageUrl angegeben, dann vollflächig über gesamte Kartenhöhe." Wenn nicht vorhanden, deckt der farbige/verlaufende Hintergrund die volle Kartenhöhe ab (kein Bildbereich). CompositionModels.kt:83.
colors CompositionColorPalette { light: CompositionColorScheme, dark: CompositionColorScheme }, wobei jedes Schema = { base: String, accent: String, textPrimary: String?, textSecondary: String?, backgroundGradient: Map<String,String> } Pro Theme — separate light- und dark-Paletten auf jeder Karte. backgroundGradient ist eine Map mit Stop-Prozent-Strings ("0", "25", "50", "75", "100") als Schlüssel → Hex-Farbe; toGradientStops() sortiert und konvertiert zu Compose-Farben (CompositionModels.kt:149–155). CompositionAdapter.kt:126–132 liest genau diese fünf Stops.
"Hat im Hero-Slider Einfluss auf die allgemeine Background Color der App über; Farbverlauf aus colors.base von Alpha 0% (oben) auf Alpha 100% (unten)."
eyebrowText String? Serie / Sendungsname über dem Titel. Einzeilig. CompositionModels.kt:76.
headlineText String? Der Titel. Im Modell nullable, auch wenn der API-Engineer ihn als Headline beschreibt. CompositionModels.kt:75.
descriptionText String? Fließtext unter dem Titel. CompositionModels.kt:77.
user.progressPercent CardUser { progressPercent: Int? }, gesamtes user nullable "Unten ist noch eine optionale ProgressBar aus user.progressPercent." Gleicher Vertrag wie CardPoster: Der Balken wird ausgeblendet, wenn user == null, wenn der Wert 0 ist oder wenn der Wert 100 ist. Wird andernfalls am unteren Rand der Karte gerendert. CompositionModels.kt:89, 128–131. Adapter konvertiert zu Bruchteil bei CompositionAdapter.kt:80 (progressPercent / 100f).
button CompositionButton { displayText: String } — nur displayText wird hier konsumiert "displayText für den Text, der angezeigt werden soll. Das Ziel des Buttons ergibt sich aus objectType und objectId."
Nutzungsregel: HeroCard_Video komponiert den Design-System-ProtoBibleButton / MediathekButton und zieht alle Button-Farben aus Design Tokens. Das Modell trägt technisch zwar auch optionale backgroundColor / textColor-Felder auf CompositionButton (CompositionModels.kt:122–126), aber diese Komponente ignoriert sie — Tokens sind die Source of Truth.
shareUrl String? Ziel für das Share-Sheet. Außerhalb des Umfangs von Iteration 1 Home, aber im Modell vorhanden (CompositionModels.kt:84). Implementierer kann dies für diese Iteration ignorieren.
tracking CardTracking { impression: CardImpression, click: CardClick } Erforderliche Impression- und Click-Events auf jeder Karte (Event-Name, cardId, cardType, objectType, objectId). Impression auslösen, wenn die Karte in den Viewport gelangt; Click, wenn der Benutzer auf die Karte oder ihren primären Button tippt. CompositionModels.kt:193–214.
durationSeconds · audioUrl · displayText Alle String? / Int? Gehören zu Geschwister-Kartentypen (card_hero Vers- / Video-Metadaten). Werden vom card_hero_media-Rendering auf Home Iteration 1 nicht verwendet. CompositionModels.kt:78–81.

Hinweise zum Abgleich für Implementierer: (1) Das Uppercasing des Badge erfolgt clientseitig bei BtvBadge.kt:119. (2) Der Progress-Bar-Vertrag lautet ausblenden, wenn user == null, wenn progressPercent == 0 oder wenn progressPercent == 100 — nur für laufende Elemente rendern. Der aktuelle Android-Quellcode (CardPoster.kt:133 + Äquivalent in HeroVideoCard.kt) blendet bei 0/null aus, rendert aber bei 100; mit dem Vertrag in Einklang bringen.

Composition-API-Kontrakt

Im Pages-Payload (/api/v1/pages/<page>) ist jeder Video-Slide ein VideoHeroItem (Diskriminator type="video") innerhalb einer hero_carousel-Sektion. Modell: PlaylistCollectionModels.kt:140.

FeldTypHinweise
idStringStabile Slide-Kennung.
type"video"Diskriminator → HeroVideoCard.
titleStringTitel des Inhalts; max. 2 Zeilen im Split-Layout.
subtitleString?Eyebrow-/Serienzeile über dem Titel.
seriesTitleString?Serienname (im Showcase nicht gesetzt).
descriptionString?Langbeschreibung; epgInfo hat Vorrang, wenn beide gesetzt sind.
contentTypeString?Kategorie-Badge ("Serie", "Drama", …).
durationString?Längen-Label ("44 Min.").
epgInfoString?EPG-Kurzinfo; verdrängt description in der Beschreibungszeile.
imageUrlString?Hero-Bild; Layout-spezifisch zugeschnitten (Hochformat mobil, ~2.54:1 Tablet).
colorsCollectionExtractedColors?Vorab extrahierte Palette {light,dark}.{base,accent} — treibt Gradient-Scrim + Badge-Farben.
deepLinkString?Navigationsziel beim Tippen (z. B. video/19225).

HeroBibleVerseCard

Auf nächste Iteration verschoben Hero-Variante

Variante der Hero-Karte, die einen Bibelvers (statt Video oder Serie) im Karussell zeigt. Im aktuellen Android-Quellcode unter packages/android/app/src/main/java/com/protobible/android/ui/components/hero/cards/HeroBibleVerseCard.kt implementiert; im Composition-API-Payload getriggert durch card.type = "card_hero" kombiniert mit card.objectType = "verse".

Iteration 1 — bewusste Lücke. Diese Spezifikation wurde noch nicht vollständig ausgearbeitet (kein detailliertes Anatomy-, Tokens-, States- oder Behavior-Layout). Iteration 1 war auf die HeroVideoCard-Variante zugespitzt; HeroBibleVerseCard erscheint in der Karussell-Rotation des Composition-Payloads, aber die Hand-off-Detaillierung für iOS/Web folgt in einer späteren Iteration.

Bekannt aus dem Code (kein vollständiger Vertrag):

  • Karten-Diskriminator: CardType.CARD_HERO (wire "card_hero") + ObjectType.VERSE (wire "verse"). CompositionModels.kt:94, 105.
  • Liest displayText (der Versinhalt, nicht headlineText) und optional audioUrl für eine Vorlese-Funktion. CompositionModels.kt:80–81.
  • Teilt colors.{light,dark}-Behandlung mit HeroVideoCard — die Karte trägt eine eigene Palette pro Slide.
  • Hat im Allgemeinen kein imageUrl (Verse zeigen Typografie + Gradient statt eines Posters).

Was die Agentur in Iteration 1 tun sollte: die Hero-Karussell-Rotation für diese Karte als Platzhalter behandeln — das Karussell sollte den Slide weiterhin paginieren und farblich wechseln (auf Basis von colors), aber der interne Render kann ein neutrales Placeholder-Layout sein, bis die volle Spezifikation nachgeliefert wird.

Composition-API-Kontrakt

Ein Vers-Slide ist ein BibleVerseHeroItem (Diskriminator type="bible_verse"). Modell: PlaylistCollectionModels.kt:119. Im Leseplan-Showcase nicht enthalten (Komponente ist iteration-2-eingefroren).

FeldTypHinweise
idStringStabile Slide-Kennung.
type"bible_verse"Diskriminator → HeroBibleVerseCard.
verseTextStringVerstext; dynamische Schriftgröße nach Textlänge.
verseReferenceStringVersangabe ("Johannes 3,16"); zugleich Label des Sekundär-CTA.
imageUrlString?Hintergrundbild des Vers-Slides.
audioUrlString?Vorgesehen für Vorlese-Audio; vom Client aktuell nicht konsumiert.
gradientColorString?Hex-Farbe für den Gradient-Hintergrund.
accentColorString?Hex-Akzentfarbe der Vers-Typografie.
colorsCollectionExtractedColors?Palette wie bei allen Hero-Slides.
deepLinkString?Navigationsziel (optional).

HeroPosterCard — image-only Hero

Iteration 2 scope Hero variant

Eine bild-only Hero-Variante (Composition-Hero-Typ poster): ein full-bleed-Bild ohne Gradient-Scrim und ohne Text-Overlay. Der Titel erscheint nur als Fallback, wenn das Bild fehlt oder nicht geladen werden kann (analog zur Poster-Karte). Es trägt zwei Asset-URLs, damit der Renderer pro Formfaktor den passenden Zuschnitt wählt.

Aufbau

Innerhalb der gemeinsamen HeroCardShell (Eckenradius, Border, Schatten, Farb-Emission) liegt genau ein full-bleed-Bild, ContentScale.Crop. Es gibt keinen Gradient und keinen Text im Normalzustand. Bei imageLoadFailed || imageUrl.isEmpty() wird der Titel zentriert gezeichnet (im hero-eigenen Fallback-Font). Der Renderer wählt das Asset anhand des Formfaktors:

  • Mobile (Portrait): posterImageUrl (Fallback imageUrl), Imgix ar=2:3, w=600.
  • Tablet / breit (Landscape): coverImageUrl (Fallback imageUrl), Imgix ar=16:9, w=1200.

HeroPosterCard.kt:41–88 · Dispatch: HeroCarousel.kt:332 (ContentType.POSTER)

Mobile (Portrait) · Default + Fallback 2:3 Mobile · 16:9 Tablet · kein Gradient, kein Text-Overlay
Bild · 2:3 Portrait (kein Text / kein Gradient) Default — image-only Titel Fallback — Bild fehlt 1 2

Callout-Legende

  1. Bild — formfaktor-abhängiger ZuschnittMobile: posterImageUrl (2:3, w=600); Tablet/breit: coverImageUrl (16:9, w=1200); Fallback-Asset imageUrl. Kein Gradient-Scrim, kein Text im Normalzustand. In der gemeinsamen HeroCardShell (Eckenradius, Border, Schatten).HeroPosterCard.kt:54–63,65
  2. Fallback-TitelNur bei imageLoadFailed || imageUrl.isEmpty(): zentrierter Titel im cardHero.fallbackTitle*-Font.HeroPosterCard.kt:71–88

Composition-API-Vertrag

FeldTypHinweise
type"poster"Diskriminator (kotlinx @SerialName("poster")).
imagePosterUrlString?Portrait-Zuschnitt (2:3) für Mobile.
imageCoverUrlString?Landscape-/Cover-Zuschnitt (16:9) für Tablet.
imageUrlString?Fallback-Asset für beide Formfaktoren (Single-Asset-Rückwärtskompatibilität).
titleStringNur als Fallback-Text gerendert.
deepLinkString?Tap-Ziel.

Zustände

Default (Bild vorhanden)
Reines Bild, formfaktor-korrekter Zuschnitt. Kein Text, kein Gradient.
Bildfehler / leere URL
Zentrierter Titel im cardHero.fallbackTitle*-Font.

Referenz

Mobile · image-only poster hero (portrait)
Mobil · Hell · image-only Poster-Hero (Portrait-Zuschnitt)

Implementierungsreferenz

HeroPosterCard.kt · PosterHeroItem (PlaylistCollectionModels.kt) · cardHero.fallbackTitle* in MediathekTokenModels.kt

Composition-API-Kontrakt

Ein Poster-Slide ist ein PosterHeroItem (Diskriminator type="poster"). Modell: PlaylistCollectionModels.kt:157.

FeldTypHinweise
idStringStabile Slide-Kennung.
type"poster"Diskriminator → HeroPosterCard (bild-only, kein Gradient/Text-Overlay).
titleStringAnzeigetitel (Accessibility/Tracking; nicht im Bild gerendert).
imageUrlString?Einzel-Asset-Fallback — aktuell das einzige Bildfeld, das der Android-Client liest.
imagePosterUrlString? (nur BFF)Portrait-Asset (3:4) für Mobile. Wird vom BFF geliefert, vom Android-Modell aber noch nicht ingestiert — siehe Gap-Hinweis.
imageCoverUrlString? (nur BFF)Landscape-Asset (16:9) für Tablet. Gleiche Einschränkung.
colorsCollectionExtractedColors?Palette (im Showcase nicht gesetzt).
deepLinkString?Navigationsziel beim Tippen.

Bekannter Gap: Der Zwei-Asset-Vertrag (imagePosterUrl + imageCoverUrl, FR-006) ist BFF-seitig ausgeliefert; PosterHeroItem (PlaylistCollectionModels.kt:157) liest derzeit nur imageUrl. Vor Iteration-2-Abschluss muss der Client beide Felder ingestieren und orientierungsabhängig wählen.

HeroGenericCard

Iteration 2 scope Hero variant

Eine Hero-Variante (Composition-Hero-Typ generic) mit identischem Look wie die HeroVideoCard — full-bleed-Bild, token-definierter Gradient-Scrim, Eyebrow / Titel / Beschreibung, optionales Badge, Primary-CTA — deren CTA jedoch eine beliebige URL öffnet statt ein Video abzuspielen. Geplanter Einsatz: Bewerbung externer Ziele, z. B. eines WhatsApp-Kanals.

Aufbau

Visuell deckungsgleich mit der HeroVideoCard (Mobile Portrait + Tablet 3-Schichten-Landscape) — die Implementierung delegiert an HeroVideoCard und überschreibt nur das CTA-Leading-Icon. Zwei semantische Unterschiede:

  • CTA-Icon: Pfeil-nach-rechts (ArrowForward) statt Play-Glyphe — das Ziel ist ein Link, kein Video.
  • Ziel: die CTA / der Karten-Tap führt zur deepLink-URL. Ist diese eine http(s)-URL, öffnet der Client sie extern (System-Handler / Browser); In-App-Deeplinks (video/…, series/…, playlist/…) routen wie gewohnt.

HeroGenericCard.kt · HeroVideoCard.kt (ctaLeadingIcon-Parameter) · Externes Routing: CollectionRenderer.kt (onSlideClick) · Dispatch: HeroMobileLayout.kt / HeroTabletLayout.kt (ContentType.GENERIC)

Mobile (Portrait) Identischer Aufbau wie HeroVideoCard · CTA-Icon [arrow_forward]
ANZEIGE Eyebrow Titel [arrow_forward] CTA 1 2 3 4 5

Callout-Legende

  1. Bild + Gradient-ScrimIdentisch zur HeroVideoCard: full-bleed-Bild in der HeroCardShell + token-definierter unterer Verlauf.HeroGenericCard.kt · HeroVideoCard.kt
  2. Badge (optional)Aus category (z. B. „Anzeige") · BtvBadge, oben-links im Padding.HeroVideoCard.kt (BtvBadge)
  3. EyebrowAus subtitle · einzeilig.RegularHeroContent · HeroSharedContent.kt
  4. Titel + BeschreibungAus title / description · adaptive Titelzeilen wie HeroVideoCard.HeroSharedContent.kt
  5. Primary-CTA — [arrow_forward], nicht [play_arrow]Beschriftung aus ctaLabel · Leading-Icon Icons.AutoMirrored.Filled.ArrowForward (Link, kein Video). Tap == Karten-Tap: beide rufen denselben onClick → öffnet deepLink (http(s) extern). Ein Secondary-CTA hätte eine eigene Aktion.HeroGenericCard.kt · HeroVideoCard.kt (onClick) · CollectionRenderer.kt (openExternalUrl)

Composition-API-Vertrag

FeldTypHinweise
type"generic"Diskriminator (kotlinx @SerialName("generic")).
titleStringHeadline.
subtitleString?Eyebrow über der Headline.
descriptionString?Beschreibungstext.
categoryString?Badge (z. B. „Anzeige").
ctaLabelString?Button-Beschriftung (z. B. „WhatsApp-Kanal beitreten").
deepLinkString?Die Ziel-URL. http(s) → öffnet extern.
imageUrlString?Hintergrundbild.

Zustände

Default
Wie HeroVideoCard: Bild + Gradient-Scrim + Eyebrow/Titel/Beschreibung + Primary-CTA (mit ArrowForward-Icon) + optionalem Badge.
Gedrückt (CTA / Karte)
Öffnet deepLink; bei http(s) extern über den System-Handler.

Referenz

Mobile · generic hero — Anzeige badge, WhatsApp CTA with forward arrow
Mobil · Hell · Generic-Hero („Anzeige" · CTA „WhatsApp-Kanal beitreten" mit [arrow_forward])

Implementierungsreferenz

HeroGenericCard.kt · HeroVideoCard.kt · CollectionRenderer.kt (openExternalUrl) · GenericHeroItem (PlaylistCollectionModels.kt) · ContentType.GENERIC (VisualParityEnums.kt)

Composition-API-Kontrakt

Ein Generic-Slide ist ein GenericHeroItem (Diskriminator type="generic"). Modell: PlaylistCollectionModels.kt:203.

FeldTypHinweise
idStringStabile Slide-Kennung.
type"generic"Diskriminator → HeroGenericCard (Video-Look, beliebiges CTA-Ziel).
titleStringTitel ("Bibel TV jetzt auf WhatsApp").
subtitleString?Sekundäre Headline.
descriptionString?Beschreibungstext unter dem Titel.
categoryString?Badge-Label ("Anzeige").
ctaLabelString?Text des Primär-CTA ("WhatsApp-Kanal beitreten"); Icon: Pfeil-nach-rechts statt Play.
imageUrlString?Hero-Bild.
colorsCollectionExtractedColors?Palette (optional).
deepLinkString?CTA-Ziel; http(s)-URLs öffnen extern (Browser/App), alles andere navigiert intern.

ButtonGroupSection

Umfang Iteration 1 Section Adaptiv

Vollbreiter Abschnitt, der eine horizontale Reihe von Navigationsaktionen unmittelbar unter dem HeroCarousel rendert. Zwei Render-Modi werden ausschließlich aus der Composition-Payload dispatched: ein Multi-Item-Modus (eine horizontale Reihe kompakter sekundärer Pillen mit einem messungsbasierten "Mehr"-Overflow-Trigger) und ein Single-Item-Modus (ein einzelner vollbreiter primärer CTA — meist "Die Bibel erkunden", aber auch die KI-Verlaufs-Pille "Bibelaufschlag mit KI"). Der Abschnitt ist die primäre Navigations-Übergabe des Home-Bildschirms nach dem Hero.

Visuelle Spezifikation

Aufbau

Vollbreiter Call-to-Action-Streifen, gerendert zwischen dem Hero und dem ersten Row-Abschnitt auf dem Home-Bildschirm. Zwei Render-Modi: einzelner primärer CTA (der häufige Fall — z. B. "Die Bibel erkunden" mit KI-Verlauf) oder eine Flex-Reihe sekundärer Chips mit einem Overflow-"Mehr"-Dropdown, wenn sie nicht passen.

Single-Item-Modus — vollbreiter primärer CTA items.size == 1 · heute auf Home am häufigsten ("Die Bibel erkunden")
Section-Container · horizontal gepolstert durch DesignTokens.Spacing.pageX [auto_awesome] Die Bibel erkunden Seitenrand-Padding 1 2 3

Single-Item Callout-Legende

  1. Section-ContainerVollbreit · horizontales Padding DesignTokens.Spacing.pageX auf beiden SeitenButtonGroupSection.kt:79
  2. Primärer CTA — vollbreitEntweder ProtoBibleButton mit KI-Verlauf (wenn item.style == "button_bibelaufschlag") oder MediathekButton Primary Medium. Vorangestelltes Icon ist für die KI-Variante standardmäßig AutoAwesome.ButtonGroupSection.kt:75–106
  3. Seitenrand-PaddingDesignTokens.Spacing.pageX — das globale Home-Content-Padding · 16dp Mobile / 32dp Tablet · entspricht views.home.layout.contentPaddingHorizontal{Mobile,Tablet}ButtonGroupSection.kt:79, 93
Default-Modus — mehrere Chips, alle passen items.size > 1 · alle Chips schmaler als verfügbare Breite · "Mehr" nicht angezeigt
Section-Container · pageX horizontales Padding Bibel Live-Streams Lesepläne Podcasts kein Overflow-Trigger, wenn alle passen SubcomposeLayout führt dennoch beide Durchläufe aus — Durchlauf 2 ermittelt einfach visibleCount == items.size 1 2

Default-Modus Callout-Legende

  1. Alle Chips — Secondary Small ButtonsGleiches Rendering pro Chip wie im Multi-Item-Overflow-Modus (MediathekButton Secondary Small mit Per-Item-Leading-/Trailing-Icons). Inter-Chip-Abstand aus DesignTokens.Spacing.gapMd.ButtonGroupSection.kt:152–165
  2. "Mehr" weggelassencalculateOverflow() liefert visibleCount = items.size, wenn die Summe der gemessenen Chip-Breiten + gap × (count − 1)availableWidth ist. Das "Mehr"-Composable wird einfach nicht platziert. Keine Dropdown-Affordance, kein Chevron.ButtonGroupSection.kt:33–62 (calculateOverflow) · 115–135 (SubcomposeLayout)
Multi-Item-Modus — Flex-Chips + "Mehr"-Overflow items.size > 1 · Chips überlaufen verfügbare Breite · "Mehr" erscheint · SubcomposeLayout Zwei-Pass-Messung
Bibel Live-Streams Lesepläne Mehr [keyboard_arrow_down] …ausgeblendete Chips werden in "Mehr" zusammengefasst SubcomposeLayout: Durchlauf 1 misst alle Chips + "Mehr"; Durchlauf 2 wählt sichtbare Anzahl via calculateOverflow() 1 2 3

Multi-Item Callout-Legende

  1. Sichtbare Chips — Secondary Small ButtonsMediathekButton Secondary Small mit Leading-/Trailing-Icons aus dem Item · spacedBy DesignTokens.Spacing.gapMd zwischen ChipsButtonGroupSection.kt:152–165
  2. "Mehr"-Overflow-DropdownErscheint nur, wenn visibleCount < items.size. Nachgestelltes Icon KeyboardArrowDown. Ausgeblendete Chips werden bei Tippen als DropdownMenuItems in einem DropdownContainer gerendert.ButtonGroupSection.kt:138–144 (Mehr) und darunter für Dropdown-Rendering
  3. Zwei-Pass-Messung (SubcomposeLayout)Durchlauf 1: jedes Item UND den "Mehr"-Button bei ihren bevorzugten Breiten messen. Durchlauf 2: calculateOverflow(itemWidths, availableWidth, moreWidth, gap) liefert den visibleCount zurück, der ohne Überlauf passt. Items jenseits von visibleCount landen im Dropdown.ButtonGroupSection.kt:33–62 (calculateOverflow) · 115–135 (SubcomposeLayout)

Verwendete Button-Varianten (Querverweis)

ButtonGroupSection definiert kein eigenes Button-Styling. Sie komponiert existierende Design-System-Buttons (ProtoBibleButton / MediathekButton) und wählt nur bestimmte Varianten aus. Die vollständige Button-Spezifikation — Größen, Typen, Funktionen, Farbschemata, Hit-Target-Aufpolsterung, Press-Feedback, Light-/Dark-Theming — befindet sich im Abschnitt ProtoBibleButton. Diese Tabelle listet nur die Varianten auf, die dieser Abschnitt instanziiert.

ModusComposableButtonTypeButtonSizeButtonFunctionBreite
Single — KI ("Bibelaufschlag")ProtoBibleButtonPrimaryMedium (40dp)AI (Verlauf-Füllung)fillMaxWidth()
Single — regulärer CTAMediathekButtonPrimaryMedium (40dp)DefaultfillMaxWidth()
Default / Multi — jeder ChipMediathekButtonSecondarySmall (32dp)Defaultintrinsisch (wrap content)
"Mehr"-Overflow-TriggerMediathekButtonSecondarySmall (32dp)Defaultintrinsisch

Branch-Auswahl: item.style == "button_bibelaufschlag" → KI-Variante; items.size == 1 & nicht KI → Single regulär; andernfalls → Chip-Reihe. Siehe ButtonGroupSection.kt:75–106.

Verwendete Icons

IconWoQuelle
MaterialSymbols.AutoAwesome (Sparkle, Material Symbols Rounded)Vorangestelltes Icon des Single-Item-KI-Buttons. Verwendet, wenn item.leadingIcon bei einem KI-Style-Item null ist — d. h. Default-Sparkle für die KI-Variante. Liefert die Payload ein anderes vorangestelltes Icon, wird stattdessen dieses verwendet.ButtonGroupSection.kt (Single-Item KI-Branch)
MaterialSymbols.KeyboardArrowDown (Down-Chevron, Material Symbols Rounded)Nachgestelltes Icon des "Mehr"-Overflow-Buttons — signalisiert Dropdown-Affordance. Fest codiert am "Mehr"-Button, nicht Payload-gesteuert.ButtonGroupSection.kt:138–144
Per-Item-Icons (Payload-gesteuert)Jedes NavigationButtonItem kann optionale leadingIcon / trailingIcon aus der Composition-Payload tragen. Wird auf dem entsprechenden Chip sowohl im Default- als auch im Overflow-Modus gerendert.NavigationButtonItem-Modell

Light- vs. Dark-Modus

ButtonGroupSection selbst hat keine Light-/Dark-Verzweigung — sie reicht alles an die Buttons durch. Sämtliches Theming (Hintergrundfüllungen, Label-Farben, KI-Verlauf-Stops, Rahmenfarben, Press-State-Tints, Chevron-Farbe) wird innerhalb von ProtoBibleButton / MediathekButton aufgelöst, indem aus LocalSemanticColors.current und den component.{mode}.protoBibleButton.*-Token-Branches gelesen wird. Implementierer sollten verifizieren, dass die Button-Komponente in beiden Schemata korrekt rendert; der darüberliegende Abschnitt benötigt keine separaten Light-/Dark-Wireframes.

Abmessungen

EigenschaftWertTokenQuelle
Seitenrand-Padding16dp Mobile / 32dp TabletDesignTokens.Spacing.pageX (auch views.home.layout.contentPaddingHorizontal{Mobile,Tablet})ButtonGroupSection.kt:79, 93
Inter-Chip-AbstandTokenDesignTokens.Spacing.gapMdButtonGroupSection.kt:107
ButtonGroupTokens.spacingTokencomponent.{mode}.buttonGroup.spacingMediathekTokenModels.kt
ButtonGroupTokens.cornerRadiusTokencomponent.{mode}.buttonGroup.cornerRadiusMediathekTokenModels.kt
ButtonGroupTokens.heightTokencomponent.{mode}.buttonGroup.heightMediathekTokenModels.kt
Primary-Button (Single-Modus)Medium-Größecomponent.protoBibleButton.primary.* in Medium-SkalaProtoBibleButton.kt
Sekundärer Chip (Multi-Modus)Small-Größecomponent.{mode}.protoBibleButton.secondary.* in Small-SkalaProtoBibleButton.kt

Zustände

Empty
items.isEmpty() → frühzeitige Rückkehr, nichts gerendert. Abschnitt im Layout unsichtbar.
Single-Item
Vollbreiter Primary-Medium-Button. KI-Verlauf-Variante, wenn der Style-Hinweis passt.
Default (Multi-Item, alle passen)
Alle Chips in einer Reihe sichtbar. calculateOverflow liefert visibleCount == items.size. "Mehr" nicht gerendert, kein Chevron, keine Dropdown-Affordance.
Multi-Item (überläuft)
Erste N Chips sichtbar (berechnet durch calculateOverflow), "Mehr"-Trailing-Chip mit KeyboardArrowDown-Chevron öffnet die übrigen in einem Dropdown.
Dropdown geöffnet
DropdownContainer erscheint unter "Mehr" mit den ausgeblendeten Items als DropdownMenuItems. Wichtig: Das Dropdown ist kein natives Material-Default-Dropdown — es ist das projekteigene DropdownContainer-Primitiv (Material 3 DropdownMenu mit Token-gesteuerten Overrides, siehe gestyltes Dropdown unten).
Chip gedrückt
Standard-ProtoBibleButton-Press-Feedback bei opacity.scale15-Tint.

Verhalten

  • Items-Quelle — servergesteuert via der NavigationButtonItem-Liste in der Composition-Payload. Jedes Item hat label, destination (Deep-Link-Route), optionale leadingIcon / trailingIcon, optionalen style-Hinweis.
  • KI-Verlauf-Routing — wenn item.style == "button_bibelaufschlag", verwendet der Single-Item-Branch ProtoBibleButton mit function = ButtonFunction.AI und gibt ihm die Accent-→-Primary-Verlauf-Füllung statt des Standard-Solid-Hintergrunds. Der Verlauf wird unter einem festen Winkel von semantic.ai.gradient.angleDeg = 135° (CSS-Konvention: oben-links → unten-rechts) gerendert — unabhängig vom Seitenverhältnis des Buttons. Wichtig (Android-Implementierungsdetail): die Standard-Compose-Brush.linearGradient(colors) verwendet Offset.Zero → Offset.Infinite, was den Verlauf entlang der Diagonale der Bounding-Box zieht. Auf einem breiten Single-Item-CTA (≈360 × 40dp) flacht das auf ~6° ab und wirkt fast horizontal. Wir verwenden stattdessen die projekteigene AngledLinearGradient ShaderBrush (ProtoBibleButton.kt:182–215), die die Gradient-Linie aus dem Mittelpunkt der Bounding-Kreis-Diagonale berechnet und so den 135°-Winkel konstant hält. iOS: LinearGradient(gradient: Gradient(colors: [...]), startPoint: .topLeading, endPoint: .bottomTrailing). Web: background: linear-gradient(135deg, …).
  • Tipp (regulärer Button) — ruft onNavigate(item.destination) auf. Routing wird vom übergeordneten Bildschirm gehandhabt.
  • Tipp (AI-Button) — verbindlicher Vertrag — Der AI-Button (jeder Button mit function = ButtonFunction.AI) MUSS auf Komponenten-Ebene so verdrahtet sein, dass er die KI-Bibelaufschlag-Modal öffnet. Diese Modal existiert bereits in der Produktions-App und ist NICHT Teil des Iteration-1-Lieferumfangs zum Neubau — die Agentur erbt sie. Wichtig: Im aktuellen Android-Quellcode ist onClick des AI-Buttons in ButtonGroupSection.kt:77 noch nicht mit der Modal verdrahtet (delegiert generisch an onNavigate(item.destination)). Das ist ein bekannter Implementierungs-Gap, der vor dem Iteration-1-Launch geschlossen werden muss. Vertrag für iOS/Web: der AI-Button-Klick MUSS — unabhängig vom item.destination-Wert in der Payload — die KI-Bibelaufschlag-Modal aufrufen.
  • Layout-Recompose — Overflow wird bei jeder Constraint-Änderung neu berechnet (Orientierung, Multi-Window-Resize), sodass Chips elegant umfließen.

Gestyltes "Mehr"-Dropdown

Das "Mehr"-Overflow-Dropdown ist nicht das native Material-Default-Dropdown. Es ist das projekteigene DropdownContainer-Primitiv (packages/android/.../ui/components/primitives/BtvDropdown.kt:305–340), das die Material-3-DropdownMenu-Struktur mit Token-gesteuerten Overrides verwendet. Die folgenden Eigenschaften sind explizit überschrieben (alle anderen Material-Defaults bleiben in Kraft):

EigenschaftWertQuelle
containerColorsemanticColors.background.canvas × semantic.effect.backdropBlur.tintAlphaMobile (0.85) — gleicher Frosted-Glass-Tint wie der MenuDrawer-PanelBtvDropdown.kt:316–322
shadowElevationThemeDesignTokens.spacing.gap2xsBtvDropdown.kt:332
shape (Ecken-Radius)RoundedCornerShape(ThemeDesignTokens.radius.md)BtvDropdown.kt:333
minWidthtokens.btvDropdown.minWidth (komponentenspezifisches Token)BtvDropdown.kt:335 · component.btvDropdown.minWidth
Trigger-ButtonMediathekButton mit type = ButtonType.Secondary, trailingIcon = MaterialSymbols.KeyboardArrowDownBtvDropdown.kt:319–326
Menü-ItemsStandard DropdownMenuItems, deren Text/Icon-Farben über MenuItemColors mit semantischen Tokens überschrieben werden (siehe BtvDropdown.kt:290–298 für den Pattern in den anderen BtvDropdown-Varianten)BtvDropdown.kt

iOS / Web Implementierungshinweis: bei Reimplementierung MUSS das Dropdown denselben visuellen Vertrag erfüllen — angehobene Surface-Hintergrundfarbe (nicht system-default), Token-gesteuerter Ecken-Radius (mittlerer Radius statt nativem System-Radius), explizite Mindestbreite, und ein Schatten in Token-Höhe. Die Material-3-Native-Defaults für DropdownMenu (System-Hintergrund, eckige/scharfe Ecken auf älteren Systemen, kein Mindestbreite-Constraint) reichen NICHT aus — sie würden im Light-Mode mit weißem System-Hintergrund auf weißer Page-Surface verschmelzen.

Visuell — Dropdown geöffnet
Mehr Gottesdienste TV Programm Musik Kinder Dokumentationen Dark · surfaceRaised = #252F35 · radius.md · drop shadow Mehr Gottesdienste TV Programm Musik Kinder Dokumentationen Light · surfaceRaised = White · radius.md · weicher Schatten

Schematische Wireframes — die Schatten- und Border-Werte verifizieren beim Implementieren gegen BtvDropdown.kt:331–334 und semantic.{mode}.background.surfaceRaised. Die Trennlinien zwischen Menü-Items kommen vom Material-3-DropdownMenu-Default (subtile onSurface-Variante).

Barrierefreiheit

  • Single-Item-Branch: native Button-Semantik (role button, Label = item.label).
  • Multi-Item-Branch: jeder Chip ist ein separater button; "Mehr" ist ebenfalls button, erweiterbar (aria-expanded entsprechend Web).
  • Dropdown-Items: jedes DropdownMenuItem ist ein separater Fokusstop; Material 3 handhabt Tastatur-Navigation (Pfeiltasten hoch/runter) standardmäßig.
  • Hit-Targets ≥ 48dp über die Standard-Aufpolsterung der Button-Komponenten.

Audit-Notizen

Keine Regressionen festgestellt. Zwei-Pass-SubcomposeLayout-Messung ist korrekt; calculateOverflow ist eine reine Funktion mit sinnvollen Randfällen (empty, single-item, full-fit, partial-fit).

Beobachtung zu Iteration 1: Home rendert heute nur eine Single-Item-ButtonGroupSection ("Die Bibel erkunden" mit KI-Verlauf). Der Multi-Item-Branch ist vollständig implementiert, wird aber derzeit von der produktiven Composition-Payload nicht ausgeübt. Implementierer sollten beide Branches funktionsfähig halten.

Zustände

Multi-Item-Pillen (pro Item, secondary Mediathek-Schema):

Default
Hintergrund = component.{mode}.button.secondary.bg (transparent-tintiert auf Dark; weiche Oberfläche auf Light). Rahmen 1dp = component.{mode}.button.secondary.border. Label = component.{mode}.button.secondary.text.
Pressed
Hintergrund = component.{mode}.button.secondary.bgPressed. Ein Plattform-Ripple in der Inhaltsfarbe wird darüber gelegt. Kehrt beim Loslassen zu Default zurück.
Focused
Plattform-Fokusring (semantic.focusRing.{bp}.*) außerhalb des Pillen-Rahmens gezeichnet, keine Formänderung.
Disabled
Hintergrund = component.{mode}.button.secondary.bgDisabled. Label = component.{mode}.button.secondary.textDisabled. Touch- und Tastaturereignisse werden ignoriert. Effektives Alpha folgt component.{mode}.protoBibleButton.disabledAlpha = 0.38.

Mehr-Trigger:

Default
Gleiches Chrome wie eine sichtbare Pille. Trailing-Chevron zeigt nach unten.
Expanded
Trigger bleibt in der Press-Feedback-Farbe, solange das Dropdown geöffnet ist. Das Chevron ist für beide Zustände als nach unten zeigend dokumentiert (keine Drehung in Iteration 1).

Mehr-Dropdown-Zeilen:

Default
Hintergrund = Surface-Raised. Label = semantic.{mode}.text.primary. Typografie = Brand-Body.
Hover (Pointer-Plattformen)
Hintergrund wird um component.{mode}.protoBibleButton.hoverAlpha = 0.08 aufgehellt.
Pressed
Hintergrund wird um component.{mode}.protoBibleButton.pressedAlpha = 0.12 aufgehellt.

Single-Item-CTA — Default-Variante (Primary, Mediathek-Schema):

Default
Solider Hintergrund = component.{mode}.button.primary.bg. Label = component.{mode}.button.primary.text. Vorangestelltes Icon (falls vorhanden) auf Labelfarbe getöntet.
Pressed
Hintergrund = component.{mode}.button.primary.bgPressed.
Focused
Plattform-Fokusring außerhalb der Pille.
Disabled
Hintergrund = component.{mode}.button.primary.bgDisabled; Label = component.{mode}.button.primary.textDisabled.

Single-Item-CTA — KI-Variante:

Default
Hintergrund ersetzt durch einen linearen Verlauf von semantic.{mode}.ai.gradient.defaultStart nach semantic.{mode}.ai.gradient.defaultEnd (links → rechts gelesen; bei RTL spiegelt sich der Verlauf mit dem Layout). Label und Icon = semantic.{mode}.ai.textOnBackground.default.
Pressed
Verlauf unverändert, der Inhaltsfarbe-Ripple der Plattform überlagert sich an der Gestenposition.
Disabled
Verlauf getauscht auf semantic.{mode}.ai.gradient.disabledStartsemantic.{mode}.ai.gradient.disabledEnd. Label = semantic.{mode}.ai.textOnBackground.disabled.

Verhalten

Modus-Dispatch (Composition-gesteuert)

Der Renderer liest items: NavigationButtonItem[] aus der Page-Composition-Payload. items.length === 0 → nichts rendern (der Abschnitt wird vom Bildschirm ausgelassen). items.length === 1 → Single-Item-Modus, wobei die KI-Variante genau dann ausgewählt wird, wenn items[0].style === "button_bibelaufschlag". items.length ≥ 2 → Multi-Item-Modus.

Overflow-Messung (Multi-Item)

Der Track muss immer auf eine einzelne Zeile passen — horizontales Scrollen ist explizit KEIN Fallback. Die Messung läuft in zwei Durchläufen:

  1. Jedes Payload-Item mit seinem realen Label + Icons subcomposen; die intrinsische Breite jedes Items erfassen.
  2. Den Mehr-Trigger subcomposen; seine intrinsische Breite erfassen.

Dann calculateOverflow(itemWidths, availableWidth, moreWidth, gap) ausführen: sei availableWidth = containerWidth − 2 × rowHorizontalPadding; wenn alle Items + Gaps in availableWidth passen, items.length zurückgeben (kein Mehr); andernfalls iterativ die längste führende Präfixlänge probieren, bei der Präfix-Breite + Gap + Mehr-Breite noch in availableWidth passt; diese Präfixlänge zurückgeben (Minimum 1). Items jenseits des Präfixes werden in das Dropdown sortiert.

Eine erneute Messung läuft, wenn sich die Container-Breite ändert (Rotation, Split-Screen-Resize, Wechsel der Foldable-Posture) oder die Items-Liste sich ändert.

Mehr öffnen/schließen

Tippen auf den Mehr-Trigger öffnet das Dropdown verankert unter ihm. Tippen außerhalb, Drücken von Escape, Auswählen einer Zeile oder Scrollen der Seite schließt es. Das Auswählen einer Zeile ruft sowohl onNavigate(item.destination) auf als auch schließt das Dropdown.

Item-Auswahl

Tippen auf eine beliebige sichtbare Pille, den Single-CTA oder eine Dropdown-Zeile dispatched onNavigate(destination). Ziele sind Deep-Link-Strings; das Routing liegt in der Verantwortung des Host-Bildschirms — diese Komponente navigiert nicht selbst.

Empty / Loading / Error

Der Abschnitt hat kein Skeleton — er wird erst eingehängt, wenn seine Payload aufgelöst ist. Wenn der übergeordnete Bildschirm noch andere Zeilen lädt, kann dieser Abschnitt normal rendern, sobald items verfügbar ist. Es gibt keinen Error-State für diesen Abschnitt isoliert; wenn der Page-Composition-Fetch fehlschlägt, rendert die gesamte Home-Seite stattdessen den globalen Fehlerblock.

Motion

AuslöserEigenschaftDauerEasing
Pille Press-InHintergrund-Opazität (Ripple)120mscubic-bezier(0.0, 0.0, 0.2, 1) (ease-out)
Pille Press-OutHintergrund-Opazität (Ripple)200mscubic-bezier(0.4, 0.0, 0.2, 1) (standard)
Mehr-Dropdown öffnenOpazität 0 → 1, Skalierung 0.96 → 1 vom Anker180mscubic-bezier(0.0, 0.0, 0.2, 1)
Mehr-Dropdown schließenOpazität 1 → 0, Skalierung 1 → 0.96120mscubic-bezier(0.4, 0.0, 1, 1) (ease-in)
Track-Neumessung bei ResizeCrossfade der sichtbaren Anzahl150mslinear
Single-CTA Größenänderung (z. B. Label-Wechsel)BreiteSpring (medium-bouncy, medium stiffness)Spring (kein Cubic-Bezier-Äquivalent)

Interaktion & UX

  • Der Track scrollt nicht horizontal. Wenn Items wirklich nicht auf ein einziges sichtbares Item plus Mehr reduziert werden können (extrem schmale Breiten), sollte die Plattform dennoch mindestens das erste Item plus Mehr rendern; niemals ein Label mitten in einem Glyphen abschneiden.
  • Zweimaliges Drücken derselben Pille ist idempotent — kein visueller Lock-Zustand, der Bildschirm leitet erneut weiter.
  • Long-Press hat in Iteration 1 kein definiertes Verhalten; es darf kein Kontextmenü erzeugen.
  • Die Single-CTA-Variante muss die verfügbare Breite einschließlich des Einzugs visuell ausfüllen; keine kleinere Pille innerhalb des Streifens zentrieren.
  • Dropdown-Zeilen zeigen in Iteration 1 keine Icons, auch wenn das Quell-Item ein vorangestelltes Icon hatte — das Dropdown ist nur Text.

Barrierefreiheit

  • Pillen · role = button · zugänglicher Name = Pillen-Label · deutsche Labels werden wortgetreu beibehalten ("Filme", "Serien", …).
  • Mehr-Trigger · role = button · zugänglicher Name = "Mehr" · Barrierefreiheitszustand = expanded / collapsed · kündigt Zustandsänderungen an.
  • Mehr-Dropdown · role = menu · Zeilen haben role = menuitem. Fokus bewegt sich beim Öffnen zur ersten Zeile und kehrt beim Schließen zum Trigger zurück. Escape schließt.
  • Single-CTA · role = button · zugänglicher Name = Label ("Die Bibel erkunden" / "Bibelaufschlag mit KI"). Das vorangestellte Icon ist dekorativ — kein separates Label.
  • Fokus-Reihenfolge auf dem Streifen: erste sichtbare Pille → … → letzte sichtbare Pille → Mehr (falls vorhanden). Das Dropdown nimmt nur dann an der Fokus-Reihenfolge teil, wenn es geöffnet ist.
  • Hit-Targets sind auf jedem interaktiven Element ≥ 48dp; Gesten-Aufpolsterung ist transparent und reicht nicht in Geschwister hinein.
  • Reduzierte Bewegung: Dropdown-Öffnen/Schließen verzichtet auf die Skalierungs-Transformation und verwendet nur Opazität (180ms / 120ms). Spring beim Single-CTA wird auf ein 150ms-Tween reduziert.
  • Farbkontrast: Der KI-Verlauf muss Label + Icon bei ≥ 4.5:1 Kontrast gegen beide Verlaufs-Endpunkte halten; Iteration 1 liefert Verläufe aus, die dies für das kanonische Label "Bibelaufschlag mit KI" erfüllen.

Genutzte Tokens

PfadTier
component.{mode}.buttonGroup.spacingcomponent
component.{mode}.buttonGroup.cornerRadiuscomponent
component.{mode}.buttonGroup.heightcomponent
component.{mode}.section.verticalSpacingcomponent
component.{mode}.section.horizontalPaddingcomponent
component.{mode}.playlist.rowHorizontalPaddingcomponent
component.{mode}.protoBibleButton.borderWidthcomponent
component.{mode}.protoBibleButton.pressedAlphacomponent
component.{mode}.protoBibleButton.hoverAlphacomponent
component.{mode}.protoBibleButton.disabledAlphacomponent
component.{mode}.button.primary.bg / bgPressed / bgDisabledcomponent
component.{mode}.button.primary.text / textDisabledcomponent
component.{mode}.button.secondary.bg / bgPressed / bgDisabled / bordercomponent
component.{mode}.button.secondary.text / textDisabledcomponent
component.{mode}.button.medium.height / paddingX / paddingY / fontSize / iconSizecomponent
component.{mode}.button.small.height / paddingX / paddingY / fontSize / iconSizecomponent
component.{mode}.btvDropdown.minWidthcomponent
semantic.{mode}.ai.gradient.defaultStart / defaultEndsemantic
semantic.{mode}.ai.gradient.disabledStart / disabledEndsemantic
semantic.{mode}.ai.textOnBackground.default / disabledsemantic
semantic.{mode}.text.primarysemantic
semantic.focusRing.{bp}.*semantic

Referenz

ButtonGroupSection mobile
Mobile · ButtonGroupSection — primärer CTA "Die Bibel erkunden"

Implementierungsreferenz

Android · packages/android/app/src/main/java/com/protobible/android/ui/components/sections/ButtonGroupSection.kt (Renderer + calculateOverflow) · packages/android/app/src/main/java/com/protobible/android/ui/components/ProtoBibleButton.kt (MediathekButton, ProtoBibleButton, AI-Gradient-Branch). Die Zwei-Pass-Messung ist mit SubcomposeLayout umgesetzt; das Dropdown wird an das projekteigene DropdownContainer-Primitiv delegiert.
iOS · ausstehend. Vorgeschlagener Ansatz: ein HStack, eingebettet in ViewThatFits, das eine Reihe progressiv kleinerer Item-Anzahl-Teilmengen durchprobiert und auf die kleinste zurückfällt, die den Mehr-Trigger enthält. Das Dropdown ist ein Menu (oder ein benutzerdefiniertes Popover zur Parität mit dem Offset des Dropdowns).
Web · ausstehend. Vorgeschlagener Ansatz: eine Flex-Zeile mit einem ResizeObserver am Container, die die offsetWidth jedes Kindes nach einem Layout-Pass misst; verlassen Sie sich nicht auf flex-wrap (das würde die Single-Line-Beschränkung brechen). Das Mehr-Dropdown ist ein an einer Schaltfläche verankertes Popover mit Menü-Semantik.
Anti-Pattern. Implementieren Sie den Multi-Item-Modus nicht als horizontal scrollenden Chip-Strip; das Design tauscht bewusst Laufzeitkosten (Messung) gegen ein stabiles, vollständig sichtbares Aktions-Set.

Composition-API-Vertrag

ButtonGroupSection rendert die Komponente BUTTON_GROUP (oder den Legacy-Alias GROUP_BUTTON). Die Item-Liste stammt aus component.buttons; der Renderer entscheidet anhand der Listenlänge und des optionalen style-Hinweises an jedem Item zwischen Single-Item- und Multi-Item-Modus.

Feld an CompositionComponent / CompositionButtonGroupItemTyp (laut CompositionModels.kt)Hinweise
type ComponentType.BUTTON_GROUP oder GROUP_BUTTON → Wire-Werte "button_group" / "group_button" Beide Wire-Werte werden an denselben Renderer geleitet (CompositionAdapter.kt:14–17). GROUP_BUTTON ist ein Legacy-Alias, der aus Gründen der Abwärtskompatibilität beibehalten wird — neue Payloads sollten "button_group" verwenden. CompositionModels.kt:60–61.
headlineText String? Optionaler Abschnittstitel über der Reihe. Die meisten Home-Payloads lassen ihn weg (der Single-AI-CTA-Fall hat keinen Titel).
buttons List<CompositionButtonGroupItem> Erforderlich. Leere Liste → Abschnitt wird vollständig übersprungen (CompositionAdapter.kt:28). Ein Item → Branch mit vollbreitem primärem CTA; mehr als ein Item → Flex-Row + Overflow-Branch.
buttons[].displayText String? (fällt auf label zurück) Button-Label. Der Adapter unter CompositionAdapter.kt:34 liest displayText ?: label ?: "" — beide werden auf der Leitung akzeptiert, neue Payloads sollten jedoch displayText senden.
buttons[].style String? Stil-Hinweis. "button_bibelaufschlag" löst das AI-Gradient-Rendering für den Einzel-Item-Fall aus. Andere Werte werden als Daten an den Renderer weitergegeben; unbekannte Styles fallen auf das Standard-Rendering zurück. CompositionModels.kt:48.
buttons[].deepLink String? (fällt auf type zurück) Tap-Ziel. Der Adapter verwendet deepLink ?: type ?: "" unter CompositionAdapter.kt:35. Die Route wird vom Host-Bildschirm verarbeitet.
buttons[].leadingIcon / buttons[].trailingIcon String? Optionale Icon-Namen aus dem Material Symbols Rounded Set. An die Schaltfläche als MaterialSymbols.{Name} weitergegeben. Das Standard-Leading-Icon der AI-Variante (AutoAwesome) wird nur angewendet, wenn kein Icon angegeben ist.
buttons[].id / buttons[].type String? Legacy-Felder, beibehalten zur Abwärtskompatibilität — id für Tracking, type als Deep-Link-Fallback. Neue Payloads müssen sich darauf nicht verlassen.
tracking ComponentTracking? Impression- und Click-Events auf Komponentenebene. Click-Events pro Button werden heute auf Komponentenebene nicht separat erfasst; der Host-Bildschirm übernimmt die Deep-Link-Telemetrie.
cards List<CompositionCard> = emptyList() Im Wire-Modell für alle Komponenten vorhanden, von BUTTON_GROUP jedoch nicht verwendet.

Composition-API-Kontrakt

Sektion mit Diskriminator type="button_group". Modelle: ButtonGroupSection (PlaylistCollectionModels.kt:84), ButtonGroupItem (PlaylistCollectionModels.kt:285).

FeldTypHinweise
idStringSektions-Kennung.
type"button_group"Diskriminator → ButtonGroupSection.
titleString?Optionaler Sektionstitel (im Showcase nicht gesetzt).
buttonsList<ButtonGroupItem>Reihenfolge = Render-Reihenfolge. 1 Button mit AI-Style → ProtoBibleButton in voller Breite; >1 Buttons → Overflow in "Mehr"-Dropdown.

Felder pro ButtonGroupItem:

FeldTypHinweise
idStringButton-Kennung; client-seitig derzeit nicht weiterverwendet.
labelStringButton-Text.
deepLinkStringNavigationsziel.
styleString?"primary" / "secondary" / "button_bibelaufschlag" (AI-Button mit Gradient).
leadingIconString?Material-Symbol-Name (z. B. AutoAwesome).
trailingIconString?Material-Symbol-Name (z. B. KeyboardArrowDown).

RowSection

Iteration 1 scope Section Repeating

Die primäre Inhaltseinheit des Home-Bildschirms. Ein vertikaler Block, der aus einem optionalen Titel und einem horizontal scrollenden Track aus Karten besteht. Jede Instanz ist eine Playlist-Reihe; der Home-Bildschirm rendert eine unbestimmte, servergesteuerte Sequenz von ihnen zwischen der ButtonGroupSection und der BottomNavigation. Die Kartenvariante für die Items jeder Reihe (CardPoster · CardLandscape · OVERLAY · SPLIT · CardAvatar) wird ebenfalls über die Payload gesteuert — RowSection ist ein schlanker, slot-basierter Dispatcher.

Visuelle Spezifikation

Aufbau

Horizontal scrollender Reihen-Abschnitt. Trägt einen optionalen Titel über einer LazyRow aus Karten. In Iteration 1 ist die einzige im Scope befindliche Kartenvariante CardPoster; die Reihe kann auch andere Typen hosten (Landscape, Top10, Avatar, Split, Overlay), diese sind jedoch nicht im Umfang dieses Builds enthalten.

Mobile & Tablet — gleiche Struktur Horizontaler Scroll · standardmäßig 1,5 Karten auf dem Smartphone sichtbar
Abschnittstitel LazyRow · horizontaler Scroll · spacedBy(rowItemSpacing) Karte 1 Karte 2 Karte 3 Karte 4 Peek → Abstand Padding 1 2 3 4 5 6

Callout-Legende

  1. Abschnittstitel (optional)Wird nur gerendert, wenn section.title != null · Schriftfamilie Diagramm Display · Größe playlist.rowTitleFontSizeSp · Gewicht playlist.rowTitleFontWeight = Medium · Padding am Start durch playlist.rowHorizontalPadding, am unteren Rand durch playlist.rowTitleBottomSpacingRowSection.kt:69–80 · MediathekTokenModels.kt PlaylistTokens
  2. Reihen-Container — LazyRowLazyRow mit contentPadding = playlist.rowHorizontalPadding horizontal · Arrangement.spacedBy(playlist.rowItemSpacing) zwischen Items · freier Scroll (kein Snap)RowSection.kt:84–87
  3. Karten-Slot — servergesteuerter TypIn Iteration 1 ist nur POSTER_PORTRAIT (CardPoster) im Umfang. Die Reihe unterstützt zusätzlich IMAGE_ONLY_LANDSCAPE, SPLIT, OVERLAY, AVATAR — der Server bestimmt dies pro Reihe über section.cardType. Die Kartenbreite wird dynamisch berechnet (siehe Formel unten).RowSection.kt:88–95 · PlaylistCardDispatch
  4. Halb-Peek der nächsten Karte (Scroll-Hinweis)Die Smartphone-Breite wird so berechnet, dass pro Viewport ~1,5 Karten sichtbar sind. Die halbe Karte ist der Scroll-Hinweis — sie signalisiert dem Benutzer, dass es horizontal weitere Inhalte gibt. Kein Edge-Fade, kein Scrollbar.RowSection.kt:108–128 (getCardWidth Phone-Formel)
  5. Abstand zwischen Kartenplaylist.rowItemSpacing · 16dp Standard auf Mobile/Tablet · ebenfalls auf View-Ebene als views.home.layout.rowCardSpacing{Mobile,Tablet} verfügbarPlaylistTokens · views/home.json
  6. Reihen-Randpaddingplaylist.rowHorizontalPadding · derselbe Wert wird als contentPadding horizontal an der LazyRow angewendet · hält die erste/letzte Karte vom Viewport-Rand abgesetztRowSection.kt:85

Kartenbreiten-Formel (POSTER_PORTRAIT, nur im Scope)

ModusKartenbreiteFormel / Quelle
Mobile (Smartphone)dynamisch berechnet, damit ~1,5 Karten sichtbar sind(screenWidthDp − 2 × rowHorizontalPadding − 0.5 × rowItemSpacing) / 1.5
RowSection.kt:108–128
Tablet — Stufe Klein (S)cardPoster.widthSmall (Token)Statischer Tokenwert aus component.{mode}.cardPoster.widthSmall
Tablet — Stufe Mittel (M)cardPoster.widthMedium (Standard)Statischer Tokenwert
Tablet — Stufe Groß (L)cardPoster.widthLargeStatischer Tokenwert
MATCHED-Größenvariante (höhengetrieben)Höhe 300dp × aspectRatioMATCHED_HEIGHT × cardPoster.aspectRatio = 300 × 0.5625 = 168.75dp
RowSection.kt:97–106

Abmessungen

EigenschaftWertTokenQuelle
Titel-SchriftgrößeTokencomponent.{mode}.playlist.rowTitleFontSizeSpMediathekTokenModels.kt PlaylistTokens
Titel-SchriftstärkeMediumcomponent.playlist.rowTitleFontWeightMediathekTokenModels.kt PlaylistTokens
Abstand Titel zu ReiheTokencomponent.{mode}.playlist.rowTitleBottomSpacingMediathekTokenModels.kt
Horizontales Padding der ReiheTokencomponent.{mode}.playlist.rowHorizontalPaddingMediathekTokenModels.kt
Abstand zwischen KartenTokencomponent.{mode}.playlist.rowItemSpacing (auch views.home.layout.rowCardSpacing{Mobile,Tablet})MediathekTokenModels.kt · views/home.json

Zustände

Idle (geladen)
Titel sichtbar (falls vorhanden), Karten ausgelegt, ~1,5 Karten lugen am Smartphone hervor.
Scrolling (Benutzer-Drag)
Reihe folgt dem Finger horizontal · Gesten-Priorität beansprucht horizontale Swipes, bevor der übergeordnete vertikale Scroll sie konsumiert · kein Snap, kein Inertia-Overshoot.
Loading (Items noch nicht aufgelöst)
Titel wird gerendert. Item-Bereich rendert nichts (oder Skelett-Karten, falls der Parent welche liefert) — RowSection selbst zeigt kein Skelett; der Parent-Bildschirm entscheidet.
Leer
Wenn displayItems leer ist, rendert die Reihe nur den Titel (oder nichts, falls kein Titel). Keine "Leerzustand"-Meldung — der Parent-Bildschirm filtert leere Abschnitte bereits auf Composition-Ebene heraus.

Verhalten

  • Item-Reihenfolge — wenn section.shuffled = true, werden Items partitioniert: solche mit fixedPosition werden an ihrem Index fixiert, die übrigen werden gemischt und anschließend an den fixierten Indizes wieder eingefügt. Memoisiert via remember(section.items, section.shuffled).
  • Karten-DispatchPlaylistCardDispatch wählt den Renderer basierend auf cardType aus. Iteration 1 leitet nur an CardPoster (POSTER_PORTRAIT) weiter.
  • Gesten-Priorität — Composes LazyRow beansprucht horizontale Gesten beim Gestenstart; der vertikale Scroll konsumiert weiterhin vertikale Drags. Das bedeutet: Wenn ein Benutzer in der Nähe einer Reihe wischt, scrollt diese Reihe, nicht die Seite.

Barrierefreiheit

  • Die Reihe ist eine logische Gruppe; auf Screenreadern leitet der Titel (falls vorhanden) die Items ein.
  • Jede Karte ist unabhängig fokussierbar (ein Stopp pro Karte). Die Fokus-Reihenfolge folgt der visuellen Reihenfolge der Reihe.
  • Der horizontale Scroll ist per Tastatur-Pfeiltasten erreichbar (Compose-Standard).

Audit-Notizen

Keine Regressionen erkannt. Alle Abmessungen sind Token-gesteuert. Die "1,5 Karten sichtbar"-Formel ist eine bewusste Laufzeitberechnung gegen screenWidthDp, keine magische Zahl.

Hinweis für Implementierer: Die Konstante MATCHED_HEIGHT (300dp) innerhalb von getCardWidth ist ein hartcodiertes Literal im Quellcode — sie wäre ein Kandidat für Tokenisierung (component.playlist.matchedRowHeight), falls die MATCHED-Größenvariante in einer zukünftigen Iteration in den Scope kommt.RowSection.kt MATCHED_HEIGHT Konstante

Zustände

Default
Track bei Scroll-Offset 0 — erste Karte bündig mit dem führenden Inset, die letzte Karte kann außerhalb des Bildschirms liegen mit einem halb sichtbaren Nachbarn als Hinweis auf weiteren Inhalt.
Scrolling
Track verschiebt sich horizontal unter Finger-/Scrollrad-/Pfeiltasten-Eingabe. Vertikaler Seiten-Scroll wird während einer aktiven horizontalen Geste unterdrückt (siehe Interaktion).
Karte gedrückt
Jede Karte behandelt ihr eigenes Druck-Feedback (siehe CardPoster u. a.). RowSection selbst zeichnet keinen Druck-Zustand.
Karte fokussiert (Tastatur / D-Pad)
Die fokussierte Karte hebt ihren eigenen Fokusring hervor; der Track scrollt gerade so weit, dass die fokussierte Karte vollständig sichtbar ist (kein Über-Scrollen).
Loading
Wenn der Abschnitt vor dem Auflösen seiner Items gemountet wird, rendern Sie den Titel und eine Skelett-Reihe von kartenförmigen Platzhaltern in der konfigurierten Breite — Anzahl = bis zu ceil(viewport / cardWidth) + 1. Skelett-Linien pulsen im globalen Skelett-Takt.
Leer
Wenn die aufgelöste items-Liste nach Filterung leer ist (z. B. alle fixedPosition-Platzierungen entfernt), rendert der Abschnitt nichts — der übergeordnete Home-Bildschirm lässt die Lücke aus.

Verhalten

Item-Reihenfolge

Jede PlaylistRowSection trägt ein shuffled: boolean-Flag und eine Liste von Items, jeweils optional annotiert mit fixedPosition: number. Wenn shuffled = true: Mischen Sie die nicht fixierten Items und fügen Sie dann jedes fixierte Item an seiner fixedPosition wieder ein (geklammert auf [0, list.length]). Wenn false: Rendern in Payload-Reihenfolge. Die Misch-Stabilität gilt pro Mount — ein Re-Mount kann erneut mischen.

Dispatch der Kartenvariante

Für jedes Item wird effectiveCardType = item.cardTypeOverride ?? section.cardType berechnet. Rendern Sie die entsprechende Karten-Komponente in der abgeleiteten Breite:

  • POSTER_PORTRAITCardPoster
  • IMAGE_ONLY_LANDSCAPECardLandscape
  • OVERLAY → MediaCard mit style = OVERLAY (Bild + am unteren Rand ausgerichtete Overlay-Metadaten)
  • SPLIT → MediaCard mit style = SPLIT (Bild links, Metadaten rechts; Mobile = horizontaler Split)
  • AVATAR → CardAvatar (kreisförmig)

Für OVERLAY/SPLIT werden zusätzliche Payload-Felder (tagVariant, tag, contentType, duration, description, series, subtitle, metadata) unverändert durchgereicht. preExtractedColors aus der Payload umgehen, wenn vorhanden, die Farb-Auflösung auf dem Gerät aus der Composition-API.

Scroll-Verhalten

Der Track scrollt horizontal mit dem nativen Momentum/Inertia der Plattform. Es gibt in Iteration 1 kein Snap-to-Card — der Track scrollt frei und bleibt dort stehen, wo die Geste ihn verlässt. Overflow wird auf zwei Arten signalisiert: (1) durch die halb sichtbare zweite Karte bei Scroll-Offset 0 (Smartphone) und (2) durch die rechtsstehende Karte, die am Inset-Rand beschnitten ist. Es wird kein Scrollbar gerendert. Es werden keine Edge-Fades oder Fade-Out-Verläufe als Overlays angewendet — der einzige Rand-Hinweis ist der Inset selbst.

Gesten-Priorität

Ein horizontaler Swipe, dessen dominante Achse beim Gestenstart X ist, wird vom Track dieser Reihe konsumiert und NICHT an den übergeordneten vertikalen Scroller weitergegeben. Ein vertikaler Swipe (dominantes Y) umgeht diese Reihe und scrollt die Home-Seite. Die Achse wird beim Gestenstart festgelegt — sobald erfasst, behält der Track die Geste bis zur Freigabe. Zeiger-/Mausrad-Ereignisse: Eine horizontale Radeingabe (Zwei-Finger-Swipe am Touchpad, Shift+Rad) steuert den Track; eine vertikale Radeingabe umgeht den Track und steuert den Seiten-Scroll, selbst wenn der Zeiger über dem Track schwebt.

Karten-Tap

Ein Tap auf eine Karte ruft onNavigate(item.deepLink) auf. Die Deep-Link-Zeichenkette ist für RowSection undurchsichtig — die Auflösung des Ziels liegt in der Verantwortung des Host-Bildschirms.

Recycling

Tracks müssen ihren Inhalt virtualisieren: Nur Karten innerhalb (oder knapp außerhalb) des Viewports werden gemessen/gezeichnet beibehalten. Bild-Ladevorgänge von Karten außerhalb des Bildschirms werden pausiert oder abgebrochen. Wenn eine Karte in den Viewport zurückkehrt, muss ihr Bild ohne Flackern wieder erscheinen — URL-basierte Caches sind verpflichtend. Die Android-Referenzimplementierung verwendet eine Lazy Horizontal List, die dies kostenlos bereitstellt; iOS und Web müssen dies replizieren.

Re-Layout bei Größenänderung

Bei einer Änderung der Viewport-Breite (Rotation, Split-Screen, Foldable, Browser-Resize) wird die Breite pro Karte erneut aus der obigen Formel abgeleitet. Der Track springt — ohne Animation — auf die äquivalente erste-vollständig-sichtbare Karte, um die Position des Benutzers zu erhalten.

Motion

AuslöserEigenschaftDauerEasing
Freier Scroll des Trackstrack translateXkontinuierlich (1:1 zur Eingabe)linear (Geste) / Plattform-Inertia (Freigabe)
Inertia-Abklingentrack translateXPlattform-Standard (≈ 350–600ms)cubic-bezier(0.0, 0.0, 0.2, 1)
Fokuskarte in Sicht scrollentrack translateX250mscubic-bezier(0.4, 0.0, 0.2, 1)
Re-Layout bei Resizetrack translateX0ms (sofortiger Snap)
Skelett-Pulsopacity1200ms Schleifecubic-bezier(0.4, 0.0, 0.2, 1)
Karten-Druckdelegiert an die Karten-Komponente

Interaktion & UX

  • Touch-Benutzer erkennen die Scrollbarkeit durch das Halb-Karten-Peek — zentrieren Sie die Karten nicht und rendern Sie sie nicht randlos ohne Overflow.
  • Mausrad-Benutzer im Web erhalten das Verhalten "Vertikales Rad = Seiten-Scroll"; dies ist beabsichtigt. Stellen Sie eine Alternative (Drag, Fokus, Pfeiltasten) für die horizontale Navigation bereit.
  • Tastatur / D-Pad: Links/Rechts bewegen den Fokus auf die vorherige/nächste Karte im Track; Hoch/Runter verlassen die Reihe vollständig (Bewegung zum vorherigen/nächsten fokussierbaren Element auf der Seite).
  • Zeichnen Sie keinen horizontalen Scrollbar. Die Track-Höhe wird durch die höchste Kind-Karte festgelegt; lassen Sie die Reihe niemals darunter kollabieren.
  • Reihen mit gemischten Größen (Small/Medium/Large-Poster in derselben Reihen-Payload) werden in Iteration 1 nicht verwendet — section.cardSize gilt für die gesamte Reihe.

Barrierefreiheit

  • Section-Landmark: role = region · zugänglicher Name = der Reihentitel (Deutsch, z. B. "Empfehlungen"); Fallback auf "Untitled row" nur als Debugging-Ausweichmöglichkeit — Produktions-Payloads tragen für Nicht-Top10-Reihen immer einen Titel.
  • Track: role = list; Karten haben role = listitem. Der Track legt eine horizontal-scrollbare Beschreibung offen, damit Screenreader den Hinweis ansagen.
  • Karten: Jede Karte ist ein einziges fokussierbares, einzeln drückbares Element mit einem eigenen zugänglichen Namen (delegiert an die Karten-Komponente — Titel + optionales Tag + Dauer).
  • Fokus-Reihenfolge: Eintritt in die Reihe bei der ersten Karte; Links/Rechts durchqueren die Karten; Tab verlässt die Reihe an der letzten sichtbaren Karte und setzt im nächsten Abschnitt fort.
  • Der Halb-Karten-Peek bleibt erhalten, wenn unterstützendes Scrollen eine Karte in den Sichtbereich bringt — die Implementierung muss gerade so weit scrollen, dass die fokussierte Karte vollständig sichtbar ist, und den nachfolgenden Peek nach Möglichkeit erhalten.
  • Reduced-Motion: Fokus-in-Sicht-Animationen werden auf sofortigen Snap (0ms) reduziert. Inertia-Abklingen wird auf ≤120ms verkürzt.
  • Das Mischverhalten beim Mounten wird als Eigenschaft der Reihe angesagt, nicht als Inhaltsänderung — feuern Sie keine Live-Region-Ansagen ab, wenn sich die Misch-Reihenfolge ändert.

Verbrauchte Tokens

PfadTier
component.{mode}.section.verticalSpacingcomponent
component.{mode}.section.horizontalPaddingcomponent
component.{mode}.playlist.rowHorizontalPaddingcomponent
component.{mode}.playlist.rowItemSpacingcomponent
component.{mode}.playlist.rowTitleFontSizecomponent
component.{mode}.playlist.rowTitleFontWeightcomponent
component.{mode}.playlist.rowTitleBottomSpacingcomponent
component.{mode}.rowSection.cardAspectRatiocomponent
component.{mode}.cardPoster.aspectRatiocomponent
component.{mode}.cardPoster.widthSmall / widthMedium / widthLargecomponent
component.{mode}.cardImageOnly.widthSmall / widthMedium / widthLargecomponent
semantic.{mode}.text.primarysemantic
semantic.{mode}.background.canvassemantic

Implementierungsreferenz

Android · packages/android/app/src/main/java/com/protobible/android/ui/components/sections/RowSection.kt (Renderer + Dispatch) · packages/android/app/src/main/java/com/protobible/android/ui/components/sections/CollectionRenderer.kt (Host) · packages/android/app/src/main/java/com/protobible/android/ui/components/sections/Top10Section.kt (gerankte Variante). Der Track ist eine Lazy Horizontal List mit dem Gesten-Achsen-Lock, der von den verschachtelten Scroll-Primitiven der Plattform bereitgestellt wird.
iOS · ausstehend. Vorgeschlagener Ansatz: ein ScrollView(.horizontal), der einen LazyHStack mit expliziten Breiten-Modifikatoren pro Karte hostet; Gesten-Achsen-Priorität wird über simultaneousGesture + ein DragGesture abgefangen, das beim ersten Delta in onChanged auf der dominanten Achse einrastet.
Web · ausstehend. Vorgeschlagener Ansatz: eine Flex-Zeile innerhalb eines overflow-x: auto-Containers; der Halb-Karten-Peek wird durch Setzen der Kartenbreite über die obige Formel erreicht, nicht durch scroll-snap. Unterdrücken Sie den horizontalen Scrollbar mit scrollbar-width: none. Verwenden Sie eine Virtualisierungs-Bibliothek (oder einen IntersectionObserver), um Karten außerhalb des Bildschirms zu recyceln.
Anti-Pattern. Zentrieren Sie die Karten nicht innerhalb des Viewports (keine horizontale Zentrierung), rasten Sie nicht an Kartenrändern ein und rendern Sie keine Scroll-Edge-Fading-Verläufe — der Inset + Halb-Karten-Peek ist der einzige dokumentierte Overflow-Hinweis.

Composition-API-Vertrag

RowSection rendert die Komponente SLIDER. Der Renderer liest den type der ersten Karte, um den Renderer pro Karte auszuwählen (Iteration 1: Nur card_poster_* wird an CardPoster weitergeleitet) und die entsprechende Kartengröße.

Feld an CompositionComponentTyp (laut CompositionModels.kt)Hinweise
type ComponentType.SLIDER → Wire-Wert "slider" Diskriminator, der diese Payload an die RowSection weiterleitet. CompositionModels.kt:58.
headlineText String? Abschnittstitel, der bei Vorhandensein über der Reihe gerendert wird. Der Adapter unter CompositionAdapter.kt:69 fällt bei null auf einen leeren String zurück — der Renderer lässt den Titelknoten dann vollständig weg.
cards List<CompositionCard> Reihen-Items. Leere Liste → Abschnitt wird übersprungen (CompositionAdapter.kt:67). Der type der ersten Karte bestimmt den Renderer und die Kartengröße für die gesamte Reihe (siehe Kartentyp-Dispatch unten). Auf Item-Ebene verwendete Felder: id, imageUrl, objectType + objectId (Deep-Link), headlineText, eyebrowText, descriptionText, durationSeconds, user.progressPercent, badge.displayText, colors.
Kartentyp-Dispatch CompositionCard.type Adapter unter CompositionAdapter.kt:105–110:
  • card_poster_smallPOSTER_PORTRAIT + Größe SMALL
  • card_poster_mediumPOSTER_PORTRAIT + Größe MEDIUM
  • card_poster_largePOSTER_PORTRAIT + Größe LARGE
  • card_hero / card_hero_mediaPOSTER_PORTRAIT + Größe MATCHED (höhengetrieben 300dp × aspect)
Andere Kartenvarianten (IMAGE_ONLY_LANDSCAPE, SPLIT, OVERLAY, AVATAR) existieren im Renderer, sind jedoch außerhalb des Scopes von Iteration 1.
tracking ComponentTracking? Impression + Click auf Komponentenebene. Impressions/Clicks pro Karte werden vom Karten-Renderer über CardTracking emittiert.
buttons List<CompositionButtonGroupItem> = emptyList() Im Wire-Modell für alle Komponenten vorhanden, von SLIDER jedoch nicht verwendet.

Hinweis für Implementierer: Gemischte Kartengrößen innerhalb einer einzelnen Reihe werden nicht unterstützt — die Größe wird einmalig aus der ersten Karte abgeleitet. Wenn die Payload Karten mit unterschiedlichen type-Werten enthält, gewinnt nur der Typ der ersten Karte. Die API sollte Homogenität pro Reihe erzwingen.

Composition-API-Kontrakt

Sektion mit Diskriminator type="playlist_row". Modelle: PlaylistRowSection (PlaylistCollectionModels.kt:49), PlaylistItem (PlaylistCollectionModels.kt:226), RowContextAction (PlaylistCollectionModels.kt:64); Enums PlaylistCardType (PlaylistCollectionModels.kt:356), CardSize (PlaylistCollectionModels.kt:365), MetadataVariant (PlaylistCollectionModels.kt:373).

FeldTypHinweise
idStringZeilen-Kennung.
type"playlist_row"Diskriminator → RowSection.
titleString?Zeilen-Headline über den Karten.
itemsList<PlaylistItem>Karten der Zeile; Reihenfolge = Render-Reihenfolge (außer shuffled).
cardTypePlaylistCardTypeOVERLAY / SPLIT / POSTER_PORTRAIT / IMAGE_ONLY_LANDSCAPE / AVATAR — wählt das Karten-Composable.
cardSizeCardSize?SMALL / MEDIUM (Default) / LARGE / MATCHED — Kartenbreite über Komponenten-Tokens (widthSmall/Medium/Large).
metadataVariantMetadataVariant?FULL (Default) / MINIMAL; wirkt auf die Metadaten-Zeile der SPLIT-Karte.
shuffledBooleanClient mischt die Items; fixedPosition pro Item bleibt respektiert.
contextActionsList<RowContextAction>?Long-Press-Kontextmenü pro Karte der Zeile. Felder: label (Menütext), kind (Aktions-Diskriminator, z. B. "remove" — Handler derzeit No-Op, wartet auf Watchlist-Backend).

Felder pro PlaylistItem (PlaylistCollectionModels.kt:226) und welche Kartentypen sie konsumieren:

PlaylistItem-FeldOVERLAYSPLITPOSTER_PORTRAITIMAGE_ONLY_LANDSCAPEAVATAR
title
subtitle
series✓ (seriesName)✓ (Eyebrow)
metadata | duration✓ (metadata vor duration)
contentType✓ (wenn kein tag)✓ (wenn kein tag)
imageUrl✓ (16:9)✓ (16:9)✓ (Hochformat)✓ (16:9)✓ (rund, Face-Crop)
progress
tag / tagVariant
colors
deepLink
contextActions (Zeile)

Wire-Felder ohne Renderer: description (definiert, von keiner Karte gerendert). Client-only-Felder (nicht im BFF-Payload): fixedPosition (Pinning beim Shuffle), cardTypeOverride (Kartentyp pro Item überschreiben). tagVariant-Werte: new, live, expiring, login_required, upcoming — steuern die Pill-Variante; tag ist der Pill-Text und verdrängt das contentType-Badge.

CardPoster

Iteration 1 scope Row item

Eine vertikale 2:3- (9:16-) Posterkarte, die als dominantes Item-Typ in horizontalen Reihen-Abschnitten auf dem Home-Bildschirm verwendet wird ("Empfehlungen"-artige Reihen, gerankte Reihen, Suchergebnisse). Die gesamte Karte ist ein einziges Tap-Ziel, das den entsprechenden Detail-Bildschirm öffnet. Sie wird in drei Breiten-Stufen (S / M / L) gerendert und unterstützt einen optionalen Fortschrittsbalken sowie einen Fallback bei Bildfehlern.

Visuelle Spezifikation

Aufbau

Vertikale 9:16-Posterkarte, die als Reihen-Item innerhalb horizontaler Reihen-Abschnitte auf dem Home-Bildschirm verwendet wird. Die gesamte Karte ist ein einziges Tap-Ziel, das den entsprechenden Detail-Bildschirm öffnet. Wird in drei Breiten-Stufen (S / M / L) gerendert; unterstützt einen optionalen Fortschrittsbalken (Resume-Watching) und einen Verlauf + zentrierten Titel als Fallback, wenn das Bild fehlt oder nicht geladen werden kann.

Default (Bild vorhanden) 9:16 Hochformat · aspectRatio = 9f / 16f = 0.5625
Schattenplatte · Elevation 8dp Bild · 9:16 cover · auf Radius geclippt Fortschrittsbalken · optional · 4dp hoch · 60% Füllung gezeigt Breiten-Stufen S 120dp M (Standard) 160dp L · 200dp 1 2 3 4

Callout-Legende für den Default-Zustand

  1. Container — Karten-ChromeEckenradius cardPoster.cornerRadius (Token, kein Standard) · 1dp Inset-Border borderColor = 0x26FFFFFF (≈ Weiß mit α 0.15) · geclipptMediathekTokenModels.kt CardPosterTokens · CardPoster.kt:85
  2. Bild-Layer — Full-Bleed-CoverURL über das CDN umgeschrieben mit ar=9:16, w=400. Auf den Container-Eckenradius geclippt. Während des Ladens ist cardPoster.bgColor = 0xFF141B1F als Skelett sichtbar.CardPoster.kt:118–130 · ImgixUtils für URL-Rewrite
  3. SchlagschattenElevation cardPoster.shadowElevation = 8.dp · Spot-Farbe shadowColor = 0x4D000000 (Schwarz α 0.3) · Material-3-Schattenform an Eckenradius angepasstCardPoster.kt:80–84
  4. Fortschrittsbalken (optional)Gesteuert durch user.progressPercent (0–100) in der Payload. API-Vertrag: Balken ausblenden, wenn user == null, wenn progressPercent == 0 oder wenn progressPercent == 100. Andernfalls: unten verankert · Höhe aus component.progress.barHeight · Füllfarbe component.progress.barColor über dem Track. Hinweis für Implementierer: Der aktuelle Android-Quellcode verwendet watchProgress != null && watchProgress > 0f — dies blendet bei 0 und null aus, jedoch nicht bei 100; richten Sie sich nach dem obigen API-Vertrag aus.CardPoster.kt:133–138 · ProgressBar component
Fallback — Bild leer oder Ladefehler Ausgelöst durch: imageLoadFailed || imageUrl.isEmpty()
Titeltext (zentriert, Diagramm Display Familie) ↧ 3-Stopp-Linearverlauf bgColor → bgColor × α 0.8 → canvas 1 2 3

Callout-Legende für den Fallback-Zustand

  1. Verlaufsfüllung — 3-Stopp-LinearverlaufStopps: 0% → cardPoster.bgColor · 50% → bgColor × α 0.8 · 100% → semantic.{mode}.background.canvas · vertikal (von oben nach unten)CardPoster.kt:90–101
  2. Titel — zentriertFamilie Diagramm Display · Größe aus cardSplit.titleFontSizeSp · Gewicht aus cardSplit.titleFontWeight · Zeilenhöhe aus cardSplit.titleLineHeightSp · Farbe cardPoster.fallbackTextColor = 0xFFEBF0F2 · zentriert mit paddingDefault = 16dp rundherumCardPoster.kt:105–116
  3. Fortschrittsbalken (wird ggf. weiterhin gerendert)Die Fallback-Schicht unterdrückt den Fortschrittsbalken nicht — er wird weiterhin am unteren Rand gerendert, wenn watchProgress ungleich null istCardPoster.kt:133–138

Abmessungen

EigenschaftWertTokenQuelle
Seitenverhältnis9:16 (≈ 0.5625)component.cardPoster.aspectRatioMediathekTokenModels.kt
Breite — KleinTokencomponent.{mode}.cardPoster.widthSmallMediathekTokenModels.kt
Breite — Mittel (Standard)Tokencomponent.{mode}.cardPoster.widthMediumMediathekTokenModels.kt
Breite — GroßTokencomponent.{mode}.cardPoster.widthLargeMediathekTokenModels.kt
EckenradiusTokencomponent.{mode}.cardPoster.cornerRadiusMediathekTokenModels.kt
Rahmen1dp bei #26FFFFFF (Weiß α 0.15)component.cardPoster.borderColor + borderWidthMediathekTokenModels.kt
Schatten-Elevation8dpcomponent.cardPoster.shadowElevationMediathekTokenModels.kt
SchattenfarbeSchwarz α 0.3component.cardPoster.shadowColorMediathekTokenModels.kt
Hintergrund (Loading + Fallback-Start)#141B1Fcomponent.cardPoster.bgColorMediathekTokenModels.kt
Fallback-Textfarbe#EBF0F2component.cardPoster.fallbackTextColorMediathekTokenModels.kt
Hit-Targetgesamte Karte (≥ 120 × 213 dp in der kleinsten Stufe — deutlich über dem 48dp-Minimum)a11y baseline
Abstand zwischen Karten (in Reihe)16dpviews.home.layout.rowCardSpacing{Mobile,Tablet}views/home.json (Spec 083)

Zustände

Loading (Skelett)
Container wird ausschließlich in cardPoster.bgColor gezeichnet. Rahmen + Schatten werden gerendert. Bild-Bereich leer.
Geladen
Bild blendet über 200ms ein (Coil crossfade(true)). Rahmen + Schatten bleiben oben.
Bildfehler / leere URL
Fällt permanent auf die Verlauf-+-zentrierter-Titel-Schicht zurück. Innerhalb der Lebensdauer der Karte erfolgt kein Retry.
Gedrückt
Material-3-Ripple innerhalb des geclippten Eckenradius · Tint semantic.{mode}.text.primary mit primitives.opacity.scale15.
Mit Fortschritt
Wird nur gerendert, wenn user != null UND 0 < user.progressPercent < 100. Fortschrittsbalken am unteren Rand verankert über die volle Innenbreite; Füllbreite = (progressPercent / 100) × Innenbreite. Ausgeblendet bei 0 und 100 gemäß API-Vertrag.

Verhalten

  • Tap — ruft onClick auf. Öffnet das Detail (Video / Serie / Playlist / Live-Programm je nach zugrunde liegendem Medientyp).
  • Bild-Fetch — wird einmal ausgelöst, wenn die Karte in den Viewport eintritt. URL über Imgix mit 9:16-Seitenverhältnis und w=400 umgeschrieben. Innerhalb der Karte selbst kein Prefetch.
  • Bildfehler — schaltet den imageLoadFailed-State über den onError-Listener von Coil um; nachfolgende Recompositions rendern die Fallback-Schicht.
  • Long-Press / Rechtsklick — kein definiertes Verhalten in Iteration 1.

Barrierefreiheit

  • Rolle button am Container (oder link im Web).
  • Zugängliches Label = der Titel des Mediums. Das Bild ist innerhalb der Tap-Fläche dekorativ — sein Alt-Text ist der Titel (bereits durch das Container-Label bereitgestellt).
  • Der Fortschrittsbalken legt den Prozentsatz über accessibilityValue offen: "Bereits angesehen: NN %".
  • Die kleinste Stufe (120dp × 213dp) überschreitet das 48dp-Minimum bereits auf jeder Achse.
  • Fokus-Reihenfolge: Ein Stopp pro Karte; keine verschachtelten Fokus-Stopps innerhalb der Karte.

Audit-Notizen

Ein Regressions-Kandidat: Der mittlere Stopp des Fallback-Verlaufs verwendet bgColor.copy(alpha = 0.8f) — eine hartcodierte Zahl. Der Token primitives.opacity.scale80 = 0.8 existiert; der Quellcode trägt eine Annotation // EXCEPTION: derived contrast. Ersetzen Sie entweder das Literal durch tokens.opacity.scale80 oder akzeptieren Sie die Ausnahme. Niedrige Priorität.CardPoster.kt:98

Die Schrift des "Titels" im Fallback verwendet cardSplit.titleFontSize etc., also die Schriftkonfiguration der CardSplit- (image-only-landscape) Komponente. Die komponentenübergreifende Token-Wiederverwendung ist beabsichtigt — diese Tokens wurden aufgrund der visuellen Rhythmik gewählt — der Querverweis ist jedoch nicht offensichtlich. Erwägen Sie, sie in einer zukünftigen Iteration in einen gemeinsamen component.card.fallbackTitle.*-Namespace zu überführen.

Zustände

Loading (Skelett)
Container in cardPoster.bgColor gezeichnet. Kein Spinner. Rahmen und Schatten sind bereits gerendert.
Geladen
Bild blendet über 200ms ein. Rahmen, Schatten und etwaige Fortschrittsbalken werden darüber gerendert.
Bildfehler
Fällt auf einen Verlaufshintergrund + zentrierten Display-Titel zurück (siehe Aufbau 6). Permanent für die Lebensdauer der Karten-Instanz — kein automatischer Retry.
Gedrückt
Standard-Plattform-Druckfeedback (Ripple/Highlight) innerhalb des geclippten Eckenradius, getönt mit semantic.{mode}.text.primary bei opacity.scale15.
Fokussiert (Tastatur / D-Pad)
Äußerer Fokusring bei semantic.focusRing.{bp}.width, versetzt um semantic.focusRing.{bp}.offset, Farbe semantic.{mode}.focusRing.default. Wird außerhalb des kartenseitigen Rahmens gezeichnet, sodass sich beide nie überlappen.
Mit Fortschritt
Fortschrittsbalken am unteren Rand verankert. Track über die volle Innenbreite sichtbar; Füllbreite = watchProgress × Innenbreite.
Deaktiviert
In Iteration 1 nicht verwendet — jede gerenderte Karte ist tappbar.

Verhalten

  • Tap — ruft onClick der Karte auf. Öffnet den Detail-Bildschirm für das zugrunde liegende Medium (Video, Serie, Playlist oder geplantes Programm).
  • Bild-Fetch — Die Anfrage wird einmal ausgelöst, wenn die Karte in den Viewport eintritt. Die CDN-URL wird synchron aus der Quell-URL erzeugt; es wird kein separater Prefetch durch die Karte selbst durchgeführt.
  • Fehlerbehandlung — Ein Fehlschlag beim Bild-Fetch versetzt die Karte für den Rest ihrer Lebensdauer in die Fallback-Schicht; der Fehler wird nicht an die Reihe oder die Seite weitergegeben.
  • Long-Press / Rechtsklick — kein definiertes Verhalten in Iteration 1.

Motion

AuslöserEigenschaftDauerEasing
Bild aufgelöstimage opacity 0 → 1200msease-out
DruckPress-Feedback-Opazität120ms ein / 200ms ausease-out
Fokus rein/rausFokusring-Opazität100mslinear
Fortschritts-FüllungFüllbreiteinnerhalb der Karte nicht animiert

Interaktion & UX

  • Karten leben innerhalb einer horizontal scrollenden Reihe. Touch-Targets nahe den Reihenrändern sind weiterhin voll tappbar — das Edge-Padding der Reihe beschneidet das Druck-Feedback nicht.
  • Die Karte zeigt in Iteration 1 niemals Text über dem Bild an. Titel und Metadaten erscheinen nur in der Fallback-Schicht; das Live-Bild trägt den Titel visuell über das Artwork.
  • Der Fortschrittsbalken ist still, wenn watchProgress 0, null oder nicht vorhanden ist — die Karte reserviert keinen Platz dafür.
  • Die Schattenfarbe kann auf Parent-Ebene mit einer Palettenfarbe getönt werden, die in der Composition-API-Payload der Karte ankommt. Die Tönung ist eine Dekoration auf Bildschirmebene, nicht Teil des Kartenvertrags; der kartenseitige Schatten-Token definiert den neutralen Standard-Schatten.

Barrierefreiheit

  • Rolle button (oder link im Web) am Container.
  • Zugängliches Label = der Titel des Mediums (z. B. "Bibel Verse Plan"). In Iteration 1 wird kein Untertitel gerendert, daher ist keine Konkatenation erforderlich.
  • Das Bild ist innerhalb der Tap-Fläche dekorativ — sein Alt-Text ist der Titel (bereits durch das Container-Label bereitgestellt); kein separater Bild-Zugänglichkeitsknoten.
  • Der Fortschrittsbalken legt seinen Prozentsatz über einen accessibilityValue offen: "Bereits angesehen: NN %".
  • Fokus-Reihenfolge: Reihenrichtung (LTR) — ein Stopp pro Karte. Keine verschachtelten Stopps.
  • Verwenden Sie den plattform-Standard-Fokusring (semantic.focusRing.{bp}.*) bei Tastatur- / D-Pad-Fokus.
  • Minimales Hit-Target in allen Größen erfüllt (≥ 120dp × 213dp).

Verbrauchte Tokens

PfadTier
component.{mode}.cardPoster.cornerRadiuscomponent
component.{mode}.cardPoster.widthSmallcomponent
component.{mode}.cardPoster.widthMediumcomponent
component.{mode}.cardPoster.widthLargecomponent
component.{mode}.cardPoster.aspectRatiocomponent
component.{mode}.cardPoster.borderColorcomponent
component.{mode}.cardPoster.borderWidthcomponent
component.{mode}.cardPoster.shadowElevationcomponent
component.{mode}.cardPoster.shadowColorcomponent
component.{mode}.cardPoster.bgColorcomponent
component.{mode}.cardPoster.fallbackGradientEndcomponent
component.{mode}.cardPoster.fallbackTextColorcomponent
component.{mode}.cardSplit.titleFontSizecomponent (Fallback-Titel)
component.{mode}.cardSplit.titleFontWeightcomponent (Fallback-Titel)
component.{mode}.cardSplit.titleLineHeightcomponent (Fallback-Titel)
component.{mode}.progress.barHeightcomponent
component.{mode}.progress.barCornerRadiuscomponent
component.{mode}.progress.barColorcomponent
component.{mode}.progress.barTrackColorcomponent
semantic.{mode}.background.canvassemantic (Fallback-Verlauf)
semantic.{mode}.text.primarysemantic (Fallback-Text + Druck-Feedback)
semantic.focusRing.{bp}.widthsemantic
semantic.focusRing.{bp}.offsetsemantic
primitives.opacity.scale15primitive (Druck-Feedback)

Referenz

Mobile · CardPoster row
Mobile · CardPoster-Reihe
Tablet · CardPoster row
Tablet · CardPoster-Reihe

Implementierungsreferenz

Android · packages/android/app/src/main/java/com/protobible/android/ui/components/cards/CardPoster.kt. Der Bild-Fetch erfolgt über ImgixUtils.getCardImageUrl(url, aspectRatio = "9:16", width = 400). Der optionale Fortschrittsbalken wird über das gemeinsam genutzte ProgressBar-Primitiv gerendert. Karten werden von RowSection.kt innerhalb einer LazyRow ausgelegt.

Composition-API-Vertrag

Annotationen vom API-Engineer verfasst. CardPoster wird in drei Größenvarianten über das type-Feld ausgeliefert: card_poster_small, card_poster_medium, card_poster_large — serverseitig ausgewählt. Der Renderer zeigt das Bild im Vollformat (imageUrl) und fällt auf einen generierten / gebrandeten Platzhalter zurück, wenn kein Bild bereitgestellt wird oder das Bild nicht geladen werden kann. user.progressPercent ist eine 0–100 Ganzzahl, die den unteren Fortschrittsbalken steuert (nur für laufende Items gerendert — siehe Vertrag unten). colors.{light,dark}.base liefert den Hintergrund pro Theme. Die Farbe von headlineText stammt aus den Design Tokens. Das Klickziel wird über objectType + objectId aufgelöst.

Composition API annotations for CardPoster — type variants, imageUrl, user.progressPercent, colors.base, headlineText, and click target
CardPoster Composition-API-Annotationen (Deutsch). Alle vier Größenvarianten teilen sich einen API-Typ; user.progressPercent null → kein Fortschrittsbalken; colors.base pro Theme; Klickziel über objectType + objectId. Quelle: Pencil-Board des API-Engineers.

API-Felder (1:1 zu CompositionModels.kt)

CardPoster teilt sich die CompositionCard-Payload mit den Hero-Karten — es unterscheiden sich nur das type-Enum und die Felder, die der Renderer tatsächlich verbraucht. Die Agentur sollte dies exakt abbilden.

Feld an CompositionCardTyp (laut CompositionModels.kt)Hinweise
type CardType-Enum → Wire-Werte "card_poster_small" · "card_poster_medium" · "card_poster_large" Ein API-Typ pro Größenstufe (S / M / L). Dies sind die einzigen drei Postergrößen, die die API liefern wird — es gibt keine legacy-Variante in der Spezifikation. Definiert in CompositionModels.kt:96–98.
objectType + objectId ObjectType-Enum + String? Klickziel der Karte. CompositionModels.kt:72–73.
imageUrl String? URL des Poster-Artworks. Wenn nicht vorhanden, wird der Fallback mit Verlauf + zentriertem Titel gerendert (siehe "Zustände · Bildfehler / fehlend" oben).
headlineText String? Sendungstitel. Farbe aus Design Tokens (semantic.{mode}.text.primary). CompositionModels.kt:75.
colors CompositionColorPalette { light, dark } — jeweils mit base, accent, backgroundGradient, optional textPrimary/textSecondary Palette pro Theme. colors.{mode}.base ist der Kartenhintergrund-Fallback (und die "Hintergrundfarbe je nach Theme", die der API-Engineer angesprochen hat).
badge CompositionBadge? Optional. Gleiche Form wie Hero — displayText, backgroundColor, textColor. Clientseitige Großschreibung.
user.progressPercent CardUser { progressPercent: Int? } (gesamtes user nullable) Laut API-Engineer (wortwörtlich):
"Falls User nicht eingeloggt / keine Infos zu diesem User vorliegen, wird das Property user=null (aber vorhanden) sein. Wenn der Wert 0 oder 100 ist, gibt es keine Leiste."
CompositionModels.kt:128–131. Der Adapter unter CompositionAdapter.kt:80 wandelt dies in einen 0–1-Bruchteil um.
tracking CardTracking { impression: CardImpression, click: CardClick } Gleiche Form wie Hero: Impression feuert beim Eintritt in den Viewport, Click beim Tap. CompositionModels.kt:193–214.
button CompositionButton? Im gemeinsamen Modell vorhanden, von CardPoster aber nicht gerendert — die gesamte Karte ist das einzige Tap-Ziel. Für das Poster-Rendering ignorieren.

Composition-API-Kontrakt

CardPoster wird von playlist_row-Zeilen mit cardType=POSTER_PORTRAIT instanziiert. Konsumierte PlaylistItem-Felder (PlaylistCollectionModels.kt:226):

FeldTypHinweise
imageUrlStringHochformat-Poster (3:4-Zuschnitt).
titleString?Titel; Fallback-Darstellung, wenn das Bild fehlt.
tag / tagVariantString?Status-Badge oben links (tagVariantVideoState-Pille); Fallback ohne tag: Kategorie-Badge aus contentType; ohne beides entfällt es. Seit 2026-06-11 unterstützen alle Kartenvarianten Badge + Fortschrittsbalken.
progressFloat?Fortschrittsbalken 0.0–1.0; entfällt bei null.
deepLinkStringNavigationsziel beim Tippen.

Breite über cardSize der Zeile → Tokens cardPoster.widthSmall/Medium/Large. Alle übrigen PlaylistItem-Felder werden von dieser Karte nicht konsumiert (Matrix in RowSection).

CardLandscape

Iteration 2 scope Row item

Eine bild-only 16:9-Querformatkarte, verwendet als Reihen-Item in horizontalen Reihen-Abschnitten (Composition-Kartentyp IMAGE_ONLY_LANDSCAPE). Die gesamte Karte ist ein einziges Tap-Ziel. Sie wird in drei Breiten-Stufen (S / M / L) gerendert, zeigt einen optionalen Fortschrittsbalken und fällt bei Bildfehlern auf einen Verlauf + zentrierten Titel zurück.

Aufbau

Box mit fester 16:9-Ratio, auf den Eckenradius geclippt, mit gemeinsamer Karten-Border und (im Dunkelmodus) Schatten. Darin: das full-bleed-Cover-Bild (Coil crossfade), unten-links optional ein ProgressBar. Bei imageLoadFailed || imageUrl.isEmpty() wird stattdessen ein 3-Stopp-Linearverlauf mit zentriertem Titel gezeichnet. Die drei Breiten werden nicht in der Komponente, sondern beim Aufruf über getCardWidth(IMAGE_ONLY_LANDSCAPE, cardSize) aus den cardImageOnly.width*-Tokens bestimmt.CardLandscape.kt · RowSection.kt:149–154

Default + Größen-Stufen 16:9 · aspectRatio = 1.7778 · S / M / L token-getrieben
Bild · 16:9 cover · auf Radius geclippt Fortschrittsbalken · optional · unten-links Breiten-Stufen S · widthSmall M · widthMedium L · widthLarge 1 2 3

Callout-Legende

  1. Container — Karten-ChromeEckenradius cardLandscape.cornerRadius (= radius.lg, 16dp) · 0.5dp Border cardLandscape.borderColor (#26FFFFFF) · geclippt · Dunkelmodus-Schatten shadowElevation = 8dpCardLandscape.kt:52,56,57,81
  2. Bild — Full-Bleed-Cover (16:9)Coil crossfade über 300ms · Lade-Skelett in cardImageOnly.bgColor (#141B1F). Fallback bei Ladefehler: 3-Stopp-Verlauf + zentrierter Titel im cardLandscape.fallbackTitle*-Font.CardLandscape.kt:90–130
  3. Fortschrittsbalken (optional)Nur bei watchProgress != null && > 0f · ProgressBar-Primitiv · unten-links verankert. Breiten-Stufen S/M/L kommen aus cardImageOnly.widthSmall/Medium/Large (in RowSection, nicht in der Karte).CardLandscape.kt:133–139 · RowSection.kt:149–154

Abmessungen

EigenschaftWertTokenQuelle
Seitenverhältnis16:9 (≈ 1.7778)component.cardLandscape.aspectRatioMediathekTokenModels.kt
Breite — KleinTokencomponent.{mode}.cardImageOnly.widthSmallRowSection.kt:150
Breite — Mittel (Standard)Tokencomponent.{mode}.cardImageOnly.widthMediumRowSection.kt:151
Breite — GroßTokencomponent.{mode}.cardImageOnly.widthLargeRowSection.kt:152
EckenradiusToken (= radius.lg, 16dp)component.{mode}.cardLandscape.cornerRadiusMediathekTokenModels.kt
Rahmen0.5dp bei #26FFFFFF (gemeinsame Karten-Border)component.cardLandscape.borderColor + borderWidthCardLandscape.kt:52,57
Schatten-Elevation (nur Dunkel)8dpcomponent.cardLandscape.shadowElevationCardLandscape.kt:81
Hintergrund (Loading)#141B1Fcomponent.cardImageOnly.bgColorMediathekTokenModels.kt
Fallback-Titel-Font14sp · SemiBold · LH 15component.cardLandscape.fallbackTitleFontSize/Weight/LineHeightCardLandscape.kt:109–111

Zustände

Loading (Skelett)
Box wird in cardImageOnly.bgColor gezeichnet; Bild blendet über 300ms ein (crossfade).
Geladen
Full-bleed-Cover-Bild, auf den Eckenradius geclippt. Border + (Dunkel-)Schatten bleiben oben.
Bildfehler / leere URL
Permanenter Fallback: 3-Stopp-Linearverlauf (bgColor → bgColor α 0.8 → canvas) + zentrierter Titel im cardLandscape.fallbackTitle*-Font.
Mit Fortschritt
Nur gerendert, wenn watchProgress != null && watchProgress > 0f. ProgressBar unten-links verankert.
Gedrückt
Material-3-Ripple innerhalb des geclippten Eckenradius.

Referenz

Mobile · CardLandscape rows (SMALL / MEDIUM)
Mobil · Hell · „Landscape SMALL" + „Landscape MEDIUM"-Reihen

Implementierungsreferenz

CardLandscape.kt · RowSection.kt (PlaylistCardType.IMAGE_ONLY_LANDSCAPE, getCardWidth) · CardLandscapeTokens / CardImageOnlyTokens in MediathekTokenModels.kt

Composition-API-Kontrakt

CardLandscape wird von playlist_row-Zeilen mit cardType=IMAGE_ONLY_LANDSCAPE instanziiert. Konsumierte PlaylistItem-Felder (PlaylistCollectionModels.kt:226):

FeldTypHinweise
imageUrlString16:9-Landscape-Bild.
titleString?Titel; zentrierter Fallback-Text, wenn das Bild fehlt.
tag / tagVariantString?Status-Pill ("Neu", "Live", …).
progressFloat?Fortschrittsbalken 0.0–1.0; entfällt bei null.
deepLinkStringNavigationsziel beim Tippen.

Breite über cardSize der Zeile → Tokens cardImageOnly.widthSmall/Medium/Large (im Showcase in allen drei Größen exerziert).

CardAvatar

Iteration 2 scope Row item

Eine kreisförmige Avatar-Karte für Personen / Sendereihen (Composition-Kartentyp AVATAR): ein rundes, gesichtsfokussiert zugeschnittenes Bild mit einem Titel-Label darunter. Bei Bildfehlern werden die Initialen des Titels als Fallback gezeigt. Die gesamte Karte ist ein einziges Tap-Ziel.

Aufbau

Eine zentrierte Spalte: oben ein kreisförmiger Container (CircleShape-Clip), darunter mit kleinem Abstand das Titel-Label. Das Bild wird über eine Face-Crop-URL geladen. Der kreisförmige Container trägt die gemeinsame Karten-Border (dieselben cardLandscape-Border-Tokens wie Split-/Landscape-/Poster-/Hero-Karten) und im Dunkelmodus einen Schatten. Bei Bildfehlern füllt eine Initialen-Schicht (erste Buchstaben des Titels) den Kreis.CardAvatar.kt:52–59,71–114

Default + Fallback Kreis-Durchmesser 160dp · Label darunter
Face-Crop Titel-Label · max 2 Zeilen AB Fallback · Initialen 1 2 3 4

Callout-Legende

  1. Kreisbild — Face-CropDurchmesser cardAvatar.defaultSize = 160dp · CircleShape-Clip · ContentScale.Crop · Bild über Face-Crop-URLCardAvatar.kt:71 · MediathekTokenModels.kt:474
  2. Border — gemeinsame Karten-BorderKreis-Stroke aus cardLandscape.borderColor + borderWidth (1dp, #26FFFFFF) — geteilt mit allen anderen Karten · Dunkelmodus-Schatten cardAvatar.shadowElevationCardAvatar.kt:55,59,82–86
  3. Titel-LabelUnter dem Kreis · 12sp · Medium · DiagrammFontFamily · max 2 Zeilen · EllipsisCardAvatar.kt:131–143
  4. Initialen-FallbackBei Bildfehler: erste 2 Zeichen des Titels · Bold · 0.3 × Durchmesser · zentriertCardAvatar.kt:106–110

Abmessungen

EigenschaftWertTokenQuelle
Durchmesser (Standard)160dpcomponent.cardAvatar.defaultSizeMediathekTokenModels.kt:474
FormKreisCardAvatar.kt (CircleShape)
Rahmen1dp bei #26FFFFFF (gemeinsame Karten-Border)component.cardLandscape.borderColor + borderWidthCardAvatar.kt:55,59
Schatten-Elevation (nur Dunkel)8dpcomponent.cardAvatar.shadowElevationCardAvatar.kt:82–86
Label-Font12sp · Medium · DiagrammFontFamilycomponent.cardAvatar.labelFontSize + labelFontWeightCardAvatar.kt:135–138
Label max. Zeilen2 · Ellipsiscomponent.cardAvatar.labelMaxLinesCardAvatar.kt:141
Initialen (Fallback)2 Zeichen · Bold · 0.3 × DurchmessercardAvatar.initialsCount / initialsFontSizeRatio / initialsFontWeightCardAvatar.kt:106–110

Zustände

Loading
Kreis-Container ohne Bild; Border + (Dunkel-)Schatten sichtbar.
Geladen
Face-Crop-Bild füllt den Kreis (ContentScale.Crop).
Bildfehler / leere URL
Initialen-Fallback: die ersten Zeichen des Titels, zentriert im Kreis.
Gedrückt
Material-3-Ripple innerhalb des Kreis-Clips.

Referenz

Mobile · CardAvatar row (Personen / Sendereihen)
Mobil · Hell · „Personen / Sendereihen"-Reihe

Implementierungsreferenz

CardAvatar.kt · RowSection.kt (PlaylistCardType.AVATAR) · CardAvatarTokens in MediathekTokenModels.kt

Composition-API-Kontrakt

CardAvatar wird von playlist_row-Zeilen mit cardType=AVATAR instanziiert. Konsumierte PlaylistItem-Felder (PlaylistCollectionModels.kt:226):

FeldTypHinweise
imageUrlStringRundes Avatar-Bild; gesichtszentrierter Zuschnitt über Imgix-Face-Crop.
titleString?Label unter dem Avatar (Person / Sendereihe).
deepLinkStringNavigationsziel beim Tippen.

Kein Fortschritt, keine Pills, keine Metadaten — bewusst minimaler Vertrag (Matrix in RowSection).

Motion-Sprache

Cross-cutting-Vertrag Erforderlich

Motion ist Bestandteil der Spezifikation, nicht Dekoration. Jeder animierte Übergang im Produkt muss eine Dauer aus der Dauer-Skala und ein Easing aus der nachstehenden Easing-Skala wählen — beliebige Werte sind nicht zulässig. Motion drückt Hierarchie aus: kleines UI-Feedback ist schnell und knapp; Surface-Wechsel (Drawer, Modals, Carousels) sind mittelschnell und nutzen Anticipation; Umgebungswechsel (Theme-Crossfade, Vollbild-Route) sind lang und symmetrisch.

Dauer-Skala

Drei benannte Ebenen. Wählen Sie nach Größe des bewegten Elements und Wichtigkeit des Wechsels.

TokenWertVerwendung
motion.duration.short120msPress-Feedback rein, Hover-/Focus-Statuswechsel, Tooltip-Erscheinen, Icon-Tausch, Beginn eines Ripples.
motion.duration.medium220msPress-Feedback raus, Enter/Exit von Listenelementen, Page-Indicator-Dot-Übergänge, kleine Surface-Fades, Tab-Indicator-Slide.
motion.duration.long340msDrawer Öffnen/Schließen, Modal-Scrim-Fade, Crossfade-Body eines Hero-Slides, Theme-Wechsel, Banner-Overlay-Reveal.
motion.duration.carousel600msNur für den Seitenübergang beim Auto-Advance des Hero-Carousels. Lang genug, dass der Nutzer den Wechsel als bewusst wahrnimmt, ohne abgelenkt zu sein.

Alles, was länger als 600ms ist, muss eine kontinuierliche, scroll- oder mediengesteuerte Animation sein (z. B. audio-reaktiver Puls, Video-Timeline). Es ist kein Statuswechsel.

Easing-Skala

Fünf benannte Easings. Verwenden Sie in Specs den symbolischen Namen; die Plattformen mappen auf die nachstehende Kurve.

TokenKurveVerwendung
motion.easing.linearlinearNur für kontinuierliches Tracking — scroll-getriebene Opacity, Drag-Tracking-Transforms, Scrubber. Niemals für Statuswechsel.
motion.easing.standardOut (Entrance)cubic-bezier(0, 0, 0.2, 1)Entrances, Expansions, Fade-ins, Drawer-Öffnen. Das Element verlangsamt in den Ruhezustand.
motion.easing.standardIn (Exit)cubic-bezier(0.4, 0, 1, 1)Exits, Collapses, Fade-outs, Drawer-Schließen. Das Element beschleunigt beim Verlassen.
motion.easing.standard (symmetrisch)cubic-bezier(0.4, 0, 0.2, 0.8)Symmetrische, in beide Richtungen verlaufende Wechsel: Skalen-Toggles, Farb-Crossfades, Indicator-Slides, Hero-Auto-Advance.
motion.easing.spring.gentlespring(stiffness=MediumLow, damping=Medium)Surface-Zustände, die sich physisch anfühlen sollen: Scale-in des Notification-Scrims, Einrasten des Tablet-Menü-Drawers. Sparsam einsetzen.
motion.easing.spring.snappyspring(stiffness=Medium, damping=High)Schließen derselben Surfaces. Kehrt ohne Overshoot in den Ruhezustand zurück. In Paarung mit spring.gentle für ein abgestimmtes In/Out.

Der Auto-Advance des Hero-Carousels nutzt motion.easing.standard bei motion.duration.carousel — siehe HeroCarousel. Das Tablet-Notification-Overlay verwendet gepaarte Springs — siehe Notification-Surface der BottomNavigation.

Choreografie-Regeln

Vorderkante erscheint, Hinterkante verschwindet

Wenn zwei Elemente wechseln (z. B. Hero-Slide A → B, Drawer geschlossen → offen), startet das eingehende Element seinen Enter bei t=0 und das ausgehende Element beendet seinen Exit spätestens bei t=Dauer. Keine Pause dazwischen. Gleichzeitigkeit, keine Sequenz — es sei denn, das ausgehende Element verdeckt das eingehende; in diesem Fall den Enter um 50 % der Exit-Dauer versetzen.

Stagger

Wenn ≥ 3 Geschwister-Elemente gemeinsam erscheinen (Reveal von Row-Section-Inhalt, Menü-Einträge), staffeln Sie um 30ms pro Element, gedeckelt auf 6 Elemente (180ms Gesamtstaffelung). Elemente ab Nummer 7 erscheinen gemeinsam mit Element 6.

Anticipation & Follow-Through

Anticipation (eine kleine Gegenbewegung vor der Hauptbewegung) ist ausschließlich Spring-getriebenen Surfaces vorbehalten. Keine Anticipation bei Fades oder scroll-getriebener Motion einsetzen. Follow-Through (Overshoot und dann Einschwingen) gehört nur zu motion.easing.spring.gentle.

Richtung passt zur Affordance

Eine Surface, die von rechts öffnet (MenuDrawer), schließt nach rechts. Ein Bottom Sheet erscheint von unten und verschwindet nach unten. Niemals die Richtung zwischen Enter und Exit umkehren — das bricht das räumliche Modell.

Keine gleichzeitigen großen Bewegungen

Zu jedem Zeitpunkt darf höchstens ein Element, das mehr als 50 % der Viewport-Fläche einnimmt, animiert werden. Während ein Drawer öffnet, darf das darunterliegende Hero nicht ebenfalls animiert werden. Pausieren Sie den Auto-Advance während Overlays — siehe Reduced Motion unten.

Touch- & Press-Feedback

Press-Feedback ist die einzige Bewegung, die Dauern mischt: Es dehnt sich schnell aus, um den Kontakt zu bestätigen, und zieht sich langsam zusammen, um nachgiebig zu wirken.

PhaseDauerEasingEigenschaft
Press rein (Finger drückt)motion.duration.short (120ms)motion.easing.standardOutRipple-Opacity 0 → token-definierter Press-Alpha; Ripple-Radius 0 → Max.
Press raus (Finger löst)200ms (custom — Ausnahme von der Skala)motion.easing.standardInRipple-Opacity → 0; Radius bleibt auf Max.

200ms ist die einzige zulässige Dauer abseits der Skala. Sie existiert, weil sich 220ms beim Lösen unter dem Finger träge und 120ms ruckartig anfühlen.

Reduced Motion

Meldet die Plattform, dass der Nutzer reduzierte Bewegung bevorzugt (prefers-reduced-motion: reduce im Web, UIAccessibility.isReduceMotionEnabled auf iOS, Settings.Global.ANIMATOR_DURATION_SCALE = 0 oder "Animationen entfernen" auf Android), muss das Produkt:

  • Alle motion.duration.*-Werte auf 0ms reduzieren — bzw. auf 50ms bei Übergängen, bei denen ein sofortiges Umschalten die Zustandskontinuität verlöre (z. B. Drawer öffnen → ein sofortiges Teleportieren wirkt kaputt; ein 50ms-Fade nicht).
  • Alle automatisch fortschreitenden Medien anhalten: der Auto-Advance des Hero-Carousels pausiert unbefristet, scroll-getriebene Loop-Animationen stoppen, audio-reaktive Visualisierungen frieren auf dem letzten Frame ein.
  • Springs durch harte Snaps ersetzen. motion.easing.spring.* wird zu einem motion.easing.standard mit 0ms oder 50ms.
  • Alle Statuswechsel erhalten — nur die Animation zwischen den Zuständen entfällt. Die Funktionalität ist identisch.
  • Die Einstellung bei jedem Lesen respektieren; den Wert nicht über eine App-Sitzung hinweg cachen — Nutzer können sie aus den Systemeinstellungen umschalten, ohne die App in den Hintergrund zu schicken.

Seitenübergänge

Zurückgestellt. Iteration 1 spezifiziert keine bildschirmübergreifenden Route-Übergänge (Home → Detail, Home → Bibel). Verwenden Sie die Plattform-Standards (Android Shared-Axis-X, iOS Push, Web instant), bis die Spec für Seitenübergänge in einer späteren Iteration ausgeliefert wird. Komponenten-Overlays (Drawer, Modal, Banner) sind im Scope und folgen den obigen Regeln.

Referenz

  • HeroCarousel — Auto-Advance: motion.duration.carousel + motion.easing.standard; Verweildauer von 3000ms zwischen Wechseln.
  • MenuDrawer — Slide-in horizontal + Fade: motion.duration.long + motion.easing.standardOut beim Öffnen, standardIn beim Schließen.
  • Notification-Surface der BottomNavigation (Tablet) — gepaartes motion.easing.spring.gentle rein / spring.snappy raus.
  • Scroll-Fade der TopBarmotion.easing.linear, scroll-getrieben (keine Dauer).

Barrierefreiheits-Baseline

Cross-cutting-Vertrag WCAG 2.2 AA

Dieser Abschnitt ist die Mindestbasis. Jede Komponente in diesem Dokument erbt diese Anforderungen; die komponentenspezifischen Accessibility-Blöcke ergänzen lediglich Detailangaben (Labels, Fokus-Reihenfolge, Rolle). Eine Komponente, die eine Baseline-Regel verletzt, ist unabhängig von ihrer eigenen Spezifikation nicht konform.

Hit-Targets

  • Jedes interaktive Element muss eine Hit-Area von mindestens 48dp × 48dp exponieren (Android-Material-Guidance; das iOS-HIG-Minimum von 44pt ist für unsere visuelle Dichte zu klein — wir runden auf).
  • Ist das visuelle Element kleiner, wird transparent nach außen gepolstert, um das Minimum zu erreichen. Das Visual nicht vergrößern.
  • Benachbarte Hit-Areas dürfen in den visuellen Gutter überlappen, ein Tap muss aber stets auf genau ein Target auflösen. Mehrdeutigkeiten werden über das nächstgelegene Zentrum aufgelöst.
  • Long-Press, Swipe und andere zusammengesetzte Gesten müssen eine alternative Single-Tap- oder Button-Affordance für Nutzer bieten, die diese Gesten nicht ausführen können.

Tastatur & Fokus

  • Jedes aktionsfähige Element muss per Tastatur / D-Pad / externem Switch erreichbar sein. Keine Ausnahmen.
  • Fokus-Indikator: 2dp-Ring in semantic.focusRing.{bp}.color mit 2dp Offset außerhalb der Element-Bounds. Niemals nur auf Farbwechsel verlassen.
  • Der Fokus ist sichtbar, sobald ein nicht-zeigendes Eingabegerät verwendet wird; er darf unterdrückt (aber nicht entfernt) werden, wenn die letzte Eingabe ein Touch oder Mausklick war. Web: :focus-visible-Semantik; andernorts äquivalentes Plattformverhalten.
  • Die Tab-Reihenfolge entspricht der visuellen Leseordnung (von oben nach unten, von Anfang nach Ende in der Leserichtung des Dokuments). Komponenten definieren ihre interne Fokus-Reihenfolge in ihrem Accessibility-Abschnitt.
  • Skip-Links: in Iteration 1 nicht erforderlich (die Home-Ansicht ist flach), aber die BottomNavigation muss von der TopBar aus in ≤ N+2 Tabs erreichbar sein, wobei N die Anzahl fokussierbarer Elemente dazwischen ist.

Labels

  • Jedes interaktive Element hat ein Label. Sichtbarer Text genügt; bei reinen Icon-Elementen aria-label (Web), accessibilityLabel (iOS), contentDescription (Android) bereitstellen.
  • Labels sind lokalisiert. Iteration 1 liefert deutsche Strings; Englisch ist eine zusätzliche Locale. Lokalisierte Strings niemals zusammensetzen — ganze Sätze durch ICU schleusen.
  • Labels beschreiben die Aktion, nicht das Icon. "Menü öffnen", nicht "Drei Linien".
  • Zustände müssen bei Änderung angesagt werden (toggled, aufgeklappt/zugeklappt, ausgewählt). Die State-APIs der Plattform verwenden, nicht ein Label-Suffix wie "(aktiv)".

Farbkontrast

  • Body-Text und Icons auf Hintergrund: ≥ 4,5:1 Kontrast (WCAG AA). Gegen beide Token-Sätze (Dark und Light) prüfen.
  • Großer Text (≥ 24sp Regular, ≥ 18,66sp Bold) und UI-Elemente (Fokus-Ringe, bedeutungstragende Icons, Borders, die Hit-Areas abgrenzen): ≥ 3:1.
  • Rein dekorative Elemente (Gradient-Overlays, Scrims auf Hero-Cards) sind vom Kontrasterfordernis befreit, dürfen aber nicht alleiniger Bedeutungsträger sein.
  • Hero-Cards dürfen Text über Fotografie rendern; ein dunkler Gradient-Scrim ist erforderlich, wenn das darunterliegende Bild heller ist, als die Kontrastschwelle es zulässt. Siehe HeroCardShell.

Dynamic Type

  • Die Schriftskalierung des Nutzers wird bis 200% ohne Beschneidung oder Kürzung respektiert. Die Skalierung wird auf allen Text via sp/Dynamic-Type-Pipeline angewendet; reine Display-Strings (Brand-Wordmark) dürfen sich ausklinken.
  • Layouts müssen umfließen: Karten dürfen vertikal wachsen, Zeilen dürfen umbrechen, Bars mit fester Höhe (TopBar, BottomNavigation) dürfen im Rahmen wachsen.
  • Kürzung ist nur bei Metadaten-Strings zulässig, die in der Komponenten-Spec explizit als kürzbar markiert sind — niemals bei primären Titeln, Button-Labels oder Alerts.
  • Vor dem "ausgeliefert"-Status bei 100%, 130%, 175% und 200% Skalierung testen.

Reduced Motion

Siehe Motion-Sprache § Reduced Motion. Der Vertrag ist für Barrierefreiheit verbindlich, nicht nur Vorliebe: Animationen, die blinken, pulsieren oder automatisch fortschreiten, sind in voller Stärke vestibuläre und epileptische Risiken.

Screenreader-Navigation

  • Die Traversierungsreihenfolge entspricht der visuellen Leseordnung. Komponenten, die Kinder aus visuellen Gründen umordnen (z. B. absolut positionierte Badges), müssen die Reihenfolge im Accessibility-Baum explizit korrigieren.
  • Dekorative Elemente werden mit aria-hidden / accessibilityElementsHidden / importantForAccessibility="no" markiert. Dazu gehören: Gradient-Overlays, Badges, die visuell bereits ausgesagten Text duplizieren (z. B. eine "LIVE"-Pille, wenn der Titel bereits "LIVE" enthält), Hintergrundbilder auf Hero-Cards, Page-Indicator-Dots, wenn das Carousel eine Ansage "Seite X von Y" exponiert.
  • Verwandte Elemente gruppieren: Eine Karte mit Bild + Titel + Metadaten wird als ein Fokus-Stopp mit kombiniertem Label angesagt, nicht als drei.
  • Heading-Level sind im Accessibility-Baum korrekt, auch wenn das visuelle Styling vom semantischen Level abweicht.

Modal-Semantik

  • Drawer, Dialoge und Banner-Overlays deklarieren Modal-/Dialog-Rollen. Solange geöffnet: der Fokus ist im Modal gefangen, der dahinterliegende Inhalt ist inert (nicht fokussierbar, nicht angesagt), und die Back-Geste / Escape-Taste schließt.
  • Das Öffnen eines Modals führt den Fokus auf das erste fokussierbare Element darin (oder auf den Modal-Container, falls fokussierbar). Beim Schließen kehrt der Fokus zum öffnenden Element zurück.
  • Der Scrim ist aria-hidden, erhält jedoch einen Tap-to-Dismiss-Handler. Der Scrim ist kein fokussierbares Element.
  • Gilt für: MenuDrawer, das Tablet-Notification-Overlay (BottomNavigation) sowie alle künftigen Bottom Sheets.

Live-Regionen

  • Dynamische Inhalte, die sich ohne Nutzeraktion ändern (Programmwechsel des LIVE-Badges, Erscheinen eines Banner-Overlays, Ersetzungen im Fehlerzustand), werden standardmäßig über eine polite Live-Region angesagt.
  • assertive nur für Inhalte verwenden, die sofortige Aufmerksamkeit erfordern — Verbindung verloren, Wiedergabefehler mitten im Stream. Iteration 1 hat auf dem Home-Screen keine assertive Fälle.
  • Aktualisierungen sind entprellt: aus derselben Region nicht mehr als eine Änderung pro ~2s ansagen.

Formular-Verträge

Nicht in Iteration 1. Der Home-Screen und das Burger-Menü enthalten keine Formular-Eingaben. Wenn Formulare ausgeliefert werden (Suche, Konto, Einstellungen), müssen sie zusätzlich spezifizieren: Eingabe-Labels (sichtbar, programmatisch zugeordnet), Markierung von Pflichtfeldern (nicht ausschließlich über Farbe), Inline-Fehlermeldungen, die per aria-describedby mit Eingaben verknüpft sind, sowie Tastatur-Submit. Hier festgehalten, damit Reviewer die Lücke als beabsichtigt bestätigen können.

Plattform-Mappings

KonzeptAndroidiOSWeb
LabelcontentDescriptionaccessibilityLabelaria-label / sichtbarer Text
RolleModifier.semantics { role = ... }accessibilityTraitsrole-Attribut
VerstecktimportantForAccessibility="no"accessibilityElementsHiddenaria-hidden="true"
Live-RegionliveRegion in semanticsUIAccessibility.post(notification:)aria-live="polite"
Reduced MotionSettings.Global.ANIMATOR_DURATION_SCALEUIAccessibility.isReduceMotionEnabledprefers-reduced-motion
Fokus-RingCompose indication + FocusRing-ModifierUIFocusEffect:focus-visible-Outline

Breakpoints

Cross-cutting-Vertrag 2 aktiv · 2 vorgerüstet

Das Produkt hat einen einzigen, im Root berechneten Layout-Modus. Es gibt keine komponenten-spezifische Media-Query: das Root löst den Breakpoint des Geräts einmal auf, propagiert ihn nach unten, und Komponenten verzweigen über einen booleschen isTablet-Hinweis, sofern ihr Layout abweicht. Token sind per Breakpoint geschlüsselt (semantic.mobile.*, semantic.tablet.*, semantic.desktop.*, semantic.tv.*), und Komponenten wählen den passenden Schlüssel basierend auf dem aufgelösten Modus.

Definierte Breakpoints

ModusToken-SchlüsselTriggerStatus in Iteration 1
mobilesemantic.mobile.*Kürzeste Dimension < 600dp und Hochformat.Aktiv. Referenz-Designfläche.
tabletsemantic.tablet.*Kürzeste Dimension ≥ 600dp und Seitenverhältnis erfüllt die Hysterese-Schwelle (siehe unten). Oder: jedes Gerät im Querformat, das die Mobile-Regel nicht erfüllt.Aktiv. Referenz-Designfläche.
desktopsemantic.desktop.*Kürzeste Dimension ≥ 900dp und Tablet-Regel ist erfüllt. Reserviert für Web/Foldable.Vorgerüstet — Token existieren; keine Layouts in Iteration 1.
tvsemantic.tv.*Plattform meldet TV-UI-Modus (Android UI_MODE_TYPE_TELEVISION, tvOS, Web-10-Foot-Media-Query).Vorgerüstet — Token existieren; keine Layouts in Iteration 1.

Auflösungsregel (verbindlich)

Der Breakpoint wird einmal pro Layout-Pass im Application-Root aufgelöst und in einem Kontextwert gespeichert (Android LocalDeviceMode; iOS Environment-Wert; Web React-Context). Komponenten messen das Fenster niemals selbst.

  1. Meldet die Plattform TV-UI-Modus → tv.
  2. Andernfalls shortSide = min(widthDp, heightDp) und ratio = widthDp / heightDp berechnen.
  3. Wenn shortSide ≥ 900dp und die Tablet-Regel (Schritt 4) zutrifft → desktop.
  4. Wenn shortSide ≥ 600dp und ratio die Hysterese-Schwelle erfüllt → tablet.
  5. Andernfalls, wenn das Gerät im Querformat ist → tablet. (Telefone im Querformat übernehmen das Tablet-Layout.)
  6. Andernfalls → mobile.

Hysterese

Um Flackern beim Größenwechsel über die Grenze hinweg (Split-Screen, Foldables, Browser-Drag) zu verhindern, verwendet der Übergang Tablet ↔ Mobile zwei Schwellen:

  • Aktuell Mobile, Wechsel zu Tablet: erfordert ratio ≥ 0.62 (Wiedereintrittsschwelle).
  • Aktuell Tablet, Wechsel hinaus: bleibt Tablet, bis ratio < 0.60 (Austrittsschwelle).
  • Die Erstbestimmung beim Kaltstart nutzt die Wiedereintrittsschwelle (0.62).

Folgt der Android-Referenz unter packages/android/app/src/main/java/com/protobible/android/ui/utils/ScreenUtils.kt (isTabletLayout). Andere Plattformen müssen dasselbe Hysterese-Verhalten implementieren — nicht nur dieselben primären Schwellen.

Orientierungswechsel

  • Ein Orientierungswechsel evaluiert den Breakpoint neu. Der neue Modus wird im nächsten Layout-Pass den Baum hinunter propagiert.
  • Komponenten müssen sich beim Breakpoint-Wechsel neu komponieren (bzw. neu rendern), ohne Zustand zu verlieren. Scroll-Position, Carousel-Index, Drawer-Offen-Zustand und Formulareingaben überleben den Wechsel.
  • Laufende Animationen werden abgebrochen und auf die Ruhewerte des neuen Breakpoints gesnappt; nicht zwischen Breakpoints animieren.

Was sich zwischen Mobile und Tablet ändert

Komponenten verzweigen auf isTablet auf drei Arten:

  1. Token-Auswahl — derselbe logische Token löst zu einem anderen Wert auf: component.heroVideo.titleFontSize liest auf Tablet aus component.heroVideo.tabletTitleFontSize.
  2. Seitenverhältnis — Hero-Cards sind auf Mobile 3:4 Hochformat und auf Tablet 2,54:1 Querformat. Siehe HeroCarousel.
  3. Surface-Behandlung — Modals, die auf Mobile Vollbild gehen, werden auf Tablet zu Seitenpanels oder zentrierten Dialogen (z. B. rechtes Seitenpanel beim MenuDrawer, zentriertes Tablet-Notification-Overlay mit Scrim).

Die strukturelle Anatomie jeder Komponente ist über alle Breakpoints hinweg identisch — dieselben Ebenen in derselben Reihenfolge. Nur Dimensionen, Schriftgrößen und Surface-Behandlungen ändern sich.

Was sich nicht ändert

  • Die Token-Ebenen-Hierarchie, die Page-Composition-Payload oder die von der API gelieferten Daten.
  • Die Komponenten-Identität: ein CardPoster auf Mobile ist auch auf Tablet ein CardPoster, nur größer.
  • Die Navigationsstruktur: die BottomNavigation hat auf Mobile und Tablet dieselben fünf Tabs.

Authoring-Regel für Komponenten-Specs

Jede Dimensionstabelle in diesem Dokument hat eine Mobile-Spalte und eine Tablet-Spalte. Falls ein Wert zwischen beiden identisch ist, wiederholen Sie ihn — die Spalte nicht zusammenfassen. So bleibt der Vertrag auditierbar und versehentliches Drift wird verhindert, wenn ein Breakpoint aktualisiert wird.

Referenz

  • Android: packages/android/app/src/main/java/com/protobible/android/ui/theme/DeviceMode.kt — Enum + Root-Resolver.
  • Android: packages/android/app/src/main/java/com/protobible/android/ui/utils/ScreenUtils.kt — Pure-Function-Hysterese-Regel, rememberIsTabletLayout().
  • Token-Form: packages/design-tokens-api/tokens.json unter semantic.{mobile,tablet,desktop,tv}.

Token-Referenz

Generiert

Die vollständigen, automatisch generierten Token-Tabellen (Primitives · Semantic · Component · Views) befinden sich im benachbarten Referenz-Dokument index.html. Die kanonisch generierten Tabellen an einem Ort zu halten verhindert Drift und stellt sicher, dass jeder Implementierer dieselben Werte sieht.

API-Endpunkt

Implementierungen holen sich zur Laufzeit den vollständigen, DTCG-formatierten Token-Payload von der Design-Tokens-API. Ein Endpunkt, ein JSON-Bundle — die vier Tier-Ebenen werden zusammen ausgeliefert. Die Android-Referenzimplementierung tut dies beim App-Start und überlagert die Antwort über die mitgelieferten Defaults.

GET https://<tokens-host>/api/v1/tokens
Accept: application/json
If-None-Match: "<cached-etag>"      ← optional, für ETag-Caching

200 OK
ETag: "<sha-256 of payload>"
{
  "version": "<sha-256 of source>",
  "timestamp": "2026-05-10T...",
  "layers": {
    "primitives": { "colors": {...}, "spacing": {...}, "fontWeights": {...}, "opacity": {...} },
    "semantic":   { "dark": {...}, "light": {...}, "typography": {...}, "spacing": {...}, "elevation": {...}, "effect": {...} },
    "component":  { "dark": {...}, "light": {...} },
    "views":      { "home": {...} }
  }
}

304 Not Modified                       ← wenn ETag übereinstimmt — Client behält bestehende Tokens

Token-Sync-Verhalten (Client-Vertrag)

Alle Plattformen sollten dasselbe Sync-Verhalten implementieren, damit Token-Änderungen via Coolify-Redeploy live in der App landen, ohne die App neu zu installieren. Die Android-Referenz folgt diesem Muster:

PhaseVerhaltenAndroid-Quelle
1. Bundled defaults App-Build enthält bundled Default-Token-Werte (in Kotlin: MediathekTokenModels.kt + SemanticColors.kt). Damit funktioniert die App auch bei Erstinstallation OHNE Netzwerk. MediathekTokenModels.kt · SemanticColors.kt
2. Cold-Start: Persistenten Cache laden Beim App-Start: aus lokalem Key-Value-Store den zuletzt synchronisierten Payload und seinen ETag laden (Android DataStore mit Key token_json / token_etag). Wenn vorhanden, wird er direkt in die StateFlow/Compose-Schicht emittiert und überlagert die bundled Defaults — offline-first. DesignTokenRepository.kt:54–69 (initialize) · 169–186 (loadPersistedTokens)
3. Hot-Sync: Hintergrund-Refresh Direkt nach Cold-Start (und periodisch via WorkManager): GET /api/v1/tokens mit If-None-Match: <cached-etag>. Antworten:
  • 200 OK → neuer Payload, persistieren (DataStore overwrite), StateFlow updaten, UI rekomponiert via Compose-Beobachtung.
  • 304 Not Modified → bestehende Tokens behalten, kein Re-Persist.
  • Netzwerkfehler / 5xx → bestehende Tokens behalten. KEIN Crash, KEIN Clear.
DesignTokenRepository.kt:76–102 (syncWithApi) · TokenSyncWorker.kt (WorkManager periodic)
4. Force-Refresh (debug/dev) forceSync() löscht den ETag-Cache und re-downloaded den vollen Payload. Für Entwickler-/QA-Builds gedacht, nicht für Produktionsnutzer. DesignTokenRepository.kt:108–112
5. Reaktive UI-Aktualisierung UI-Schicht beobachtet einen Token-Stream (Android: StateFlow<TokenPayloadResponse?>) und rekomponiert auf Updates. Theme-Provider mergt den Remote-Payload über die bundled Defaults Layer-für-Layer (primitives → semantic → component → views). Theme.kt:75 (collectAsState) · 126–145 (resolveSemanticColors + mergeFromRemote)

iOS / Web Implementierungshinweis: die plattform-äquivalenten Patterns sind: iOS = UserDefaults oder FileManager für Persistenz + URLSession mit If-None-Match + BGAppRefreshTask für Hintergrund-Refresh + @Published Combine-Stream. Web = localStorage oder IndexedDB + fetch mit If-None-Match + Service Worker für Background-Sync + Observable/Signal-Stream. Plattform-Idiome unterscheiden sich; der Vertrag (ETag-Caching, Offline-First, niemals crashen) ist identisch.

Authoring-Quelle

Tokens werden als W3C-DTCG-formatierte JSON-Dateien unter tokens/{primitive,semantic,component,view}/*.json im Repository gepflegt. Die API liefert einen aus diesen Dateien abgeleiteten, konsolidierten Snapshot aus.

EbeneQuellpfadDateien
Primitivetokens/primitive/colors.json, opacity.json
Semantictokens/semantic/colors.json, spacing.json, typography.json
Componenttokens/component/badge.json, brand.json, button.json, card.json, detail.json, epg.json, hero.json, navigation.json, player.json, screen.json, switch.json
Viewtokens/view/home.json

Governance

Zwei CI-Gates schützen den Token-Vertrag:

  • node packages/design-tokens-api/scripts/audit-kotlin-defaults.mjs — jeder Default eines Kotlin-Token-Model-Felds muss auf einen DTCG-Eintrag auflösen oder eine @TokenExempt-Annotation tragen. Aktuell: 0 Verstöße · 328 Felder · 193 angehoben · 21 ausgenommen · 114 ohne Default.
  • node tools/audit/kotlin-inline-literal-lint.mjs — keine Inline-Farb-, Alpha- oder dp-Literale in Composables (mit allowlisteten Ausnahme-Präfixen). Aktuell: 0 Verstöße / 112 Dateien gescannt.

Beide Gates laufen in .github/workflows/tokens-governance.yml. Pull Requests, die untokenisierte Werte einführen, werden blockiert.

Vollständige Token-Tabellen

Automatisch generiert aus packages/design-tokens-api/tokens.json durch specs/080-create-a-plan/docs/scripts/generate-tokens.mjs. Jede Ebene klappt zu ihrer vollständigen Tabelle auf. Diese Tabellen sind die Source of Truth bei der Implementierung — jeder Wert entspricht exakt dem, was die API zur Laufzeit ausliefert.

Primitives — Farben, Spacing, Font Weights, Opacity-Skala
Colors — 11 scales, 99 tokens

base

PathValue
colors.base.white#FFFFFF
colors.base.black#141B1F

neutral

PathValue
colors.neutral.100#F8F9FA
colors.neutral.150#F3F5F7
colors.neutral.200#EBF0F2
colors.neutral.300#DCE4E8
colors.neutral.400#CFD9DD
colors.neutral.500#A8B5BC
colors.neutral.600#7A8B94
colors.neutral.700#556570
colors.neutral.800#3A474F
colors.neutral.900#252F35
colors.neutral.950#141B1F
colors.neutral.975#121212
colors.neutral.999#0A0A0A

warm

PathValue
colors.warm.100#FFF7E8
colors.warm.150#FAF0E1
colors.warm.200#F5EBDB
colors.warm.300#EADED2
colors.warm.400#DBCDB9
colors.warm.500#C7B3A0
colors.warm.600#A99178
colors.warm.700#826B52
colors.warm.800#4A3A2D
colors.warm.900#2B211A

blue

PathValue
colors.blue.100#EEF3F5
colors.blue.150#DEECF2
colors.blue.200#B1D0E1
colors.blue.300#66B0D9
colors.blue.400#1B8FD2
colors.blue.500#055F96
colors.blue.600#0D477B
colors.blue.700#143764
colors.blue.800#10223A
colors.blue.900#080D16

red

PathValue
colors.red.100#FBEAEA
colors.red.150#F8D8D8
colors.red.200#F4CACA
colors.red.300#EAA3A3
colors.red.400#DE7979
colors.red.500#D25151
colors.red.600#C14545
colors.red.700#B03939
colors.red.800#8F2E2E
colors.red.900#4D1818

green

PathValue
colors.green.100#E9F3E6
colors.green.150#DEEDD9
colors.green.200#C5E0BC
colors.green.300#9CCB8E
colors.green.400#6DB258
colors.green.500#4A9A36
colors.green.600#3D8C29
colors.green.700#347A22
colors.green.800#2A641B
colors.green.900#14300D

yellow

PathValue
colors.yellow.100#FFF9E5
colors.yellow.150#FFF4D1
colors.yellow.200#FFF0B8
colors.yellow.300#FFE066
colors.yellow.400#FABE00
colors.yellow.500#E0A800
colors.yellow.600#B8860B
colors.yellow.700#8B6914
colors.yellow.800#5C4510
colors.yellow.900#3D2E0A

purple

PathValue
colors.purple.100#F0EDF6
colors.purple.150#E5E1EF
colors.purple.200#D9D2E8
colors.purple.300#BEB2D6
colors.purple.400#9F8FC2
colors.purple.500#7C6CAD
colors.purple.600#6A5A9A
colors.purple.700#5E4D89
colors.purple.800#4D3F71
colors.purple.900#2B2341

orange

PathValue
colors.orange.100#FFF3E5
colors.orange.150#FFEAD1
colors.orange.200#FFE0B8
colors.orange.300#FFC980
colors.orange.400#FFB24D
colors.orange.500#FFA014
colors.orange.600#E68C00
colors.orange.700#CC7A00
colors.orange.800#A36200
colors.orange.900#523100

marianBlue

PathValue
colors.marianBlue.100#E0F7FA
colors.marianBlue.150#D3F3F8
colors.marianBlue.200#B2EBF2
colors.marianBlue.300#80DEEA
colors.marianBlue.400#4DD0E1
colors.marianBlue.500#26C6DA
colors.marianBlue.600#00ACC1
colors.marianBlue.700#0097A7
colors.marianBlue.800#00838F
colors.marianBlue.900#006064

brand

PathValue
colors.brand.dunkelblau#143764
colors.brand.orange#FFA014
colors.brand.gelb#FABE00
colors.brand.creme#FAFAEB
Spacing — 37 tokens
PathValue
spacing.Space00
spacing.Space0_52
spacing.Space14
spacing.Space1_56
spacing.Space28
spacing.Space2_259
spacing.Space2_510
spacing.Space2_7511
spacing.Space312
spacing.Space3_2513
spacing.Space3_514
spacing.Space3_7515
spacing.Space416
spacing.Space4_2517
spacing.Space4_518
spacing.Space520
spacing.Space5_522
spacing.Space624
spacing.Space728
spacing.Space832
spacing.Space936
spacing.Space1040
spacing.Space1144
spacing.Space1248
spacing.Space1352
spacing.Space1456
spacing.Space14_2557
spacing.Space1560
spacing.Space1664
spacing.Space1872
spacing.Space2080
spacing.Space2288
spacing.Space2496
spacing.Space28112
spacing.Space32128
spacing.Space40160
spacing.Space48192
Font Weights — 4 tokens
PathValue
fontWeights.Normal400
fontWeights.Medium500
fontWeights.SemiBold600
fontWeights.Bold700
Opacity — 14 rungs
PathValue
opacity.scale00
opacity.scale100.1
opacity.scale150.15
opacity.scale250.25
opacity.scale400.4
opacity.scale450.45
opacity.scale500.5
opacity.scale600.6
opacity.scale700.7
opacity.scale750.75
opacity.scale800.8
opacity.scale900.9
opacity.scale950.95
opacity.scale1001
Semantic — theme-aware (Dark/Light) + breakpoint-aware

Theme-aware (dark / light)

background — 6 tokens
PathDarkLight
background.canvas#080D16#F8F9FA
background.surface#3A474F#F5EBDB
background.surfaceRaised#252F35#FFFFFF
background.surfaceSunken#141B1F#EBF0F2
background.inverse#FFFFFF#080D16
background.elevated#141B1F#FFFFFF
text — 12 tokens
PathDarkLight
text.display#FFFFFF#080D16
text.heading#F8F9FA#10223A
text.primary#EBF0F2#252F35
text.secondary#CFD9DD#556570
text.tertiary#A8B5BC#7A8B94
text.disabled#7A8B94#A8B5BC
text.disabledOnPrimary#7A8B94#EBF0F2
text.inverse#080D16#FFFFFF
text.onColor#FFFFFF#FFFFFF
text.link#66B0D9#0D477B
text.linkHover#1B8FD2#1B8FD2
text.linkVisited#9F8FC2#5E4D89
border — 6 tokens
PathDarkLight
border.default#3A474F#CFD9DD
border.subtle#252F35#DCE4E8
border.strong#556570#A8B5BC
border.focus#055F96#1B8FD2
border.focusStrong#1B8FD2#0D477B
border.disabled#3A474F#DCE4E8
primary — 9 tokens
PathDarkLight
primary.background.default#055F96#055F96
primary.background.hover#1B8FD2#1B8FD2
primary.background.pressed#0D477B#0D477B
primary.background.disabled#10223A#B1D0E1
primary.subtle#080D16#EEF3F5
primary.text.default#FFFFFF#FFFFFF
primary.text.disabled#1B8FD2#FFFFFF
primary.text.secondary#B1D0E1#66B0D9
primary.text.secondaryDisabled#055F96#EEF3F5
secondary — 9 tokens
PathDarkLight
secondary.background.default#3A474F#EBF0F2
secondary.background.hover#556570#DCE4E8
secondary.background.pressed#3A474F#CFD9DD
secondary.background.disabled#252F35#F8F9FA
secondary.border#556570#CFD9DD
secondary.text.default#EBF0F2#252F35
secondary.text.disabled#7A8B94#A8B5BC
secondary.text.secondary#DCE4E8#556570
secondary.text.secondaryDisabled#3A474F#CFD9DD
tertiary — 6 tokens
PathDarkLight
tertiary.hover#3A474F#EBF0F2
tertiary.pressed#252F35#DCE4E8
tertiary.text.default#EBF0F2#252F35
tertiary.text.disabled#7A8B94#A8B5BC
tertiary.text.secondary#DCE4E8#556570
tertiary.text.secondaryDisabled#556570#CFD9DD
destructive — 19 tokens
PathDarkLight
destructive.background.default#D25151#B03939
destructive.background.hover#DE7979#C14545
destructive.background.pressed#C14545#8F2E2E
destructive.background.disabled#10223A#EAA3A3
destructive.subtle#4D1818#FBEAEA
destructive.text#DE7979#8F2E2E
destructive.border#D25151#B03939
destructive.secondary.default#4D1818#FBEAEA
destructive.secondary.hover#8F2E2E#F4CACA
destructive.secondary.pressed#B03939#EAA3A3
destructive.secondary.disabled#4D1818#FBEAEA
destructive.textColors.default#DE7979#8F2E2E
destructive.textColors.disabled#10223A#EAA3A3
destructive.textColors.secondary#D25151#B03939
destructive.textColors.secondaryDisabled#8F2E2E#DE7979
destructive.textOnBackground.default#141B1F#FFFFFF
destructive.textOnBackground.disabled#DE7979#D25151
destructive.textOnBackground.secondary#4D1818#F4CACA
destructive.textOnBackground.secondaryDisabled#D25151#DE7979
constructive — 19 tokens
PathDarkLight
constructive.background.default#4A9A36#347A22
constructive.background.hover#6DB258#3D8C29
constructive.background.pressed#3D8C29#2A641B
constructive.background.disabled#2A641B#9CCB8E
constructive.subtle#14300D#E9F3E6
constructive.text#6DB258#2A641B
constructive.border#4A9A36#347A22
constructive.secondary.default#14300D#E9F3E6
constructive.secondary.hover#2A641B#C5E0BC
constructive.secondary.pressed#347A22#9CCB8E
constructive.secondary.disabled#14300D#E9F3E6
constructive.textColors.default#6DB258#2A641B
constructive.textColors.disabled#2A641B#9CCB8E
constructive.textColors.secondary#4A9A36#347A22
constructive.textColors.secondaryDisabled#14300D#6DB258
constructive.textOnBackground.default#141B1F#FFFFFF
constructive.textOnBackground.disabled#6DB258#4A9A36
constructive.textOnBackground.secondary#14300D#C5E0BC
constructive.textOnBackground.secondaryDisabled#4A9A36#6DB258
ai — 32 tokens
PathDarkLight
ai.background.default#7C6CAD#5E4D89
ai.background.hover#9F8FC2#6A5A9A
ai.background.pressed#6A5A9A#4D3F71
ai.background.disabled#4D3F71#BEB2D6
ai.subtle#2B2341#F0EDF6
ai.text#9F8FC2#4D3F71
ai.border#7C6CAD#5E4D89
ai.textColors.default#B1D0E1#4D3F71
ai.textColors.disabled#4D3F71#BEB2D6
ai.textColors.secondary#7C6CAD#5E4D89
ai.textColors.secondaryDisabled#2B2341#9F8FC2
ai.textOnBackground.default#FFFFFF#FFFFFF
ai.textOnBackground.disabled#9F8FC2#FFFFFF
ai.textOnBackground.secondary#F0EDF6#FFFFFF
ai.textOnBackground.secondaryDisabled#BEB2D6#FFFFFF
ai.gradient.defaultStart#0097A7#00ACC1
ai.gradient.defaultEnd#4D3F71#5E4D89
ai.gradient.hoverStart#00ACC1#26C6DA
ai.gradient.hoverEnd#5E4D89#6A5A9A
ai.gradient.pressedStart#00838F#0097A7
ai.gradient.pressedEnd#2B2341#4D3F71
ai.gradient.disabledStart#00838F#B2EBF2
ai.gradient.disabledEnd#6A5A9A#9F8FC2
ai.gradient.angleDeg135135
ai.gradientSecondary.defaultStart#006064#B2EBF2
ai.gradientSecondary.defaultEnd#2B2341#D9D2E8
ai.gradientSecondary.hoverStart#00838F#E0F7FA
ai.gradientSecondary.hoverEnd#4D3F71#F0EDF6
ai.gradientSecondary.pressedStart#006064#80DEEA
ai.gradientSecondary.pressedEnd#2B2341#BEB2D6
ai.gradientSecondary.disabledStart#006064#E0F7FA
ai.gradientSecondary.disabledEnd#2B2341#F0EDF6
warning — 8 tokens
PathDarkLight
warning.default#FABE00#FABE00
warning.hover#FFE066#E0A800
warning.pressed#E0A800#B8860B
warning.disabled#5C4510#FFF4D1
warning.subtle#3D2E0A#FFF9E5
warning.text#FFE066#5C4510
warning.border#E0A800#E0A800
warning.textOnBackground#141B1F#141B1F
accent — 7 tokens
PathDarkLight
accent.default#6A5A9A#7C6CAD
accent.hover#5E4D89#5E4D89
accent.pressed#4D3F71#4D3F71
accent.disabled#4D3F71#BEB2D6
accent.subtle#2B2341#F0EDF6
accent.text#9F8FC2#5E4D89
accent.textOnBackground#FFFFFF#FFFFFF
input — 8 tokens
PathDarkLight
input.background#3A474F#FFFFFF
input.backgroundDisabled#252F35#EBF0F2
input.border#556570#CFD9DD
input.borderHover#7A8B94#A8B5BC
input.borderFocus#055F96#1B8FD2
input.borderError#D25151#C14545
input.borderSuccess#4A9A36#347A22
input.placeholder#A8B5BC#7A8B94
brand — 8 tokens
PathDarkLight
brand.dunkelblau#143764#143764
brand.orange#FFA014#FFA014
brand.gelb#FABE00#FABE00
brand.creme#FAFAEB#FAFAEB
brand.primary#FAFAEB#143764
brand.accent#FABE00#FFA014
brand.text#FFFFFF#071322
brand.background#143764#FFFFFF
overlay — 4 tokens
PathDarkLight
overlay.light{"color":"#000000","alpha":0.2}{"color":"#000000","alpha":0.2}
overlay.medium{"color":"#000000","alpha":0.5}{"color":"#000000","alpha":0.5}
overlay.heavy{"color":"#000000","alpha":0.7}{"color":"#000000","alpha":0.7}
overlay.strong{"color":"#000000","alpha":0.8}{"color":"#000000","alpha":0.8}

Responsive (mobile / tablet / desktop / tv)

typography — 37 tokens
PathMobileTabletDesktopTV
typography.display575780128
typography.h132324896
typography.h228283680
typography.h324242864
typography.h422222456
typography.bodyLarge18181852
typography.body16161648
typography.bodySmall14141440
typography.label12121236
typography.caption11111132
typography.overline10101028
typography.finePrint99924
typography.micro8880
typography.xs12121224
typography.sm14141432
typography.base16161640
typography.md16161844
typography.lg18182048
typography.xl20202456
typography.xxl24242864
typography.xxxl32323680
typography.xxxxl40404896
typography.heroTitle26324064
typography.heroSubtitle14161828
typography.screenTitle32404880
typography.playerTitle18181818
typography.playerTime12121212
typography.epgTime10101010
typography.epgTitle14141414
typography.epgDetail11111111
typography.bodyMedium15151545
typography.labelMedium13131339
typography.lineHeightNotificationTitle28282884
typography.lineHeightNotificationBody24242472
typography.lineHeightHeroTitle28344368
typography.lineHeightHeroSub19222438
typography.letterSpacingOverline1113
spacing — 24 tokens
PathMobileTabletDesktopTV
spacing.gap4xs2224
spacing.gap3xs4448
spacing.gap2_5xs66612
spacing.gap2xs88816
spacing.gapXs10101020
spacing.gapSm12121224
spacing.gapDefault16161632
spacing.gapM20202040
spacing.gapL24242448
spacing.gapXl28282856
spacing.gap2xl32323264
spacing.gap3xl36363672
spacing.gap4xl40404080
spacing.gap8xl969696192
spacing.paddingSm12141624
spacing.paddingDefault16202440
spacing.paddingLg24324864
spacing.sectionGap32804880
spacing.sectionMargin16482448
spacing.elevationCard8888
spacing.elevationHero16161616
spacing.dividerHeight0.50.50.50.5
spacing.gap5xl48644896
spacing.bottomScrollPadding100120100200
radius — 11 tokens
PathMobileTabletDesktopTV
radius.none0000
radius.sm4848
radius.default816816
radius.md12241224
radius.lg16321632
radius.xl20402040
radius.xxl24482448
radius.xxxl28000
radius.xxxxl32000
radius.xxxxxl36000
radius.full9999999999999999
icons — 9 tokens
PathMobileTabletDesktopTV
icons.xs16161632
icons.sm20202040
icons.md24242448
icons.lg32323264
icons.xl36363672
icons.xxl40404080
icons.xxxl565656112
icons.xxxxl808080160
icons.xxxxxl969696192
buttons — 17 tokens
PathMobileTabletDesktopTV
buttons.xsmall.height28362848
buttons.xsmall.paddingX12161224
buttons.xsmall.paddingY68612
buttons.xsmall.fontSize12141224
buttons.small.height32443256
buttons.small.paddingX16241632
buttons.small.paddingY812816
buttons.small.fontSize14161428
buttons.medium.height40564072
buttons.medium.paddingX20322048
buttons.medium.paddingY10161024
buttons.medium.fontSize16181632
buttons.radius816816
buttons.height44644064
buttons.paddingX24402440
buttons.paddingY12201220
buttons.fontSize16161632
input — 4 tokens
PathMobileTabletDesktopTV
input.height44644064
input.paddingX16321632
input.radius816816
input.fontSize16161632
card — 4 tokens
PathMobileTabletDesktopTV
card.padding16202440
card.gap16161632
card.radius16161632
card.nestedRadius0000
videoCard — 3 tokens
PathMobileTabletDesktopTV
videoCard.padding12141632
videoCard.radius16161632
videoCard.thumbnailRadius4448
navigation — 2 tokens
PathMobileTabletDesktopTV
navigation.navBarHeight56806480
navigation.tabBarHeight48805680
focusRing — 2 tokens
PathMobileTabletDesktopTV
focusRing.width2224
focusRing.offset4448
safeArea — 4 tokens
PathMobileTabletDesktopTV
safeArea.top4432032
safeArea.bottom3232032
safeArea.left048048
safeArea.right048048
effect — 0 tokens
PathMobileTabletDesktopTV
elevation — 0 tokens
PathMobileTabletDesktopTV
Component — Komposition pro Komponente
hero — 21 tokens
PathDarkLight
hero.titleMaxFontSize2626
hero.titleMinFontSize1616
hero.autoAdvanceIntervalMs30003000
hero.aspectRatioMobile0.750.75
hero.aspectRatioTablet0.3940.394
hero.indicatorDotSize66
hero.indicatorDotSelectedWidth2020
hero.indicatorDotSpacing33
hero.cornerRadius2424
hero.contentPadding2424
hero.pageSpacing1616
hero.tabletTitleFontSize2626
hero.tabletTitleLineHeight2828
hero.tabletTitleFontWeightMediumMedium
hero.tabletDescFontSize1414
hero.tabletDescLineHeight1818
hero.tabletDescFontWeightMediumMedium
hero.mobileTitleFontSize1616
hero.mobileTitleFontWeightSemiBoldSemiBold
hero.mobileDescFontSize1212
hero.mobileDescFontWeightNormalNormal
cardOverlay — 15 tokens
PathDarkLight
cardOverlay.gradientStartOpacity00
cardOverlay.gradientEndOpacity10.85
cardOverlay.cornerRadius1616
cardOverlay.textPadding1616
cardOverlay.titleFontSize1414
cardOverlay.titleFontWeightSemiBoldSemiBold
cardOverlay.titleLineHeight1515
cardOverlay.subtitleFontSize1111
cardOverlay.subtitleFontWeightNormalNormal
cardOverlay.subtitleLineHeight1313
cardOverlay.metadataFontSize1010
cardOverlay.metadataFontWeightNormalNormal
cardOverlay.metadataLineHeight1313
cardOverlay.eyebrowTitleSpacing22
cardOverlay.actionInset4848
cardSplit — 15 tokens
PathDarkLight
cardSplit.infoAreaPaddingHorizontal1010
cardSplit.infoAreaPaddingVertical1010
cardSplit.cornerRadius1616
cardSplit.titleFontSize1414
cardSplit.titleFontWeightSemiBoldSemiBold
cardSplit.titleLineHeight1515
cardSplit.subtitleFontSize1111
cardSplit.subtitleFontWeightNormalNormal
cardSplit.subtitleLineHeight1313
cardSplit.metadataFontSize1010
cardSplit.metadataFontWeightNormalNormal
cardSplit.metadataLineHeight1313
cardSplit.titleFooterSpacing1616
cardSplit.eyebrowTitleSpacing22
cardSplit.actionInset4848
cardImageOnly — 5 tokens
PathDarkLight
cardImageOnly.cornerRadius1616
cardImageOnly.widthSmall200200
cardImageOnly.widthMedium280280
cardImageOnly.widthLarge360360
cardImageOnly.bgColor#141B1F#FFFFFF
cardPoster — 12 tokens
PathDarkLight
cardPoster.cornerRadius1616
cardPoster.widthSmall120120
cardPoster.widthMedium160160
cardPoster.widthLarge200200
cardPoster.aspectRatio0.56250.5625
cardPoster.borderColorrgba(255, 255, 255, 0.15)rgba(26, 26, 26, 0.15)
cardPoster.borderWidth11
cardPoster.shadowElevation88
cardPoster.shadowColorrgba(0, 0, 0, 0.3)rgba(0, 0, 0, 0.3)
cardPoster.bgColor#141B1F#FFFFFF
cardPoster.fallbackGradientEnd#080D16#FFFFFF
cardPoster.fallbackTextColor#EBF0F2#252F35
cardTop10 — 10 tokens
PathDarkLight
cardTop10.rankFontSize6464
cardTop10.cardAspectRatio0.56250.5625
cardTop10.gradientColors[ #7A8B94 #252F35 ][ #F8F9FA #CFD9DD ]
cardTop10.posterWidth160160
cardTop10.rankNumFontSize200200
cardTop10.rankNumStrokeWidth33
cardTop10.rankNumLetterOverlapFactor-0.2-0.2
cardTop10.rankNumOuterOverlap100100
cardTop10.rankNumBorderColorDarkrgba(255, 255, 255, 0.25)rgba(255, 255, 255, 0.25)
cardTop10.rankNumBorderColorLightrgba(26, 26, 26, 0.25)rgba(26, 26, 26, 0.25)
playlist — 5 tokens
PathDarkLight
playlist.rowHorizontalPadding1616
playlist.rowItemSpacing1616
playlist.rowTitleFontSize1616
playlist.rowTitleFontWeightMediumMedium
playlist.rowTitleBottomSpacing88
section — 2 tokens
PathDarkLight
section.verticalSpacing2424
section.horizontalPadding1616
buttonGroup — 3 tokens
PathDarkLight
buttonGroup.spacing88
buttonGroup.cornerRadius5050
buttonGroup.height3232
badge — 9 tokens
PathDarkLight
badge.height1818
badge.cornerRadius3434
badge.horizontalPadding88
badge.fontSize1010
badge.fontWeightMediumMedium
badge.letterSpacing0.50.5
badge.defaultBackgroundColorrgba(0, 0, 0, 0.6)rgba(0, 0, 0, 0.6)
badge.textColor#FFFFFF#FFFFFF
badge.textColorOnLight#1A1A1A#1A1A1A
cardAvatar — 9 tokens
PathDarkLight
cardAvatar.initialsFontWeightBoldBold
cardAvatar.labelFontSize1212
cardAvatar.labelFontWeightMediumMedium
cardAvatar.defaultSize128128
cardAvatar.borderColorrgba(255, 255, 255, 0.15)rgba(26, 26, 26, 0.15)
cardAvatar.borderWidth11
cardAvatar.shadowElevation88
cardAvatar.shadowColorrgba(0, 0, 0, 0.3)rgba(0, 0, 0, 0.3)
cardAvatar.bgColor#141B1F#FFFFFF
liveTv — 3 tokens
PathDarkLight
liveTv.progressBarColor#FFA014#FFA014
liveTv.livePillBackgroundColor#FF0000#FF0000
liveTv.channelLogoSize4848
progress — 4 tokens
PathDarkLight
progress.barHeight44
progress.barCornerRadius22
progress.barColor#FFFFFF#141B1F
progress.barTrackColorrgba(255, 255, 255, 0.15)rgba(0, 0, 0, 0.15)
heroCarousel — 17 tokens
PathDarkLight
heroCarousel.autoAdvanceIntervalMs30003000
heroCarousel.aspectRatioMobile0.750.75
heroCarousel.aspectRatioTablet0.3940.394
heroCarousel.indicatorDotSize66
heroCarousel.indicatorDotSelectedWidth2020
heroCarousel.indicatorDotSpacing33
heroCarousel.indicatorDotCornerRadius44
heroCarousel.contentPadding2424
heroCarousel.pageSpacing1616
heroCarousel.cornerRadius2424
heroCarousel.borderColorrgba(255, 255, 255, 0.15)rgba(26, 26, 26, 0.15)
heroCarousel.borderWidth11
heroCarousel.elevation1616
heroCarousel.shadowColor#000000#000000
heroCarousel.tabletImageWidthFraction0.70.7
heroCarousel.tabletGradientWidthFraction0.50.6
heroCarousel.tabletContentWidthFraction0.40.4
heroVideo — 35 tokens
PathDarkLight
heroVideo.titleMaxFontSize2626
heroVideo.titleMinFontSize1616
heroVideo.tabletTitleFontSize2626
heroVideo.tabletTitleLineHeight2828
heroVideo.tabletTitleFontWeightMediumMedium
heroVideo.tabletDescFontSize1414
heroVideo.tabletDescLineHeight1818
heroVideo.tabletDescFontWeightMediumMedium
heroVideo.mobileTitleFontSize1616
heroVideo.mobileTitleFontWeightSemiBoldSemiBold
heroVideo.mobileDescFontSize1212
heroVideo.mobileDescFontWeightNormalNormal
heroVideo.contentTopPadding140140
heroVideo.iconSize1414
heroVideo.maxLines.mobileTitle22
heroVideo.maxLines.mobileDescription33
heroVideo.maxLines.tabletPortraitTitle22
heroVideo.maxLines.tabletPortraitDescription33
heroVideo.maxLines.tabletPortraitCrowdedTitle22
heroVideo.maxLines.tabletPortraitCrowdedDescription22
heroVideo.maxLines.tabletLandscapeTitle33
heroVideo.maxLines.tabletLandscapeDescription55
heroVideo.titleFontSize.thresholdShortChars2020
heroVideo.titleFontSize.thresholdMediumChars3030
heroVideo.titleFontSize.thresholdLongChars4040
heroVideo.titleFontSize.thresholdVeryLongChars5050
heroVideo.titleFontSize.spShort2828
heroVideo.titleFontSize.spMedium2424
heroVideo.titleFontSize.spLong2222
heroVideo.titleFontSize.spVeryLong2020
heroVideo.titleFontSize.spXVeryLong1818
heroVideo.titleFontSize.lineHeightDelta22
heroVideo.gradient.stops[ [object Object] [object Object] [object Object] [object Object] [object Object] [object Object] [object Object] ][ [object Object] [object Object] [object Object] [object Object] [object Object] [object Object] ]
heroVideo.scrimFadeShift5252
heroVideo.scrimFadeHeight240240
heroBibleVerse — 9 tokens
PathDarkLight
heroBibleVerse.tabletTitleFontSize2626
heroBibleVerse.tabletTitleLineHeight2828
heroBibleVerse.tabletTitleFontWeightMediumMedium
heroBibleVerse.tabletDescFontSize1414
heroBibleVerse.tabletDescLineHeight1818
heroBibleVerse.tabletDescFontWeightMediumMedium
heroBibleVerse.mobileVerseFontSizeMax3636
heroBibleVerse.mobileVerseFontSizeMin1616
heroBibleVerse.contentTopPadding140140
heroFeatured — 18 tokens
PathDarkLight
heroFeatured.tabletTitleFontSize2626
heroFeatured.tabletTitleLineHeight2828
heroFeatured.tabletTitleFontWeightMediumMedium
heroFeatured.tabletDescFontSize1414
heroFeatured.tabletDescLineHeight1818
heroFeatured.tabletDescFontWeightMediumMedium
heroFeatured.mobileTitleFontSize1616
heroFeatured.mobileTitleFontWeightSemiBoldSemiBold
heroFeatured.mobileDescFontSize1212
heroFeatured.mobileDescFontWeightNormalNormal
heroFeatured.contentTopPadding140140
heroFeatured.scrimFadeShift5252
heroFeatured.scrimFadeHeight240240
heroFeatured.tabletPortraitTitleMaxLines22
heroFeatured.tabletPortraitDescMaxLines33
heroFeatured.tabletImageWidthFraction0.70.7
heroFeatured.tabletGradientWidthFraction0.50.6
heroFeatured.tabletContentWidthFraction0.40.4
heroLive — 1 tokens
PathDarkLight
heroLive.cornerRadius2424
cardLandscape — 10 tokens
PathDarkLight
cardLandscape.aspectRatio1.77781.7778
cardLandscape.cornerRadius1212
cardLandscape.borderColorrgba(255, 255, 255, 0.15)rgba(26, 26, 26, 0.15)
cardLandscape.borderWidth11
cardLandscape.shadowElevation88
cardLandscape.shadowColorrgba(0, 0, 0, 0.3)rgba(0, 0, 0, 0.3)
cardLandscape.bgColor#141B1F#FFFFFF
cardLandscape.fallbackTitleFontSize1414
cardLandscape.fallbackTitleFontWeightSemiBoldSemiBold
cardLandscape.fallbackTitleLineHeight1515
cardNotification — 1 tokens
PathDarkLight
cardNotification.aspectRatio1.77781.7778
mediaCard — 11 tokens
PathDarkLight
mediaCard.defaultMaxWidth280280
mediaCard.defaultHeight256256
mediaCard.metadataSpacing33
mediaCard.overlayLinearAlphaMiddle0.20.2
mediaCard.overlayLinearAlphaEnd0.60.6
mediaCard.overlayRadialAlphaMiddle0.30.3
mediaCard.overlayRadialAlphaEnd0.70.7
mediaCard.overlayAngledAlphaFirst0.20.2
mediaCard.overlayAngledAlphaMid0.50.5
mediaCard.overlayAngledAlphaEnd0.80.8
mediaCard.overlayFallbackAlphaEnd0.40.4
programGuideCard — 6 tokens
PathDarkLight
programGuideCard.borderWidth11
programGuideCard.cornerRadius3434
programGuideCard.dotSize66
programGuideCard.progressHeight33
programGuideCard.progressCornerRadius1.51.5
programGuideCard.liveBadgeBackgroundAlpha0.40.4
btvTopBar — 1 tokens
PathDarkLight
btvTopBar.height4242
btvDropdown — 1 tokens
PathDarkLight
btvDropdown.minWidth250250
loadingSpinner — 1 tokens
PathDarkLight
loadingSpinner.strokeWidth33
playlistVideoListItem — 3 tokens
PathDarkLight
playlistVideoListItem.thumbnailWidth120120
playlistVideoListItem.thumbnailCornerRadius22
playlistVideoListItem.thumbnailVerticalPadding22
episodeListItem — 1 tokens
PathDarkLight
episodeListItem.aspectRatio1.77781.7778
detailHeroMobile — 2 tokens
PathDarkLight
detailHeroMobile.aspectRatio1.77781.7778
detailHeroMobile.spacerWidth22
protoBibleButton — 4 tokens
PathDarkLight
protoBibleButton.borderWidth11
protoBibleButton.pressedAlpha0.120.12
protoBibleButton.disabledAlpha0.380.38
protoBibleButton.hoverAlpha0.080.08
btvLogo — 4 tokens
PathDarkLight
btvLogo.starBibelGap55
btvLogo.bibelAppGap33
btvLogo.bibelWidth5252
btvLogo.appWidth2828
rowSection — 1 tokens
PathDarkLight
rowSection.cardAspectRatio0.56250.5625
cardHero — 22 tokens
PathDarkLight
cardHero.cornerRadius1616
cardHero.minHeight480480
cardHero.badgePaddingHorizontal1010
cardHero.badgePaddingVertical55
cardHero.badgeCornerRadius66
cardHero.badgeFontSize1111
cardHero.eyebrowFontSize1212
cardHero.titleFontSize2222
cardHero.descriptionFontSize1414
cardHero.buttonHeight4444
cardHero.buttonFontSize1414
cardHero.buttonHorizontalPadding2020
cardHero.gradientStartFraction0.30.3
cardHero.contentBottomPadding2424
cardHero.contentHorizontalPadding2020
cardHero.eyebrowTopSpacing44
cardHero.descriptionTopSpacing66
cardHero.buttonTopSpacing1212
cardHero.progressBarHeight44
cardHero.fallbackTitleFontSize1414
cardHero.fallbackTitleFontWeightSemiBoldSemiBold
cardHero.fallbackTitleLineHeight1515
slider — 4 tokens
PathDarkLight
slider.titleFontSize1818
slider.titleBottomPadding1212
slider.horizontalPadding1616
slider.itemSpacing1212
sliderHero — 4 tokens
PathDarkLight
sliderHero.heightMobile520520
sliderHero.heightTablet600600
sliderHero.autoAdvanceIntervalMs50005000
sliderHero.indicatorBottomPadding1616
bannerOverlay — 2 tokens
PathDarkLight
bannerOverlay.scrimAlpha0.60.6
bannerOverlay.closeButtonBackgroundAlpha0.50.5
menuDrawer — 2 tokens
PathDarkLight
menuDrawer.rowBgColor#3A474F#EBF0F2
menuDrawer.overlayBgAlpha0.950.95
button — 40 tokens
PathDarkLight
button.primary.bgrgba(235, 240, 242, 0.75)#252F35
button.primary.bg-pressedrgba(235, 240, 242, 0.85)rgba(37, 47, 53, 0.8)
button.primary.bg-disabledrgba(235, 240, 242, 0.3)rgba(37, 47, 53, 0.3)
button.primary.text#252F35#EBF0F2
button.primary.text-disabledrgba(37, 47, 53, 0.4)rgba(235, 240, 242, 0.5)
button.primary.secondary-textrgba(37, 47, 53, 0.7)rgba(235, 240, 242, 0.7)
button.primary.secondary-text-disabledrgba(37, 47, 53, 0.4)rgba(235, 240, 242, 0.4)
button.primary.borderrgba(0, 0, 0, 0)rgba(0, 0, 0, 0)
button.secondary.bgrgba(235, 240, 242, 0.15)rgba(37, 47, 53, 0.1)
button.secondary.bg-pressedrgba(235, 240, 242, 0.25)rgba(37, 47, 53, 0.15)
button.secondary.bg-disabledrgba(235, 240, 242, 0.08)rgba(37, 47, 53, 0.05)
button.secondary.text#EBF0F2#252F35
button.secondary.text-disabledrgba(235, 240, 242, 0.4)rgba(37, 47, 53, 0.4)
button.secondary.secondary-textrgba(235, 240, 242, 0.7)rgba(37, 47, 53, 0.7)
button.secondary.secondary-text-disabledrgba(235, 240, 242, 0.4)rgba(37, 47, 53, 0.3)
button.secondary.borderrgba(235, 240, 242, 0.3)rgba(37, 47, 53, 0.15)
button.tertiary.bgrgba(0, 0, 0, 0)rgba(0, 0, 0, 0)
button.tertiary.bg-pressedrgba(235, 240, 242, 0.1)rgba(37, 47, 53, 0.1)
button.tertiary.bg-disabledrgba(0, 0, 0, 0)rgba(0, 0, 0, 0)
button.tertiary.text#EBF0F2#252F35
button.tertiary.text-disabledrgba(235, 240, 242, 0.4)rgba(37, 47, 53, 0.4)
button.tertiary.secondary-textrgba(235, 240, 242, 0.7)rgba(37, 47, 53, 0.7)
button.tertiary.secondary-text-disabledrgba(235, 240, 242, 0.4)rgba(37, 47, 53, 0.3)
button.tertiary.borderrgba(0, 0, 0, 0)rgba(0, 0, 0, 0)
button.overlay.bgrgba(37, 47, 53, 0.5)rgba(37, 47, 53, 0.5)
button.overlay.bg-pressedrgba(37, 47, 53, 0.65)rgba(37, 47, 53, 0.65)
button.overlay.bg-disabledrgba(37, 47, 53, 0.25)rgba(37, 47, 53, 0.25)
button.overlay.text#EBF0F2#EBF0F2
button.overlay.text-disabledrgba(235, 240, 242, 0.4)rgba(235, 240, 242, 0.4)
button.overlay.secondary-textrgba(235, 240, 242, 0.7)rgba(235, 240, 242, 0.7)
button.overlay.secondary-text-disabledrgba(235, 240, 242, 0.3)rgba(235, 240, 242, 0.3)
button.overlay.borderrgba(0, 0, 0, 0)rgba(0, 0, 0, 0)
button.tertiary-dark.bgrgba(0, 0, 0, 0)rgba(0, 0, 0, 0)
button.tertiary-dark.bg-pressedrgba(37, 47, 53, 0.1)rgba(37, 47, 53, 0.1)
button.tertiary-dark.bg-disabledrgba(0, 0, 0, 0)rgba(0, 0, 0, 0)
button.tertiary-dark.text#252F35#252F35
button.tertiary-dark.text-disabledrgba(37, 47, 53, 0.4)rgba(37, 47, 53, 0.4)
button.tertiary-dark.secondary-textrgba(37, 47, 53, 0.7)rgba(37, 47, 53, 0.7)
button.tertiary-dark.secondary-text-disabledrgba(37, 47, 53, 0.3)rgba(37, 47, 53, 0.3)
button.tertiary-dark.borderrgba(0, 0, 0, 0)rgba(0, 0, 0, 0)
Views (4. Ebene) — Komposition pro Screen (Home)
home — 14 tokens
PathValue
views.home.navBar.borderAlpha0.15
views.home.videoSurface.background#000000
views.home.modalScrim.base#000000
views.home.modalScrim.alpha0.8
views.home.layout.contentPaddingHorizontalMobile16
views.home.layout.contentPaddingHorizontalTablet32
views.home.layout.sectionGapMobile24
views.home.layout.sectionGapTablet32
views.home.layout.contentPaddingTopMobile8
views.home.layout.contentPaddingTopTablet16
views.home.layout.contentPaddingBottomMobile24
views.home.layout.contentPaddingBottomTablet32
views.home.layout.rowCardSpacingMobile16
views.home.layout.rowCardSpacingTablet16

Coverage der in dieser Spec zitierten Pfade

Vor der Implementierung lesen. Dieses Dokument zitiert Token-Pfade in der Naming-Konvention der Consumer-Seite (entsprechend dem, wie der Kotlin-Code den Wert liest). Nicht jeder Pfad ist bereits in die Design-Tokens-API überführt — manche existieren als Kotlin-Model-Defaults; einige sind reine Spec-Vorschläge, die der Implementierer formalisieren soll. Die nachstehende Coverage-Tabelle katalogisiert alle 250 in der Spec zitierten, eindeutigen Pfade nach ihrem aktuellen Status.

Coverage-Übersicht

StatusAnzahlBedeutung
⚙️ API168Pfad löst aktuell über die Design-Tokens-API auf. Implementierer holen den Wert zur Laufzeit von /api/v1/tokens.
🔧 Kotlin76Pfad entspricht einem Feld in der Android-Kotlin-Token-Model-Datenklasse (mit Default-Wert), ist aber noch nicht in die API überführt. Implementierer können den Wert bis zur Überführung aus dem Android-Quellcode als kanonischen Default lesen.
📝 Spec-only24Reiner Spec-Pfad — noch kein Token in irgendeiner Form. Als logischer Platzhalter behandeln; der Implementierer sollte beim Adaptieren der Spec einen echten Token vorschlagen.
Detaillierte Liste (268 Pfade)
PfadStatus
component.btvLogo.heightMobile🔧 Kotlin
component.btvLogo.heightTablet🔧 Kotlin
component.btvTopBar.actionSpacing📝 Spec-only
component.btvTopBar.horizontalPaddingMobile📝 Spec-only
component.btvTopBar.horizontalPaddingTablet📝 Spec-only
component.btvTopBar.scrollFadeDistanceDp📝 Spec-only
component.btvTopBar.verticalPaddingMobile📝 Spec-only
component.btvTopBar.verticalPaddingTablet📝 Spec-only
component.cardLandscape.aspectRatio⚙️ API
component.heroBibleVerse.tabletContentWidthFraction🔧 Kotlin
component.heroBibleVerse.tabletGradientOnlyContentWidthFraction🔧 Kotlin
component.heroBibleVerse.tabletGradientWidthFraction🔧 Kotlin
component.heroBibleVerse.tabletImageWidthFraction🔧 Kotlin
component.heroCarousel.aspectRatioMobile⚙️ API
component.heroCarousel.aspectRatioTablet⚙️ API
component.heroCarousel.autoAdvanceAnimationDurationMs🔧 Kotlin
component.heroCarousel.autoAdvanceIntervalMs⚙️ API
component.heroCarousel.beyondViewportPageCount📝 Spec-only
component.heroCarousel.borderColor⚙️ API
component.heroCarousel.borderWidth⚙️ API
component.heroCarousel.colorEmitThresholdMs🔧 Kotlin
component.heroCarousel.contentPadding⚙️ API
component.heroCarousel.cornerRadius⚙️ API
component.heroCarousel.elevation⚙️ API
component.heroCarousel.indicatorDotCornerRadius⚙️ API
component.heroCarousel.indicatorDotSelectedWidth⚙️ API
component.heroCarousel.indicatorDotSize⚙️ API
component.heroCarousel.indicatorDotSpacing⚙️ API
component.heroCarousel.pageIndicatorAnimationDurationMs🔧 Kotlin
component.heroCarousel.pageIndicatorInactiveAlpha🔧 Kotlin
component.heroCarousel.pageSpacing⚙️ API
component.heroCarousel.shadowAmbientAlpha🔧 Kotlin
component.heroCarousel.shadowColor⚙️ API
component.heroCarousel.tabletContentWidthFraction🔧 Kotlin
component.heroCarousel.tabletGradientWidthFraction🔧 Kotlin
component.heroCarousel.tabletImageWidthFraction🔧 Kotlin
component.heroFeatured.bodyFontFamily🔧 Kotlin
component.heroFeatured.contentTopPadding⚙️ API
component.heroFeatured.displayFontFamily🔧 Kotlin
component.heroFeatured.mobileDescFontSize⚙️ API
component.heroFeatured.mobileDescFontWeight⚙️ API
component.heroFeatured.mobileTitleFontSize⚙️ API
component.heroFeatured.mobileTitleFontWeight⚙️ API
component.heroFeatured.tabletContentWidthFraction🔧 Kotlin
component.heroFeatured.tabletDescFontSize⚙️ API
component.heroFeatured.tabletDescFontWeight⚙️ API
component.heroFeatured.tabletDescLineHeight⚙️ API
component.heroFeatured.tabletDescMaxLines🔧 Kotlin
component.heroFeatured.tabletGradientWidthFraction🔧 Kotlin
component.heroFeatured.tabletImageWidthFraction🔧 Kotlin
component.heroFeatured.tabletTitleFontSize⚙️ API
component.heroFeatured.tabletTitleFontWeight⚙️ API
component.heroFeatured.tabletTitleLineHeight⚙️ API
component.heroFeatured.tabletTitleMaxLines🔧 Kotlin
component.heroLive.bodyFontFamily🔧 Kotlin
component.heroLive.borderColorAlpha🔧 Kotlin
component.heroLive.channelNameMaxLines🔧 Kotlin
component.heroLive.cornerRadius⚙️ API
component.heroLive.displayFontFamily🔧 Kotlin
component.heroLive.epgRefreshIntervalMs🔧 Kotlin
component.heroLive.miniTimelineTitleMaxLines🔧 Kotlin
component.heroLive.programTitleMaxLines🔧 Kotlin
component.heroLive.progressUpdateIntervalMs🔧 Kotlin
component.heroLive.tabletInfoWidthFraction🔧 Kotlin
component.heroVideo.tabletTitleFontSize⚙️ API
component.heroVideo.titleFontSize🔧 Kotlin
component.programGuideCard.progressCornerRadius⚙️ API
component.programGuideCard.progressHeight⚙️ API
component.{mode}.btvDropdown.minWidth⚙️ API
component.{mode}.button.medium.height🔧 Kotlin
component.{mode}.button.primary.bg🔧 Kotlin
component.{mode}.button.primary.bgDisabled📝 Spec-only
component.{mode}.button.primary.bgPressed📝 Spec-only
component.{mode}.button.primary.text🔧 Kotlin
component.{mode}.button.primary.textDisabled📝 Spec-only
component.{mode}.button.secondary.bg🔧 Kotlin
component.{mode}.button.secondary.bgDisabled📝 Spec-only
component.{mode}.button.secondary.bgPressed📝 Spec-only
component.{mode}.button.secondary.border🔧 Kotlin
component.{mode}.button.secondary.text🔧 Kotlin
component.{mode}.button.secondary.textDisabled📝 Spec-only
component.{mode}.buttonGroup.*🔧 Kotlin
component.{mode}.buttonGroup.cornerRadius⚙️ API
component.{mode}.buttonGroup.height⚙️ API
component.{mode}.buttonGroup.spacing⚙️ API
component.{mode}.cardImageOnly.widthMedium⚙️ API
component.{mode}.cardLandscape.aspectRatio⚙️ API
component.{mode}.cardLandscape.bgColor⚙️ API
component.{mode}.cardLandscape.borderColor⚙️ API
component.{mode}.cardLandscape.borderWidth⚙️ API
component.{mode}.cardLandscape.cornerRadius⚙️ API
component.{mode}.cardLandscape.shadowColor⚙️ API
component.{mode}.cardLandscape.shadowElevation⚙️ API
component.{mode}.cardPoster.*🔧 Kotlin
component.{mode}.cardPoster.aspectRatio⚙️ API
component.{mode}.cardPoster.bgColor⚙️ API
component.{mode}.cardPoster.borderColor⚙️ API
component.{mode}.cardPoster.borderWidth⚙️ API
component.{mode}.cardPoster.cornerRadius⚙️ API
component.{mode}.cardPoster.fallbackGradientEnd⚙️ API
component.{mode}.cardPoster.fallbackTextColor⚙️ API
component.{mode}.cardPoster.shadowColor⚙️ API
component.{mode}.cardPoster.shadowElevation⚙️ API
component.{mode}.cardPoster.widthLarge⚙️ API
component.{mode}.cardPoster.widthMedium⚙️ API
component.{mode}.cardPoster.widthSmall⚙️ API
component.{mode}.cardPoster.widthSmall|Medium|Large📝 Spec-only
component.{mode}.cardSplit.titleFontSize⚙️ API
component.{mode}.cardSplit.titleFontWeight⚙️ API
component.{mode}.cardSplit.titleLineHeight⚙️ API
component.{mode}.cardTop10.cardAspectRatio⚙️ API
component.{mode}.cardTop10.posterWidth⚙️ API
component.{mode}.cardTop10.rankNumBorderColorDark⚙️ API
component.{mode}.cardTop10.rankNumBorderColorLight⚙️ API
component.{mode}.cardTop10.rankNumFontSize⚙️ API
component.{mode}.cardTop10.rankNumLetterOverlapFactor⚙️ API
component.{mode}.cardTop10.rankNumOuterOverlap⚙️ API
component.{mode}.cardTop10.rankNumStrokeWidth⚙️ API
component.{mode}.heroBibleVerse.contentTopPadding⚙️ API
component.{mode}.heroBibleVerse.displayFontFamily🔧 Kotlin
component.{mode}.heroBibleVerse.mobileVerseFontSizeMax⚙️ API
component.{mode}.heroBibleVerse.mobileVerseFontSizeMin⚙️ API
component.{mode}.heroBibleVerse.tabletContentWidthFraction🔧 Kotlin
component.{mode}.heroBibleVerse.tabletGradientOnlyContentWidthFraction🔧 Kotlin
component.{mode}.heroBibleVerse.tabletGradientWidthFraction🔧 Kotlin
component.{mode}.heroBibleVerse.tabletImageWidthFraction🔧 Kotlin
component.{mode}.heroBibleVerse.verseBaseSizeMobile🔧 Kotlin
component.{mode}.heroCarousel.aspectRatioMobile⚙️ API
component.{mode}.heroCarousel.aspectRatioTablet⚙️ API
component.{mode}.heroCarousel.bodyFontFamily🔧 Kotlin
component.{mode}.heroCarousel.borderColor⚙️ API
component.{mode}.heroCarousel.borderWidth⚙️ API
component.{mode}.heroCarousel.cornerRadius⚙️ API
component.{mode}.heroCarousel.descriptionMaxLines🔧 Kotlin
component.{mode}.heroCarousel.displayFontFamily🔧 Kotlin
component.{mode}.heroCarousel.eyebrowMaxLines🔧 Kotlin
component.{mode}.heroCarousel.tabletContentWidthFraction🔧 Kotlin
component.{mode}.heroCarousel.tabletGradientWidthFraction🔧 Kotlin
component.{mode}.heroCarousel.tabletImageWidthFraction🔧 Kotlin
component.{mode}.heroCarousel.titleMaxLines🔧 Kotlin
component.{mode}.heroVideo.bodyFontFamily🔧 Kotlin
component.{mode}.heroVideo.contentTopPadding⚙️ API
component.{mode}.heroVideo.displayFontFamily🔧 Kotlin
component.{mode}.heroVideo.eyebrowMaxLines🔧 Kotlin
component.{mode}.heroVideo.iconSize⚙️ API
component.{mode}.heroVideo.mobileDescFontSize⚙️ API
component.{mode}.heroVideo.mobileDescFontWeight⚙️ API
component.{mode}.heroVideo.mobileTitleFontSize⚙️ API
component.{mode}.heroVideo.mobileTitleFontWeight⚙️ API
component.{mode}.heroVideo.tabletDescFontSize⚙️ API
component.{mode}.heroVideo.tabletDescFontWeight⚙️ API
component.{mode}.heroVideo.tabletDescLineHeight⚙️ API
component.{mode}.heroVideo.tabletDescMaxLines🔧 Kotlin
component.{mode}.heroVideo.tabletTitleFontSize⚙️ API
component.{mode}.heroVideo.tabletTitleFontWeight⚙️ API
component.{mode}.heroVideo.tabletTitleLineHeight⚙️ API
component.{mode}.heroVideo.tabletTitleMaxLines🔧 Kotlin
component.{mode}.menuDrawer.dividerAlpha🔧 Kotlin
component.{mode}.menuDrawer.overlayBgAlpha⚙️ API
component.{mode}.menuDrawer.rowBgColor⚙️ API
component.{mode}.menuDrawer.sectionLabelAlpha🔧 Kotlin
component.{mode}.menuDrawer.swipeDismissThresholdDp🔧 Kotlin
component.{mode}.menuDrawer.tabletDrawerWidth🔧 Kotlin
component.{mode}.menuDrawer.tabletScrimAlpha🔧 Kotlin
component.{mode}.playlist.rowHorizontalPadding⚙️ API
component.{mode}.playlist.rowItemSpacing⚙️ API
component.{mode}.playlist.rowTitleBottomSpacing⚙️ API
component.{mode}.playlist.rowTitleFontSize⚙️ API
component.{mode}.playlist.rowTitleFontWeight⚙️ API
component.{mode}.progress.barColor⚙️ API
component.{mode}.progress.barCornerRadius⚙️ API
component.{mode}.progress.barHeight⚙️ API
component.{mode}.progress.barTrackColor⚙️ API
component.{mode}.protoBibleButton.*🔧 Kotlin
component.{mode}.protoBibleButton.borderWidth⚙️ API
component.{mode}.protoBibleButton.disabledAlpha⚙️ API
component.{mode}.protoBibleButton.hoverAlpha⚙️ API
component.{mode}.protoBibleButton.pressedAlpha⚙️ API
component.{mode}.rowSection.*🔧 Kotlin
component.{mode}.rowSection.cardAspectRatio⚙️ API
component.{mode}.section.horizontalPadding⚙️ API
component.{mode}.section.verticalSpacing⚙️ API
primitives.colors.blue.150⚙️ API
primitives.colors.blue.300⚙️ API
primitives.colors.blue.600⚙️ API
primitives.colors.blue.800⚙️ API
primitives.fontWeights.Normal⚙️ API
primitives.opacity.*🔧 Kotlin
primitives.opacity.scale10⚙️ API
primitives.opacity.scale15⚙️ API
primitives.opacity.scale25⚙️ API
primitives.opacity.scale40⚙️ API
primitives.opacity.scale45⚙️ API
primitives.opacity.scale5🔧 Kotlin
primitives.opacity.scale50⚙️ API
primitives.opacity.scale70⚙️ API
primitives.opacity.scale75⚙️ API
primitives.opacity.scale80⚙️ API
primitives.opacity.scale90⚙️ API
primitives.opacity.scale95⚙️ API
primitives.opacity.scaleNN📝 Spec-only
primitives.radius.sm📝 Spec-only
semantic.color.accent.genres📝 Spec-only
semantic.color.broadcast.live🔧 Kotlin
semantic.dark.text.onColor⚙️ API
semantic.dark.text.primary⚙️ API
semantic.desktop.*🔧 Kotlin
semantic.focusRing.{bp}.*🔧 Kotlin
semantic.focusRing.{bp}.color🔧 Kotlin
semantic.focusRing.{bp}.offset⚙️ API
semantic.focusRing.{bp}.width⚙️ API
semantic.icons.{bp}.md⚙️ API
semantic.light.text.primary⚙️ API
semantic.mobile.*🔧 Kotlin
semantic.navigation.{bp}.iconLabelGap📝 Spec-only
semantic.navigation.{bp}.navBarHeight⚙️ API
semantic.radius.{bp}.sm⚙️ API
semantic.spacing.{bp}.gap2_5xs⚙️ API
semantic.spacing.{bp}.gap2xs⚙️ API
semantic.spacing.{bp}.gap3xs⚙️ API
semantic.spacing.{bp}.gap4xs⚙️ API
semantic.spacing.{bp}.gapDefault⚙️ API
semantic.spacing.{bp}.gapSm⚙️ API
semantic.spacing.{bp}.gapXl⚙️ API
semantic.spacing.{bp}.paddingDefault⚙️ API
semantic.tablet.*🔧 Kotlin
semantic.tv.*🔧 Kotlin
semantic.typography.body.fontFamily📝 Spec-only
semantic.typography.display.fontFamily📝 Spec-only
semantic.typography.{bp}.label⚙️ API
semantic.{mobile,tablet,desktop,tv}📝 Spec-only
semantic.{mode}.ai.gradient.*🔧 Kotlin
semantic.{mode}.ai.gradient.defaultEnd⚙️ API
semantic.{mode}.ai.gradient.defaultStart⚙️ API
semantic.{mode}.ai.gradient.disabledEnd⚙️ API
semantic.{mode}.ai.gradient.disabledStart⚙️ API
semantic.{mode}.ai.textOnBackground.default⚙️ API
semantic.{mode}.ai.textOnBackground.disabled⚙️ API
semantic.{mode}.background.canvas⚙️ API
semantic.{mode}.background.elevated📝 Spec-only
semantic.{mode}.background.surface⚙️ API
semantic.{mode}.background.surfaceSunken⚙️ API
semantic.{mode}.border.default⚙️ API
semantic.{mode}.border.strong⚙️ API
semantic.{mode}.border.subtle⚙️ API
semantic.{mode}.border.{strong|default}📝 Spec-only
semantic.{mode}.brand.accent⚙️ API
semantic.{mode}.brand.orange⚙️ API
semantic.{mode}.brand.primary⚙️ API
semantic.{mode}.focusRing.default🔧 Kotlin
semantic.{mode}.scrim.medium📝 Spec-only
semantic.{mode}.text.onColor⚙️ API
semantic.{mode}.text.primary⚙️ API
semantic.{mode}.text.secondary⚙️ API
views.home.layout.contentPaddingBottomMobile⚙️ API
views.home.layout.contentPaddingBottomTablet⚙️ API
views.home.layout.contentPaddingHorizontalMobile⚙️ API
views.home.layout.contentPaddingHorizontalTablet⚙️ API
views.home.layout.contentPaddingTopMobile⚙️ API
views.home.layout.contentPaddingTopTablet⚙️ API
views.home.layout.rowCardSpacingMobile⚙️ API
views.home.layout.rowCardSpacingTablet⚙️ API
views.home.layout.sectionGapMobile⚙️ API
views.home.layout.sectionGapTablet⚙️ API
views.home.modalScrim.alpha⚙️ API
views.home.modalScrim.base⚙️ API
views.home.navBar.borderAlpha⚙️ API
views.home.videoSurface.background⚙️ API

Bei der Implementierung auf einer neuen Plattform: zuerst Pfade mit der Markierung ⚙️ API bevorzugen (Werte von der API holen); auf 🔧 Kotlin-Defaults zurückfallen (aus packages/android/.../MediathekTokenModels.kt lesen), bis diese Tokens überführt sind; 📝 Spec-only-Einträge als kleines Lift-Backlog behandeln und während der Implementierung als DTCG-Ergänzungen vorschlagen.

Anhang — Pflege dieses Dokuments

Dieses Dokument besteht aus handgeschriebener Prosa mit zwei automatisierten Komponenten: den Token-Referenz-Tabellen (im benachbarten index.html) und dem Token-Coverage-Anhang (in diesem Dokument, von einem Skript erzeugt). Alles Übrige — Anatomie, Motion, Verhalten, Barrierefreiheit — ist handgeschrieben.

Wann aktualisieren

  • Komponenten-Vertrag geändert (visuell, Verhalten, Motion, a11y) → den entsprechenden Abschnitt von Hand aktualisieren. Bei visuellen Änderungen neuen Screenshot erstellen und die Datei unter screenshots/ ersetzen.
  • Token hinzugefügt / umbenannt / entferntnode specs/080-create-a-plan/docs/scripts/generate-tokens.mjs ausführen (regeneriert die von index.html konsumierten partials/tokens-*.html) und node specs/080-create-a-plan/docs/scripts/gen-token-coverage.mjs (frischt die oben eingebettete Coverage-Tabelle auf). Beide Skripte sind idempotent.
  • Neue Iteration beginnt → entweder neue <section>-Blöcke nach dem etablierten Template hinzufügen oder diese Datei zu handover-v2.html forken, falls sich der Vertrag radikal ändert.

Authoring-Konventionen

  • Jede Komponenten-<section> muss alle Unterabschnitte in dieser Reihenfolge enthalten: Überblick · Visuelle Spezifikation (Anatomie + Dimensionen + Hit-Targets) · Zustände · Verhalten · Motion · Interaktion & UX · Barrierefreiheit · Verwendete Token · Referenz · Implementierungsreferenz.
  • Plattform-agnostisches Vokabular verwenden: "Container", "Stack", "Press-Feedback", niemals "Box / Modifier / Composable / View / UIView / div".
  • Größen in dp oder sp, niemals in px oder pt. Zeit in ms. Easings als cubic-bezier(...) oder symbolischer Name (ease-out, linear, …).
  • DTCG-Token-Pfade innerhalb von <code>-Tags referenzieren. Das Coverage-Skript liest diese, um den obigen Anhang zu berechnen; konsistente Pfadbenennung ist es, was den Anhang korrekt hält.
  • Jedes interaktive Element erhält ein dokumentiertes Hit-Target ≥ 48dp.
  • Jeder Statuswechsel erhält einen dokumentierten Motion-Eintrag (oder einen expliziten "instant"-Hinweis).

Validatoren (vor dem Mergen von Änderungen an diesem Dokument ausführen)

  • node specs/080-create-a-plan/docs/scripts/gen-token-coverage.mjs — frischt die Coverage auf. Wächst die Spec-only-Anzahl unerwartet, haben Sie einen Token zitiert, der nirgends existiert — Pfad korrigieren.
  • node packages/design-tokens-api/scripts/audit-kotlin-defaults.mjs — jedes Kotlin-Feld mit Default löst weiterhin über DTCG auf. Muss mit Exit-Code 0 enden.
  • node tools/audit/kotlin-inline-literal-lint.mjs — keine Inline-Farb-, Alpha- oder dp-Literale in Composables (nur allowlistete Ausnahmen). Muss mit Exit-Code 0 enden.

Provenienz

v1 verfasst am 10.05.2026 auf Basis der Android-Referenzimplementierung. Die initialen Komponentenabschnitte wurden in 7 parallelen agentischen Durchgängen gegen den Kotlin-Quellcode + die DTCG-Tokens + das Engineering-Referenz-Dokument (index.html) erstellt und anschließend integriert, poliert und validiert. Mehr als 30 Commits liefern Codebasis, Token-API, Token-System und dieses Dokument gemeinsam aus. Die vollständige Historie findet sich über git log --grep="084\|spec(083)\|083\|handover" im Projekt-Root.

Browser-Unterstützung

Dieses Dokument benötigt einen modernen Evergreen-Browser (Chrome / Edge / Firefox / Safari, in den letzten 2 Jahren veröffentlicht). Es verwendet CSS Grid, position: sticky, scroll-margin-top, IntersectionObserver, prefers-color-scheme, backdrop-filter und ES6+-JavaScript. Getestet in Chrome 130+, Safari 17+, Firefox 130+. Das Dokument ist auch ohne JavaScript vollständig nutzbar (TOC-Scroll-Spy, Theme-Persistenz, ⌘K-Filter und Scroll-to-Top sind Progressive Enhancements).