Every component of the design system with a live example, usage rule and types – named in German, from one source, for web, Mac, iPhone and iPad.
A component here is a finished piece of interface with a fixed meaning: a card, a button, the sidebar. Each component exists exactly once, in packages/design-system. The examples on this page are not pictures. They are the real components, showing the Sonntag family on Wednesday, 3 September.
Every component has four parts. They sit in the same folder and share the same name.
File
What it holds
Name.jsx
the component in React, using role tokens only
Name.d.ts
the props, with German names and explanations
Name.prompt.md
usage, language register and an example with the Sonntag family – for people and for agents
gruppe.card.html
a card with every state, for review
The names are German, on purpose. The button is Knopf, not Button; the sidebar is Leiste, not Sidebar. Whoever reads the code reads the same words as in the interface and in this documentation.
The smallest parts: people, icons, buttons, pills, numbers. Every other component is built from them. A person always appears as a sticky note in their person colour, with an initial. Colour alone never carries meaning.
Der Klebezettel eines Menschen – Personenfarbe mit Initiale, nie Farbe allein.
<Avatar wer="anna" groesse={32} />
<Avatar alle gewaehlt onClick={alleZeigen} />
Personenfarben sind über die ganze App gleich: Anna Mint, Tobias Türkis, Felix Flieder, Lena Pfirsich, Sofia Himmel. Text darauf ist immer Tinte – auch nachts, denn der Zettel bleibt hell.
Types (.d.ts)
/** Klebezettel je Mensch: Personenfarbe plus Initiale, optional mit Namen. */
export interface AvatarProps {
/** Schluessel aus Familie Sonntag ("anna", "tobias", "felix", "lena", "sofia", "grete", "werner", "hilde") oder ein Objekt {name, initiale, platz}. */
wer?: string | { name: string; initiale: string; platz: number };
/** Durchmesser in px. 20 im Kartenkopf, 32 im Filter, 44 am iPhone, 56 fuer Kinder. */
groesse?: number;
/** Name neben dem Kreis - fuer Leiste und Listen. */
mitName?: boolean;
/** Der schwarze Kreis mit zwei Koepfen: "alle". */
alle?: boolean;
/** Im Personenfilter: true = schwarzer Ring, false = grau zurueckgenommen. */
gewaehlt?: boolean | null;
ring?: boolean;
onClick?: () => void;
style?: React.CSSProperties;
}
export function Avatar(props: AvatarProps): JSX.Element;
Blau heisst Fahrt, immer. Der Text lautet „Papa fährt 15:40 · Kombi“ — wer und wann ohne Trenner, das Auto mit Punkt. Uhrzeit als 15:40, nie "15.40" und nie "15 Uhr".
Types (.d.ts)
/** Blaue Pille fuer eine Fahrt, die an einer anderen Sache haengt. */
export interface FahrtPilleProps {
/** Wer faehrt - "Papa", "Anna", "Jonas' Mama". */
wer?: string;
/** Uhrzeit als HH:MM. */
zeit?: string;
/** Auto aus dem Buendel - "Kombi". */
auto?: string;
children?: React.ReactNode;
style?: React.CSSProperties;
}
export function FahrtPille(props: FahrtPilleProps): JSX.Element;
Ein Zeichen aus dem Lucide-Satz — für alles, was ein Symbol braucht.
<Icon name="fahrt" groesse={16} titel="Fahrt" />
name nimmt eine Sach-Rolle (fahrt, schule, sport, essen, arzt, musik, freude, familie, ritual, schwimmen, vorschlag) oder direkt einen Lucide-Dateinamen (bell, search, plus).
Das Zeichen wird inline gerendert und über stroke: currentColor eingefärbt — es trägt also die Textfarbe seines Elternteils und kommt in Screenshots und Exporten mit.
Quelle sind die Dateien in assets/icons/; components/bausteine/zeichen.js hält ihren Pfad-Inhalt. Kommt ein Zeichen dazu: Datei nach assets/icons/ legen und zeichen.js neu erzeugen — nie ein SVG von Hand zeichnen.
Ohne titel ist das Zeichen dekorativ (aria-hidden) — richtig, weil neben jedem Zeichen Text steht.
Types (.d.ts)
/** Zeichen aus dem Lucide-Satz, inline gerendert und über `stroke` eingefärbt. */
export interface IconProps {
/** Sach-Rolle ("fahrt", "schule", "essen", "familie", "ritual", "vorschlag", ...) oder Lucide-Dateiname ("bell"). */
name: string;
/** Kantenlänge in px. Standard 16. */
groesse?: number;
/** Strichstärke. Standard 2 — wie im Satz. */
strich?: number;
/** Strichfarbe. Standard currentColor. */
farbe?: string;
/** Wenn gesetzt, wird das Zeichen für VoiceOver benannt; sonst ist es dekorativ. */
titel?: string;
style?: React.CSSProperties;
}
export function Icon(props: IconProps): JSX.Element | null;
export const SACHEN: Record<string, string>;
Register Family Manager: kurz, zupackend, ein Verb — „Ich fahre“, „Tobias fragen“, „Übernehmen“, „Rückgängig“. Nie „OK“, nie „Abbrechen“ ohne Gegenstand.
Ein Knopf ist immer einzeilig. Knöpfe stehen in einer Knopfreihe: nebeneinander, wenn beide Platz haben, sonst untereinander in voller Breite — nie ein zweizeiliger Knopf, nie zwei ungleich breite Knöpfe untereinander. ziel="kind" gibt 56 px mit 16-px-Schrift, wo Kinder bedienen. inaktiv rechnet eigene Rollen statt Deckkraft.
Types (.d.ts)
/**
* Handlungsknopf. Primär schwarz mit hellem Text („Ich fahre“), Zweitknopf beige („Später“),
* Klein ohne Fläche für Nebenwege. Schriftgrad und Polster wachsen mit der Zielfläche.
*/
export interface KnopfProps {
art?: "primaer" | "zweit" | "klein";
/** Zielfläche: Zeiger 32 / 12,5 px, Finger 44 / 14 px, Kind 56 / 16 px. */
ziel?: "zeiger" | "finger" | "kind";
/** Sach-Rolle oder Lucide-Name eines Zeichens links im Knopf. */
zeichen?: string;
/** Inaktiv ist eine eigene Rolle, keine Deckkraft. */
inaktiv?: boolean;
/** Füllt die Breite - für iPhone-Karten. */
breit?: boolean;
/** Erlaubt Zeilenumbruch in der Beschriftung. Standard aus: ein Knopf ist einzeilig;
* wird die Reihe zu eng, stapelt die Knopfreihe die Knöpfe in voller Breite. */
umbruch?: boolean;
type?: "button" | "submit" | "reset";
children?: React.ReactNode;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
style?: React.CSSProperties;
}
export function Knopf(props: KnopfProps): JSX.Element;
/** Die Knopfreihe: nebeneinander, wenn Platz ist, sonst untereinander in voller Breite. */
export interface KnopfreiheProps {
children?: React.ReactNode;
/** Mindestbreite je Knopf in px, ab der zwei nebeneinander passen. Standard 104. */
mindest?: number;
ausrichtung?: "mitte";
style?: React.CSSProperties;
}
export function Knopfreihe(props: KnopfreiheProps): JSX.Element;
Wer dabei ist – die überlappenden Punkte rechts im Kartenkopf.
<Punktgruppe leute={["anna", "lena"]} />
Der Zuständige steht immer zuerst. Alles hat genau einen Zuständigen; fehlt er, trägt die Karte den gestrichelten Offen-Rand und nicht einfach keinen Punkt.
Types (.d.ts)
/** Die Punkte rechts im Kartenkopf: wer dabei ist. Initiale in Personenfarbe, bei mehreren ueberlappend. */
export interface PunktgruppeProps {
/** Schluessel aus Familie Sonntag, in der Reihenfolge der Zustaendigkeit. */
leute?: Array<string | { name: string; initiale: string; platz: number }>;
/** Durchmesser in px. Standard 20. */
groesse?: number;
style?: React.CSSProperties;
}
export function Punktgruppe(props: PunktgruppeProps): JSX.Element;
Der helle Kreis mit dem Zeichen der Sache – immer links im Kartenkopf, nie woanders.
<SachSymbol art="fahrt" titel="Fahrt" />
Was für eine Sache es ist, sagt dieses Symbol; wer dabei ist, sagen die Punkte rechts (Punktgruppe); was für eine Art es ist, sagt die Kartenfarbe. Kein Menü, kein Chevron im Kopf.
Types (.d.ts)
/** Zeichen der Sache in einem hellen Kreis - steht links im Kartenkopf. */
export interface SachSymbolProps {
/** Sach-Rolle: "fahrt" | "schule" | "sport" | "essen" | "arzt" | "musik" | "freude" | "familie" | "ritual" | "vorschlag" | "schwimmen". */
art?: string;
/** Kreisdurchmesser in px. Standard 26 (Mac), 24 auf dem iPhone. */
groesse?: number;
/** Auf farbigen Karten - haelt den Kreis hell. */
aufFarbe?: boolean;
titel?: string;
style?: React.CSSProperties;
}
export function SachSymbol(props: SachSymbolProps): JSX.Element;
Bei Null zeigen wir keinen Zaehler, sondern nichts – und im Ort selbst den leeren Zustand.
Types (.d.ts)
/** Rosa Zaehler an Ort, Chip oder Klingel. */
export interface ZaehlerProps {
/** Die Zahl. Bei 0 nicht anzeigen - Leere ist ein leerer Zustand, kein Zaehler. */
wert?: number | string;
/** "akzent" (Standard) | "punkt" (rosa Punkt ohne Zahl, fuer die Klingel). */
art?: "akzent" | "aufSchwarz" | "punkt";
groesse?: number;
/** Beschriftung fuer VoiceOver, z. B. "3 Sachen im Eingang". */
titel?: string;
style?: React.CSSProperties;
}
export function Zaehler(props: ZaehlerProps): JSX.Element;
The card is the most important component. It comes in five kinds, and the kind carries the meaning: beige for an event, blue for a drive, yellow for family and ritual, dashed grey for a suggestion from Albert, dashed pink for something without anyone responsible. The thing sits on the left of the card header, the people on the right.
Zeilen behalten ihre Bedeutungsfarbe. Eine Zeile ohne Zuständigen wird laut gezeigt: gestrichelter Offen-Rand, nie stillschweigend leer.
Types (.d.ts)
/** Eine Karte fuer Gleichzeitiges: mehrere Sachen zur selben Stunde, jede als Zeile. */
export interface BuendelTeil {
titel: string;
art?: "termin" | "fahrt" | "familie";
/** Sach-Symbol, wenn es nicht aus der Art folgt. */
symbol?: string;
zeit?: string;
leute?: string[];
/** Ohne Zustaendigen - die Zeile bekommt den gestrichelten Offen-Rand. */
offen?: boolean;
}
export interface BuendelKarteProps {
/** Was die Stunde traegt: "Drei Sachen um 16:00". */
titel: string;
zeit?: string;
teile: BuendelTeil[];
style?: React.CSSProperties;
}
export function BuendelKarte(props: BuendelKarteProps): JSX.Element;
Die Karte – das Ding, aus dem Heute, Woche und Kalender bestehen. Ein Datenmodell, viele Ansichten.
<Karte art="fahrt" titel="Rückfahrt Auswärtsspiel" ort="Sportplatz Nord → Zuhause"
zeit="14:00 – 14:30" leute={["felix"]} hoehe="hoch"
pillen={<ZustandsPille>Kein Fahrer</ZustandsPille>}
knoepfe={<Knopf>Ich fahre</Knopf>} />
<Karte art="vorschlag" titel="Bei Tobias steht ab 13:00 nichts. Fragen?"
unterzeile="Vorschlag von Albert"
knoepfe={<KarteVorschlagKnoepfe onUebernehmen={…} onZurueck={…} />} />
Register Family Manager, kurz und ruhig. Die Bedeutungen werden nicht getauscht: blau ist Fahrt, gelb ist Familie, gestrichelt rosa heisst "ohne Zuständigen". Ein Vorschlag hat immer zwei Knöpfe – Albert erledigt nie still.
Types (.d.ts)
/**
* Die Karte in fuenf Arten. Die Farbe sagt die Art, das Symbol die Sache, die Punkte die Leute.
*/
export interface KarteProps {
/** "termin" beige | "fahrt" blau | "familie" gelb | "vorschlag" gestrichelt grau | "offen" gestrichelt rosa. */
art?: "termin" | "fahrt" | "familie" | "vorschlag" | "offen";
/** Kartentitel, ein Gegenstand: "Rückfahrt Auswärtsspiel". */
titel: string;
/** Zweitzeile: Ort oder Bündel-Teile, mit " · " getrennt. */
ort?: React.ReactNode;
/** Zeitspanne als "16:00 – 17:00" (Halbgeviertstrich mit Leerzeichen). */
zeit?: string;
/** Wer dabei ist; der Zustaendige zuerst. */
leute?: string[];
/** Sach-Symbol, wenn es nicht aus der Art folgt: "schule", "sport", "arzt", "musik". */
symbol?: string;
/** "kurz" (78 px) oder "hoch" (132 px) - fuer das Stundenraster. */
hoehe?: "kurz" | "hoch";
/** Zustands- und Fahrt-Pillen. */
pillen?: React.ReactNode;
/** Handlungsknoepfe; bei Vorschlaegen immer zwei. */
knoepfe?: React.ReactNode;
/** Kleine Zeile unter dem Inhalt, z. B. "Vorschlag von Albert". */
unterzeile?: React.ReactNode;
/** Einzeiler fuer Listen: Symbol, Titel, Zeit, Punkt. */
kompakt?: boolean;
klickbar?: boolean;
/** Leicht gekippt, wie ein liegender Klebezettel. */
gekippt?: boolean;
children?: React.ReactNode;
style?: React.CSSProperties;
}
export function Karte(props: KarteProps): JSX.Element;
export function KarteVorschlagKnoepfe(props: { uebernehmen?: string; zurueck?: string; onUebernehmen?: () => void; onZurueck?: () => void }): JSX.Element;
Drei Träger, drei Orte: Kartenfarbe sagt die Art, Symbol sagt die Sache, Punkte sagen die Leute. Wird nur von Karte gebraucht – eigenständig nur, wenn du eine eigene Kartenform baust.
Types (.d.ts)
/** Der Kopf jeder Karte: Sach-Symbol links, Punktgruppe rechts. */
export interface KartenkopfProps {
/** Sach-Rolle - bestimmt das Symbol, nicht die Kartenfarbe. */
art?: string;
leute?: Array<string>;
/** Durchmesser des Symbolkreises; die Punkte folgen mit 77 %. */
groesse?: number;
titel?: string;
style?: React.CSSProperties;
}
export function Kartenkopf(props: KartenkopfProps): JSX.Element;
Die Karte für zwei Sachen, die sich treffen – sie zeigt es und hält niemanden auf.
<KonfliktKarte titel="Lena schwimmt, Felix spielt Handball – beides um 16:00."
erklaerung="Ein Auto, zwei Orte. Sofia ist bis 17:00 da." wer={["lena", "felix"]}
schritt="Sofia fragen" zweitSchritt="Später" />
Register Family Manager. Kein Rot, kein Ausrufezeichen, kein "Fehler" – die Karte stellt fest und nennt einen Schritt.
Types (.d.ts)
/** Zwei Sachen treffen sich. Sichtbar, nie blockierend, mit naechstem Schritt. */
export interface KonfliktKarteProps {
/** Was sich trifft, als Feststellung: "Lena schwimmt und Felix spielt - beides 16:00." */
titel: string;
/** Warum es klemmt, ein Satz. */
erklaerung?: React.ReactNode;
wer?: string[];
/** Der naechste Schritt als Verb: "Tobias fragen". */
schritt?: string;
onSchritt?: () => void;
zweitSchritt?: string;
onZweitSchritt?: () => void;
style?: React.CSSProperties;
}
export function KonfliktKarte(props: KonfliktKarteProps): JSX.Element;
Rückfahrt vom Auswärtsspiel, 14:00. Tobias hat ab 13:00 frei.
Vorschlag von Albert
Alberts Vorschlag auf dem iPhone – gelb, mit Formen, immer mit zwei Knöpfen.
<VorschlagKarte titel="Samstag fehlt ein Fahrer."
text="Rückfahrt vom Auswärtsspiel, 14:00. Bei Tobias steht ab 13:00 nichts."
schritt="Tobias fragen" zweitSchritt="Später" />
So klingt Albert: eine Beobachtung, eine Frage, zwei Knöpfe. Nie "Ich habe für dich…". Auf dem Mac nimmst du stattdessen <Karte art="vorschlag"> - dort ist der Vorschlag gestrichelt und ohne Füllung.
Types (.d.ts)
/**
* Alberts Vorschlag am iPhone: gelbe Karte mit Formen, zwei Knoepfe, Unterzeile.
*/
export interface VorschlagKarteProps {
/** Eine Beobachtung: "Samstag fehlt ein Fahrer." */
titel: string;
/** Zwei kurze Saetze: was er sieht, was daraus folgt. */
text?: React.ReactNode;
/** Erster Knopf, ein Verb: "Tobias fragen", "Übernehmen". */
schritt?: string;
onSchritt?: () => void;
/** Zweiter Knopf. Es gibt immer zwei - Albert erledigt nie still. */
zweitSchritt?: string;
onZweitSchritt?: () => void;
unterzeile?: string;
style?: React.CSSProperties;
}
export function VorschlagKarte(props: VorschlagKarteProps): JSX.Element;
The header of every surface: greeting, date, search, areas, a filter by person, the span of the view. On the Mac it sits above the week board, on the iPhone above the day.
<Anredezeile>Guten Morgen, Anna.</Anredezeile>
<Anredezeile geraet="phone" nebenzeile="3. September · KW 36">Mittwoch</Anredezeile>
Register Erzähler, wenn sie grüßt; Family Manager, wenn sie nur benennt. Punkt am Satzende, nie ein Ausrufezeichen. "du" zum Einzelnen, "ihr" zur Familie.
Types (.d.ts)
/** Die grosse Zeile oben auf der Flaeche - Bricolage 800, eng gelaufen. */
export interface AnredezeileProps {
/** "Guten Morgen, Anna." | "Mittwoch" | "Woche 36" */
children: React.ReactNode;
/** 40 px auf dem Mac, 30 px auf dem iPhone. */
geraet?: "mac" | "phone";
/** Kleine Zeile darunter: "3. September · KW 36". */
nebenzeile?: React.ReactNode;
style?: React.CSSProperties;
}
export function Anredezeile(props: AnredezeileProps): JSX.Element;
Die Reihenfolge der Kreise steht fest: Klingel, Nachrichten, Plus – Plus ist rosa und immer der letzte. Chips sind gestrichelt, weil sie Bereiche einschränken und nichts anlegen.
Types (.d.ts)
/**
* Der Kopf jeder Flaeche: Suchfeld mit rosa Lupe, Bereichs-Chips, drei Kreise rechts.
*/
export interface KopfzeileProps {
platzhalter?: string;
/** Bereichs-Chips, gestrichelt: "Termine", "Fahrten", "Aufgaben", "Dokumente", { name: "Post", zaehler: 2 }. */
bereiche?: Array<string | { name: string; zaehler?: number }>;
gewaehlterBereich?: string;
onBereich?: (name: string) => void;
onSuche?: (e: React.ChangeEvent<HTMLInputElement>) => void;
/** Rosa Punkt an der Klingel, wenn etwas Neues da ist. */
klingelPunkt?: boolean;
onKlingel?: () => void;
onNachrichten?: () => void;
/** Oeffnet das Schnellmenue "Schnell anlegen". */
onPlus?: () => void;
style?: React.CSSProperties;
}
export function Kopfzeile(props: KopfzeileProps): JSX.Element;
Der erste Kreis ist schwarz mit zwei Köpfen und heisst "alle". Personenfarben sind überall dieselben; jeder Kreis trägt seine Initiale, damit Farbe nicht allein trägt.
Types (.d.ts)
/**
* Die Kreise, mit denen man die Flaeche auf Leute einschraenkt.
*/
export interface PersonenFilterProps {
/** Schluessel aus Familie Sonntag, in fester Reihenfolge. */
leute: string[];
/** Leeres Feld heisst alle. */
gewaehlt?: string[];
/** 32 px auf dem Mac, 44 px am iPhone, 56 px im Kinderbereich. */
groesse?: number;
onWahl?: (gewaehlt: string[]) => void;
style?: React.CSSProperties;
}
export function PersonenFilter(props: PersonenFilterProps): JSX.Element;
The black bar is a signature of Hey Albert. On the Mac it sits on the left, full at 220 px or collapsed at 72 px. On the iPhone it sits at the bottom, fixed, with the protruding plus.
Der aktive Ort trägt einen rosa Strich links und ein aufgehelltes Feld. Eingeklappt als fassung="pille" tritt er als rosa Kreis seitlich heraus und die Leiste beult sich um ihn. Albert steht nicht in der Leiste – er ist kein Ort.
Das Logo lädt aus assets/logo-albert-hell.png; setze auf <html>data-albert-logo="<pfad>", wenn es woanders liegt.
Types (.d.ts)
/**
* Die schwarze Leiste auf dem Mac - voll (220 px) oder eingeklappt (72 px) als Block oder als Pille.
*/
export interface LeisteOrt { name: string; zeichen: string; zaehler?: number }
export interface LeisteGruppe { gruppe: string; eintraege: LeisteOrt[] }
export interface LeisteMacProps {
/** Der Ort, auf dem man steht: "Heute" | "Woche" | "Eingang" | "Essen" | ... */
aktiverOrt?: string;
/** Gruppen der Navigation. Standard: Flächen, Bereiche, Persönlich. */
orte?: LeisteGruppe[];
eingeklappt?: boolean;
/** Eingeklappt gibt es zwei Fassungen: schmaler "block" oder rundum abgerundete "pille",
* bei der der aktive Ort als rosa Kreis seitlich heraustritt. */
fassung?: "block" | "pille";
/** Der Mensch unten in der Leiste. */
ich?: string;
onWahl?: (ort: string) => void;
onKlappen?: () => void;
onProfil?: () => void;
style?: React.CSSProperties;
}
export function LeisteMac(props: LeisteMacProps): JSX.Element;
export const ORTE: LeisteGruppe[];
Everything that orders the week: the day tabs above the grid, the hour grid itself, the now line, the week as a list and the week overview. They all show the same things from the same data model.
Rosa heisst hier "jetzt" – niemals "Fehler". Die Linie ist gestrichelt, läuft über die ganze Breite und fängt keine Klicks.
Types (.d.ts)
/** Gestrichelte Achse quer durch das Raster, mit der Uhrzeit als rosa Pille. */
export interface JetztLinieProps {
/** Uhrzeit als HH:MM. */
zeit?: string;
/** px von oben, wenn die Linie im Raster absolut sitzt. */
oben?: number;
style?: React.CSSProperties;
}
export function JetztLinie(props: JetztLinieProps): JSX.Element;
Die Woche als Liste – für das iPhone, wo ein Raster zu dicht wird.
<Listenzeile tag="Mi 3." heute text="heute · Anna und Felix" wert="3,0 h" aufgeklappt>
<Karte kompakt titel="Elternabend Klasse 6" symbol="schule" zeit="19:00" leute={["anna"]} />
</Listenzeile>
Der Text sagt, wer den Tag trägt, nicht wie voll er ist. Zahlen deutsch: "3,0 h".
Types (.d.ts)
/** Eine Zeile der Woche als Liste: Tag-Pille, Satzteil, verplante Zeit. */
export interface ListenzeileProps {
/** Tag als Pille: "Mo 1.", "Mi 3.". */
tag: string;
heute?: boolean;
/** Ein Satzteil, klein: "heute · Anna und Felix", "vorbei · zum Aufklappen". */
text?: React.ReactNode;
/** Verplante Zeit rechts: "3,0 h". */
wert?: string;
/** Setzt das Chevron; undefined heisst kein Chevron. */
aufgeklappt?: boolean;
onClick?: () => void;
/** Die Karten des Tages, wenn die Zeile offen ist. */
children?: React.ReactNode;
style?: React.CSSProperties;
}
export function Listenzeile(props: ListenzeileProps): JSX.Element;
Heute ist schwarz, der Sonntag steht in gedämpftem Rosa-Grau. Am iPhone heisst ein rosa Kreis: an diesem Tag ist etwas offen.
Types (.d.ts)
/**
* Die Reiter ueber dem Raster: Wochentag in Kapitaelchen, Datum darunter.
*/
export interface Tag {
/** Wochentag: "MONTAG" auf dem Mac, "MO" am iPhone.
* Das ist der Schluessel: onWahl meldet diesen Wert zurueck, und "gewaehlt" wird dagegen geprueft. */
tag: string;
/** Datum: "1.9." auf dem Mac, "1" am iPhone. */
zahl: string;
/** Nur Mac: eigener Schluessel fuer onWahl, wenn "tag" nicht eindeutig ist. */
name?: string;
heute?: boolean;
/** Am iPhone: Tag mit Offenem bekommt einen rosa Kreis. */
offen?: boolean;
}
export interface TagesreiterProps {
tage: Tag[];
/** Am iPhone der gewaehlte Tag - derselbe Wert wie "tag", z. B. "MI". */
gewaehlt?: string;
variante?: "mac" | "phone";
/** Kalenderwoche links vor den Reitern - nur auf dem Mac. */
kw?: string | number;
onWahl?: (tag: string) => void;
style?: React.CSSProperties;
}
export function Tagesreiter(props: TagesreiterProps): JSX.Element;
Jede Sache ist ein kleiner Zettel: die Initiale sagt wer, das Zeichen sagt was. Der Tag mit Offenem zeigt es laut — gestrichelter Zettel, und oben zählt „1 ohne Zuständigen“. Dieselben Daten wie Raster und Liste; das ist der Blick, den man am iPhone im Vorbeigehen wirft.
Types (.d.ts)
/**
* Die Woche auf einen Blick: wer trägt welchen Tag, wo fehlt jemand.
* Je Sache ein kleiner Zettel — Initiale in Personenfarbe plus Zeichen der Sache.
*/
export interface UeberblickSache {
/** Personenschlüssel des Zuständigen. */
wer?: string;
/** Sach-Rolle für das Zeichen: "fahrt", "schule", "sport", "essen", "arzt", "musik", "freude", "ritual". */
art?: string;
titel?: string;
/** Ohne Zuständigen: gestrichelter Zettel mit „?“ in Offen-Rosa. Zählt oben mit. */
offen?: boolean;
/** Familie und Ritual: gelber Zettel. */
familie?: boolean;
}
export interface UeberblickTag {
/** Kurzform: "MO" … "SO". */
tag: string;
zahl: string;
heute?: boolean;
/** Verplante Zeit des Tages: "3,0 h". */
stunden?: string;
sachen?: UeberblickSache[];
}
export interface WochenueberblickProps {
/** Überschrift der Karte. Standard „Wer trägt die Woche“. */
titel?: string;
tage: UeberblickTag[];
style?: React.CSSProperties;
}
export function Wochenueberblick(props: WochenueberblickProps): JSX.Element;
Eine Spalte je Tag (Woche) oder je Mensch (Spuren). Uhrzeiten immer HH:MM; die Achse läuft in Roboto Mono mit tabellarischen Ziffern.
Die Achse setzt nur den Anfang und eine Mindesthöhe aus der Dauer. Sie deckelt nie – eine Karte wird so hoch, wie ihr Inhalt braucht, und legt sich bei Bedarf über die nächste (spätere Sachen liegen oben). Gib der Karte darum kein height und kein overflow: hidden mit, sonst schneidest du ihren Inhalt ab.
Types (.d.ts)
/**
* Das Stundenraster: Achse links, eine Spalte je Tag oder Mensch, Sachen nach Uhrzeit gesetzt.
*/
export interface ZeitachseSache {
/** Beginn als HH:MM. */
von: string;
/** Ende als HH:MM; ohne Ende bekommt die Sache ihre Mindesthoehe. */
bis?: string;
/** Was gezeichnet wird - in der Regel eine <Karte>. */
element: React.ReactNode;
/** Zusaetzliche Mindesthoehe in px, damit eine kurze Sache lesbar bleibt.
* Die Dauer setzt ebenfalls eine Mindesthoehe - gedeckelt wird nie, Karten wachsen mit ihrem Inhalt. */
mindest?: number;
}
export interface ZeitachseSpalte { name?: string; sachen?: ZeitachseSache[] }
export interface ZeitachseProps {
von?: string;
bis?: string;
/** Abstand der Marken in Minuten. Standard 30. */
schritt?: number;
/** Hoehe einer Stunde in px. Standard 80. */
pxProStunde?: number;
spalten: ZeitachseSpalte[];
/** Uhrzeit der Jetzt-Linie, z. B. "16:40". Ohne Angabe keine Linie. */
jetzt?: string;
style?: React.CSSProperties;
}
export function Zeitachse(props: ZeitachseProps): JSX.Element;
Empty, loading, confirmed. An empty state says what belongs here, why it is empty and what you can do. Loading shows a skeleton in the shape of the content, never a spinner. Every change gets a toast with „Rückgängig“ (Undo).
<LeererZustand mitAlbert
was="Hier landet die Post der Schule."
warum="Diese Woche kam nichts an. Sobald etwas kommt, macht Albert daraus einen Vorschlag."
schritt="Post verbinden"
randnotiz="Ich schaue täglich rein." />
Kein "Fehler", kein "Ups", kein Ausrufezeichen. Albert erscheint hier als Figur – in Listen, im Safe, bei Finanzen nie.
Types (.d.ts)
/**
* Der leere Zustand: was hierher gehoert, warum es leer ist, ein Knopf.
*/
export interface LeererZustandProps {
/** Was hierher gehoert - ein Satz, kein Titel: "Hier landet die Post der Schule." */
was: React.ReactNode;
/** Warum es leer ist: "Diese Woche kam nichts an." */
warum?: React.ReactNode;
/** Was man tun kann, als Verb: "Post verbinden". */
schritt?: string;
onSchritt?: () => void;
/** Albert als Figur - nur in Onboarding, leeren Zustaenden, Briefings, Kinderbereich, Jahresbuch. */
mitAlbert?: boolean;
/** Alberts Bleistift-Randnotiz in Reenie Beanie. Nur im Register Erzaehler. */
randnotiz?: string;
style?: React.CSSProperties;
}
export function LeererZustand(props: LeererZustandProps): JSX.Element;
Nicht verhandelbar: keine Rangliste, kein Geschwistervergleich, kein Abzug, kein roter Stand, kein Serien-Druck. Der Streifen zeigt das Sparziel – nie, wie viel jemand "aufholen" muss. Zielflächen 56 px.
Types (.d.ts)
/**
* Der Taler-Stand eines Kindes: grosse Zahl, Sparziel als Streifen, Regal der Eltern.
*/
export interface TalerStandProps {
/** Das Kind - jedes Kind sieht nur den eigenen Stand. */
wer?: string;
/** Erreichte Taler. Nie negativ, es gibt keinen Abzug. */
taler: number;
/** Taler, die das Sparziel kostet. */
ziel?: number;
/** Name des Sparziels: "Skateboard". */
zielName?: string;
/** Was das Kind geschafft hat, als Pillen. Nie eine Quote. */
geschafft?: string[];
/** Das Regal der Eltern: was man sich holen kann und was es kostet. */
regal?: Array<{ name: string; taler: number }>;
style?: React.CSSProperties;
}
export function TalerStand(props: TalerStandProps): JSX.Element;
From paper to suggestion: camera with edge frame, page stack, then a suggestion with sender, amount, deadline and filing place. Nothing is filed until a person chooses „Übernehmen“ (Accept).
Der Vorschlag läuft im Register Sachbearbeiter: nüchtern, vollständig, kein Schmuck, keine Formen. „Übernehmen“ und „Rückgängig“ stehen immer beide da — Albert legt nichts still ab.
Types (.d.ts)
/**
* Der Scanner-Fluss: Kamera mit Kantenrahmen, Seitenstapel, Vorschlagskarte.
*/
export interface ScannerVorschlag {
/** Was Albert gelesen hat, als Gegenstand: "Rechnung Musikschule". */
titel: string;
absender?: string;
/** Betrag deutsch: "48,00 €". */
betrag?: string;
/** Frist als Datum: "18. September". */
frist?: string;
/** Wohin es soll: "Wichtiges › Verträge". */
ablage?: string;
}
export interface ScannerFlussProps {
schritt?: "kamera" | "stapel" | "vorschlag";
/** Zahl der aufgenommenen Seiten. */
seiten?: number;
vorschlag?: ScannerVorschlag;
onAufnehmen?: () => void;
onUebernehmen?: () => void;
onZurueck?: () => void;
style?: React.CSSProperties;
}
export function ScannerFluss(props: ScannerFlussProps): JSX.Element;
The homepage components, taken from the homepage blueprint. One goal per screen, one verb per button, one field per form. The website shares its palette and tokens with the app. There is no separate token set for the website.
Die Anmerkung in Tinte – drei bis vier auf der Heldenansicht, absolut über dem Bild gesetzt.
<Anmerkung text="Gestrichelt in Rosa: alles, wofür noch niemand zuständig ist" richtung="links" style={{ position: "absolute", left: 620, top: 300 }} />
Handschrift (Reenie Beanie) ist hier erlaubt: Register Erzähler, keine Bedienung.
Types (.d.ts)
/** Anmerkungslinie in Tinte, die auf ein Detail im Bild zeigt. */
export interface AnmerkungProps {
text: string;
/** Wohin der Zeiger geht. */
richtung?: "links" | "rechts";
/** Laenge der Linie in px. */
laenge?: number;
style?: React.CSSProperties;
}
export function Anmerkung(props: AnmerkungProps): JSX.Element;
TuschezeichnungTurnbeutel am Haken, ein gelber Marker
Foto · ohne Akzent
FotoDer Gründer am Küchentisch
Der Platzhalter für Zeichnung, Foto oder Screenshot auf der Website.
<Bildplatz was="Tuschezeichnung" motiv="Turnbeutel am Haken, ein gelber Marker" />
<Bildplatz was="Foto" motiv="Das gedruckte Jahresbuch, aufgeschlagen, Küchentisch von oben" verhaeltnis="16 / 10" akzent={false} />
Regeln fürs Bild: Tinte auf Papier, genau ein gelber Akzent, kein Stockfoto, kein 3D, kein KI-Bild, keine Kindergesichter. Genau ein echtes Foto auf der ganzen Seite – der Gründer.
Types (.d.ts)
/** Platzhalter fuer eine Tuschezeichnung, ein Foto oder einen Screenshot - sagt, was hierher gehoert. */
export interface BildplatzProps {
/** "Tuschezeichnung" | "Foto" | "Screenshot" */
was?: string;
/** Das Motiv in einem Satzteil: "Turnbeutel am Haken, gelber Marker". */
motiv?: string;
/** CSS aspect-ratio, z. B. "4 / 3", "1 / 1", "16 / 10". */
verhaeltnis?: string;
/** Der eine gelbe Markerakzent. Aus, wenn das Bild selbst einen traegt. */
akzent?: boolean;
children?: React.ReactNode;
style?: React.CSSProperties;
}
export function Bildplatz(props: BildplatzProps): JSX.Element;
Der Plan, der die Einkaufsliste schreibtIhr legt die Essen der Woche fest. Albert setzt die Zutaten auf die Liste, Sofia kauft am Donnerstag ein.
Die Modul-Reihe – sechs auf der Homepage, nicht einundzwanzig.
<ModulReihe rollenname="Wer fährt, wen, womit, wann los" bildRechts
text="Fahrzeit und Puffer rechnet Albert. Fahrten ohne Fahrer sind gestrichelt – Oma darf übernehmen."
bild={<Bildplatz was="Screenshot" motiv="Fahrtenkarte mit Fahrer-Punkten" />} />
Rollenname statt Feature-Name. Höchstens 40 Wörter je Modul.
Types (.d.ts)
/** Eine Modul-Reihe: Rollenname, zwei Saetze, Bild - abwechselnd links und rechts. */
export interface ModulReiheProps {
/** Der Rollenname, kein Feature-Name: "Der Plan, der die Einkaufsliste schreibt". */
rollenname: string;
/** Zwei Saetze. */
text: React.ReactNode;
bild: React.ReactNode;
bildRechts?: boolean;
/** Am Handy untereinander, Bild oben. */
gestapelt?: boolean;
style?: React.CSSProperties;
}
export function ModulReihe(props: ModulReiheProps): JSX.Element;
Alle Familienmitglieder, Gäste und Helfer inklusive.
Monatlich kündbar, in der App.
Wir sagen euch den Preis, bevor ihr etwas eingebt.
Die Preiskarte – eine, nicht drei.
<PreisKarte monat="X,XX €" jahr="XX €" hinweis="Wir sagen euch den Preis, bevor ihr etwas eingebt." />
Register Sachbearbeiter. Kein "ab", kein Sternchen, keine versteckte Stufe. Steht der Preis nicht fest, sagt die Karte genau das.
Types (.d.ts)
/** Die eine Preiskarte. Ein Preis, alles drin. */
export interface PreisKarteProps {
titel?: string;
/** Monatspreis in Tabellenziffern: "6,99 €". Platzhalter "X,XX €", solange der Preis nicht feststeht. */
monat?: string;
jahr?: string;
zeilen?: string[];
knopf?: string;
onKnopf?: () => void;
/** "30 Tage kostenlos, ohne Kartendaten" - nur wenn es stimmt. */
hinweis?: string;
style?: React.CSSProperties;
}
export function PreisKarte(props: PreisKarteProps): JSX.Element;
SehenJede Person hat ihre Farbe, jeder Tag seine Spalte. Was ohne Zuständigen ist, trägt einen gestrichelten Rand.
Der Schritt – Nummer, Ausschnitt, ein Wort, zwei Sätze.
<Schritt nummer={2} titel="Sehen" text="Jede Person hat ihre Farbe, jeder Tag seine Spalte. Was ohne Zuständigen ist, trägt einen gestrichelten Rand – bis jemand es nimmt.">
<Karte art="offen" titel="Rückfahrt Turnier" zeit="18:40" symbol="fahrt" />
</Schritt>
Drei Schritte sind die Grenze – vier werden nicht mehr gelesen.
Types (.d.ts)
/** Ein Schritt in "So funktioniert es". Drei Schritte, nie vier. */
export interface SchrittProps {
nummer: number | string;
/** Ein Wort: "Sagen", "Sehen", "Sonntag". */
titel: string;
/** Zwei Saetze. */
text: React.ReactNode;
/** Der kleine Ausschnitt aus dem Produkt. */
children?: React.ReactNode;
style?: React.CSSProperties;
}
export function Schritt(props: SchrittProps): JSX.Element;
Für iPhone, iPad und Mac · Daten in Frankfurt · Albert liest keine Mail ohne Regel
Die kleine Zeile unter dem Knopf.
<Vertrauenszeile />
Drei Fakten, überprüfbar, ohne Siegel. Nie "100 % sicher", nie ein Schloss.
Types (.d.ts)
/** Kleine Zeile unter dem Knopf: drei ueberpruefbare Fakten, mit Punkt getrennt. */
export interface VertrauenszeileProps {
fakten?: string[];
style?: React.CSSProperties;
}
export function Vertrauenszeile(props: VertrauenszeileProps): JSX.Element;
Ein Knopf, nie zwei – drei oder mehr Knöpfe im Hero kosten. Keine Mega-Menüs. Der Knopf nennt die Folge, kein "Los geht's". Links: Funktionen · Vertrauen · Preis · Fragen.
Die Marke steht als schwarzer Kreis mit dem hellen Kritzelball und der Wortmarke in Bricolage – so bleibt sie auf Creme ruhig. Der Pfad zur Datei kommt aus data-albert-logo-auf-schwarz auf <html> (Vorgabe: assets/logo-albert-auf-schwarz.png).
Types (.d.ts)
/** Die Navigation der Website: Logo, drei bis vier Links, ein Knopf. */
export interface WebNavigationProps {
links?: string[];
/** Der eine Knopf. Nennt die Folge: "Auf die Liste setzen", nach dem Start "Albert laden". */
knopf?: string;
onKnopf?: () => void;
onLink?: (link: string) => void;
/** Am Handy: nur Logo und Knopf. */
geraet?: "desktop" | "phone";
style?: React.CSSProperties;
}
export function WebNavigation(props: WebNavigationProps): JSX.Element;
Two components for Hey Albert HQ, the team’s internal tool. They show families and the path from the waiting list to payment. They do not appear in the family app.
Drei Ringe, gestrichelt nach aussen hin, der Kern mit fester Linie. Jede Person als Klebezettel in ihrer Personenfarbe mit Initialen, der Vorname darunter. In der Mitte die Initialen des Haushalts auf Tinte. Register Family Manager: Beschriftungen sind Wörter, keine Sätze – "Kern", "Familie", "Freunde und Helfer".
Types (.d.ts)
/** Das Satellitenbild einer Familie: Kern, Familie, Freunde und Helfer als drei Ringe um den Haushalt. */
export interface SatellitMitglied {
id?: string; name: string;
/** Rolle im Haushalt: owner | adult | child | guest | service */
rolle: "owner" | "adult" | "child" | "guest" | "service";
/** Verwandtschaft (albert.kinship): mother, father, partner, daughter, son, grandmother, grandfather, aunt, uncle, au_pair, household_help, friend, other */
beziehung?: string | null;
/** Personenfarbe 1–8 (6 und 8 sind nicht vergeben). */
farbe?: number | null;
}
export interface SatellitProps {
mitglieder: SatellitMitglied[];
/** Name des Haushalts, Initialen in der Mitte. */
name?: string;
/** Beschriftung der Ringe. */
worte?: { kern: string; familie: string; freunde: string };
style?: React.CSSProperties;
}
export function Satellit(props: SatellitProps): JSX.Element;
/** Welcher Ring: kern (owner, adult, child), familie (Gast mit Verwandtschaft), freunde (alles andere). */
export function kreisVon(m: SatellitMitglied): "kern" | "familie" | "freunde";
Balken in Gelb (Fläche Familie), Zahl in Tabellenziffern, die Quote zur Stufe davor in Zweittext. Stufennamen klein geschrieben, wie Zustände – keine Sätze, kein Ausrufezeichen.
Types (.d.ts)
/** Stufen von der Liste bis zur Zahlung als Balken mit Quote zur Stufe davor. */
export interface TrichterStufe { name: string; n: number }
export interface TrichterProps { stufen: TrichterStufe[]; style?: React.CSSProperties }
export function Trichter(props: TrichterProps): JSX.Element;
For forms, dialogs and tables on the web there are ten primitives modelled on shadcn/ui. They live in bauteile/ui/ and run on our roles: tailwind/shadcn.css maps the shadcn variables to --grund, --karte, --tinte and the other roles. Sizes are target sizes: pointer 28, finger 44, child 56.
The website is built from blocks managed in Payload. For every block slug in packages/content-model/src/bloecke.ts there is exactly one component in bloecke/. RenderBloecke picks by blockType, filters by market and passes on language and market. pnpm test in the design system checks that slug and component match.
No component is born in just one app. Anything new built in apps/web, in AlbertUI or on the website comes here first, or at the latest in the same commit.
Check whether the component already exists. Often a new kind of an existing component is enough.
Create four files in the right folder under components/: .jsx, .d.ts, .prompt.md and a @dsCard card with every state.
Use role tokens only. If a role is missing, add it to tokens/quelle/, and pnpm tokens regenerates CSS, Tailwind and Swift.
Add the entry to _ds_manifest.json and to the list in readme.md.
Run pnpm qa: no hex value, no shadow, no banned word, no exclamation mark, no image without alt, no free radius.
Let the design-pruefer agent check the definition of done.