How components answer together – bundle, suggestion, conflict, no one responsible, lanes, forms, lists, scan, children, side sheet, confirm and delete.
A pattern is a recurring question with a fixed answer. How does Albert show a suggestion? What happens when two things happen at once? The answer is written down here once and then holds on every device. Each pattern names its components, its rule and an example from the Sonntag family’s week.
A thing rarely comes alone. A drive brings its car, driver, passengers and checklist along. That is guiding principle three: bundles, not loose parts. Whoever sees the drive sees the bundle. Whoever moves the drive moves the bundle with it.
When several things fall in the same hour, the bundle card gathers them. Each thing stays its own row with its own person responsible.
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;
Albert suggests and never acts alone. Every suggestion has four parts: a preview, „Übernehmen“ (Accept), a second way and an entry in the log. It is recognisable as a suggestion, dashed grey on the Mac, a yellow card on the iPhone. It names where it came from.
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 rules:
Always two buttons. The first is a verb that names the consequence: „Tobias fragen“, „Übernehmen“. The second is the other way: „Später“.
Always with its source. „Aus der Mail der Klassenlehrerin: Unterschrift und 8 € bis Freitag. Als zwei Aufgaben übernehmen?“ (From the class teacher’s email: signature and 8 € by Friday. Accept as two tasks?)
After accepting, a toast with „Rückgängig“. The change shows at once. The way back stays open.
Every action by Albert is logged. Who, what, when, from which source.
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;
Everything has exactly one person responsible. If no one is, that is not a neutral state but a problem, and it is shown loudly: a dashed outline in open-pink, a pill „Ohne Zuständigen“, a button that lets someone take it on. Albert names the gap and offers a way.
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;
The only exception is the task inbox. Something may sit there briefly without anyone responsible until it is handed out. In the week overview open items count at the top; on the iPhone a day with open items gets a pink circle in the day tabs.
Simultaneous things have three answers. The bundle card gathers, the conflict card names the clash. Lanes show one row per person across the hours. That way you see at a glance who is free when.
Buttons sit side by side when both fit. Otherwise they stack, full width. A button is always a single line. Two buttons of different widths stacked on top of each other do not exist.
A form asks only for what it really needs. On the website the rule is: one field per form, one verb per button. Every field beyond one costs roughly half of the sign-ups. That is why the waiting list has exactly one field, the email address.
Ein Feld: 12,4 % tragen sich ein; vier Felder: die Hälfte. Kein Name, kein Häkchen, kein zweites Feld.
Types (.d.ts)
/** Ein E-Mail-Feld und ein Knopf - nie mehr Felder. */
export interface EmailZeileProps {
platzhalter?: string;
knopf?: string;
onSenden?: (email: string) => void;
/** Am Handy untereinander. */
gestapelt?: boolean;
style?: React.CSSProperties;
}
export function EmailZeile(props: EmailZeileProps): JSX.Element;
In the app Albert fills in what he knows and marks it as a suggestion. A person confirms. Empty required fields do not make the button translucent; they make it disabled, as its own role. Field errors say what is missing and what would be right: „Die Uhrzeit fehlt. Zum Beispiel 16:40.“ (The time is missing. For example 16:40.)
The week as a list is the iPhone’s answer when the grid gets too narrow. Each day is a row with a day pill, a short phrase and the planned time. Today is black. A day expands and shows its cards in the compact form.
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;
Rules for lists:
A row says in a short phrase what the day holds: „heute · Anna und Felix“ (today · Anna and Felix).
Numbers sit on the right, in tabular figures: „3,0 h“.
The past is toned down but readable: „vorbei · zum Aufklappen“ (past · tap to expand).
An empty list shows an empty state, not a blank surface.
Paper becomes a suggestion in three steps: camera, page stack, suggestion. Recognition happens on the device. Only the recognised text goes to a model, never the image. The suggestion names sender, amount, deadline and filing place. Only „Übernehmen“ files it.
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;
Children take part; they are not an audience. The test question for every children’s view: could a nine-year-old use it alone? Children’s views use the 56 px target size, 16 px type on buttons and the Vorleser (reader) register.
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;
The rules for coins (Taler):
Each child sees only their own balance.
No leaderboard, no comparison between siblings, no deduction, no streak pressure.
What is visible is what has been done, never a ratio or a catch-up bar.
Parents set the values. Albert states; people praise: „Fertig. Bleibt noch: Zimmer aufräumen.“ (Done. Still to do: tidy your room.)
Additional information opens in a side sheet from the right instead of jumping to a new page. The surface behind stays visible and is dimmed by the veil (--flaeche-schleier). That way you do not lose your place. In HQ, metric tiles, contacts and emails open their side sheet. On the web the Sheet primitive in bauteile/ui/ does this.
Every change appears at once, optimistically. Then a toast follows. It states the fact, in the present tense or the past tense of the same verb, and offers „Rückgängig“ (Undo). It does not praise.
Deleting is the one thing „Rückgängig“ cannot catch. So Albert asks once and names the consequence without drama. Deleting happens in one place, with a confirmation dialog. In HQ that is the entry page and the bulk bar, never a single click in the row.