Skip to content
Hey AlbertBrand and design system
As MarkdownDeutschDE

Components

Patterns

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.

Bundle (Bündel)

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.

BuendelKarte

components/karten/BuendelKarte.jsx
Drei Sachen um 16:0016:00 – 19:00
Kieferorthopädin16:00AL
Training16:00F
Abholen Halle Süd19:00

Die Antwort auf Gleichzeitiges: eine Karte, drei Zeilen – statt drei Karten, die sich stapeln.

<BuendelKarte titel="Drei Sachen um 16:00" zeit="16:00 – 17:30" teile={[
  { titel: "Kieferorthopädin", symbol: "arzt", zeit: "16:00 – 17:00", leute: ["anna", "lena"] },
  { titel: "Training", art: "termin", symbol: "sport", zeit: "16:00 – 17:30", leute: ["felix"] },
  { titel: "Abholen Halle Süd", art: "fahrt", zeit: "19:00", offen: true }
]} />

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;

Suggestion from Albert (Vorschlag)

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.

VorschlagKarte

components/karten/VorschlagKarte.jsx
Samstag fehlt ein Fahrer.
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.

Conflict (Konflikt)

Two things collide. Albert states the facts, does not judge them and blocks nothing. The conflict card carries a next step as a verb.

KonfliktKarte

components/karten/KonfliktKarte.jsx
LF
Lena schwimmt, Felix spielt – beides um 16:00.
Ein Auto, zwei Orte. Sofia ist bis 17:00 da.

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;

No one responsible (ohne Zuständigen)

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.

Karte

components/karten/Karte.jsx
AL
Kieferorthopädin
Praxis Dr. Voss
16:00 – 17:00
TF
Handball, Halle Süd
Schule → Halle Süd
Fr 16:40
Tobias fährt 16:40 · Kombi
ATFL
Mittagessen bei Oma Grete
So 12:30 · alle
Bei Tobias steht ab 13:00 nichts. Fragen?
Vorschlag von Albert
Abholen Halle Süd
19:00
Ohne Zuständigen
Mathe mit Felix17:30F

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.

Lanes per person (Spuren je Mensch)

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.

SpurenJeMensch

components/karten/SpurenJeMensch.jsx
15:0016:0017:0018:0019:00
AAnna
Kieferorthopädin
TTobias
Abholen Halle Süd
SSofia
Abholen Schule

Spuren je Mensch – wenn drei Sachen gleichzeitig laufen und die Frage "wer wo" lautet.

<SpurenJeMensch leute={["anna", "tobias", "sofia"]} von={15} bis={20} spuren={{
  anna:   [{ titel: "Kieferorthopädin", von: 16, bis: 17 }],
  tobias: [{ titel: "Abholen Halle Süd", von: 19, bis: 19.5, art: "fahrt", offen: true }],
  sofia:  [{ titel: "Abholen Schule", von: 15, bis: 15.5, art: "fahrt" }]
}} />

Jede Spur trägt Name und Avatar, nicht nur Farbe. Dieselben Daten wie im Stundenraster – ein Datenmodell, viele Ansichten.

Types (.d.ts)
/** Je Mensch eine Spur ueber die Stunden - die dritte Antwort auf Gleichzeitiges. */
export interface Spur {
  titel: string;
  /** Stunde als Dezimalzahl: 16.5 heisst 16:30. */
  von: number;
  bis: number;
  art?: "termin" | "fahrt" | "familie";
  offen?: boolean;
}
export interface SpurenJeMenschProps {
  /** Schluessel aus Familie Sonntag, eine Spur je Person. */
  leute: string[];
  /** Spuren je Personenschluessel. */
  spuren: Record<string, Spur[]>;
  /** Erste und letzte Stunde der Achse. */
  von?: number;
  bis?: number;
  style?: React.CSSProperties;
}
export function SpurenJeMensch(props: SpurenJeMenschProps): JSX.Element;

Which answer fits when:

SituationPattern
Several things in the same hour, each with a person responsiblebundle card
Two things need the same person or the same carconflict card
You are looking for who is free in the afternoonlanes per person

Button row (Knopfreihe)

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.

Knopfreihe

components/bausteine/Knopf.jsx
Genug Platz · nebeneinander
Eng · gestapelt, volle Breite

Forms

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.

EmailZeile

components/website/EmailZeile.jsx
Tisch
Handy · gestapelt

Das eine Feld der Website.

<EmailZeile onSenden={email => merken(email)} />

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.)

Lists

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.

Listenzeile

components/zeit/Listenzeile.jsx
Elternabend Klasse 619:00A

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.

Scan flow

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.

ScannerFluss

components/scan/ScannerFluss.jsx
Kamera
Halte das Blatt in den Rahmen. Albert liest es gleich.
2 Seiten
Seitenstapel
123
Vorschlag
Rechnung Musikschule
Absender
Musikschule Nord
Betrag
48,00 €
Frist
18. September
Ablage
Wichtiges › Verträge
Vorschlag von Albert

Der Scanner – drei Schritte: Kamera, Stapel, Vorschlag.

<ScannerFluss schritt="vorschlag" seiten={2} vorschlag={{
  titel: "Rechnung Musikschule", absender: "Musikschule Nord",
  betrag: "48,00 €", frist: "18. September", ablage: "Wichtiges › Verträge"
}} />

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

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.

TalerStand

components/kinder/TalerStand.jsx
LDein Stand
14Taler
Sparziel: Skateboard · 30 Taler
Das hast du geschafft
Tisch gedecktZimmer aufgeräumtSchwimmzeug gepackt
Regal der Eltern
Kino20Eis4

Der Taler-Stand für Kinder – Register Vorleser, kurze Sätze, grosse Wörter.

<TalerStand wer="lena" taler={14} ziel={30} zielName="Skateboard"
  geschafft={["Tisch gedeckt", "Zimmer aufgeräumt", "Schwimmzeug gepackt"]}
  regal={[{ name: "Kino", taler: 20 }, { name: "Eis", taler: 4 }]} />

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.)

Side sheet (Seitenblatt)

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.

Confirm and undo

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.

Toast

components/zustaende/Toast.jsx
Tobias fährt am Freitag.
Schwimmen mit Lena steht am Montag.

Der Toast – eine Feststellung und ein Rückweg.

<Toast text="Tobias fährt am Samstag." onZurueck={zurueck} />

Albert stellt fest ("Fertig."), er lobt nicht. Ohne onZurueck gibt es keinen Toast – was nicht rücknehmbar ist, wird vorher gefragt.

Types (.d.ts)
/** Kurze Rueckmeldung mit Rueckweg. Jedes optimistische Update bekommt einen. */
export interface ToastProps {
  /** Feststellung im Praesens, ohne Lob: "Tobias fährt am Samstag." */
  text: React.ReactNode;
  zurueck?: string;
  onZurueck?: () => void;
  style?: React.CSSProperties;
}
export function Toast(props: ToastProps): JSX.Element;

Delete

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.