# Patterns

> How components answer together – bundle, suggestion, conflict, no one responsible, lanes, forms, lists, scan, children, side sheet, confirm and delete.

Source: https://brand.heyalbert.family/en/muster/ · Hey Albert brand and design system

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.

> **Example**
>
> Friday, 16:40, handball at Halle Süd. Tobias drives the estate car, Felix rides along. The water bottle is on the checklist. The drive hangs on the event as a blue pill.

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

Source: `packages/design-system/components/karten/BuendelKarte.jsx`

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

```jsx
<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.

```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;
```

> **Do**
>
> Attach the drive to the event as a `FahrtPille`. The pill says who drives.

> **Don't**
>
> Car, driver and checklist as three loose cards side by side. A drive without the event it leads to.

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

> **Example**
>
> „Samstag fehlt ein Fahrer. Rückfahrt vom Auswärtsspiel, 14:00. Tobias hat ab 13:00 frei.“ (Saturday is missing a driver. Ride home from the away game, 14:00. Tobias is free from 13:00.) Below it two buttons: „Tobias fragen“ (Ask Tobias) and „Später“ (Later). After „Tobias fragen“ a toast with „Rückgängig“ (Undo) appears.

### VorschlagKarte

Source: `packages/design-system/components/karten/VorschlagKarte.jsx`

Alberts Vorschlag auf dem iPhone – gelb, mit Formen, immer mit zwei Knöpfen.

```jsx
<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.

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

> **Don't**
>
> „Albert hat 2 Aufgaben für dich erstellt.“ (Albert created 2 tasks for you.) Albert never creates anything silently, never sends anything alone and never praises.

## 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

Source: `packages/design-system/components/karten/KonfliktKarte.jsx`

Die Karte für zwei Sachen, die sich treffen – sie zeigt es und hält niemanden auf.

```jsx
<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.

```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;
```

> **Do**
>
> „Lena schwimmt, Felix spielt – beides um 16:00. Ein Auto, zwei Orte. Sofia ist bis 17:00 da.“ (Lena swims, Felix plays – both at 16:00. One car, two places. Sofia is here until 17:00.) Button: „Sofia fragen“ (Ask Sofia).

> **Don't**
>
> „Fehler: Terminkonflikt!“ (Error: scheduling conflict.) No red, no warning sign, no disabled save button. Pink never means error.

## 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

Source: `packages/design-system/components/karten/Karte.jsx`

Die Karte – das Ding, aus dem Heute, Woche und Kalender bestehen. Ein Datenmodell, viele Ansichten.

```jsx
<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.

```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;
```

> **Example**
>
> „Für die Rückfahrt am Freitag fehlt noch ein Fahrer – bei Tobias steht ab 15:00 nichts.“ (The ride home on Friday still needs a driver – Tobias has nothing from 15:00.) The card „Abholen Halle Süd, 19:00“ keeps its dashed outline until someone chooses „Ich fahre“ (I’ll drive).

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.

> **Don't**
>
> Open-pink as a fill. The outline for anything other than „no one responsible“. A thing without anyone responsible sitting grey and quiet in the list.

## 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

Source: `packages/design-system/components/karten/SpurenJeMensch.jsx`

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

```jsx
<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.

```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:

| Situation | Pattern |
|---|---|
| Several things in the same hour, each with a person responsible | bundle card |
| Two things need the same person or the same car | conflict card |
| You are looking for who is free in the afternoon | lanes 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

Source: `packages/design-system/components/bausteine/Knopf.jsx`

> **Do**
>
> A verb that names the consequence: „Fahrt übernehmen“ (Take the drive), „Woche planen“ (Plan the week), „In den Safe legen“ (Put in the safe). The first button black, the second beige.

> **Don't**
>
> „OK“, „Absenden“ (Submit), „Los geht’s!“ (Let’s go). „Abbrechen“ (Cancel) without an object. A button that wraps onto two lines.

## 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

Source: `packages/design-system/components/website/EmailZeile.jsx`

Das eine Feld der Website.

```jsx
<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.

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

> **Note**
>
> For input, select, checkbox and dialog on the web there are the web primitives in `bauteile/ui/`, on our roles. See [Components](/en/bauteile/#gruppe-ui).

## 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

Source: `packages/design-system/components/zeit/Listenzeile.jsx`

Die Woche als Liste – für das iPhone, wo ein Raster zu dicht wird.

```jsx
<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".

```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

Source: `packages/design-system/components/scan/ScannerFluss.jsx`

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

```jsx
<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.

```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;
```

> **Example**
>
> „Rechnung Musikschule · Musikschule Nord · 48,00 € · Frist 18. September · Wichtiges › Verträge“ (Music school invoice · due 18 September · Important › Contracts). Buttons: „Übernehmen“ and „Später“.

## 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

Source: `packages/design-system/components/kinder/TalerStand.jsx`

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

```jsx
<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.

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

> **Don't**
>
> „Super gemacht!!“ (Great job) with confetti. „Felix hat diese Woche mehr Taler als Lena.“ (Felix has more coins than Lena this week.) A red bar for what was missed.

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

> **Do**
>
> Tap the tile „2 Fahrten heute“ (2 drives today): the sheet opens on the right with the two drives. The overview stays behind it.

> **Open**
>
> Whether the side sheet comes to the family app as well, for example on the iPad, has not been decided. So far it is established in HQ.

## 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

Source: `packages/design-system/components/zustaende/Toast.jsx`

Der Toast – eine Feststellung und ein Rückweg.

```jsx
<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.

```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;
```

> **Do**
>
> „Fahrt übernommen. Rückgängig“ (Drive taken. Undo) · „Tobias fährt am Freitag. Rückgängig“ (Tobias drives on Friday. Undo)

> **Don't**
>
> „Erfolgreich gespeichert!“ (Saved successfully) · „Super, das hat geklappt!“ (Great, that worked)

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

> **Do**
>
> „Das Album und seine 84 Bilder werden gelöscht. Das lässt sich nicht rückgängig machen.“ (The album and its 84 pictures will be deleted. This cannot be undone.) Buttons: „Album löschen“ (Delete album) and „Behalten“ (Keep).

> **Don't**
>
> „Bist du wirklich, wirklich sicher?“ (Are you really, really sure?) A red button labelled „OK“. Deleting without asking.
