Skip to content
Hey AlbertBrand and design system
As MarkdownDeutschDE

Screens

States

Empty, loading, error, offline, undo, optimistic update, conflict, no one responsible, Albert’s suggestion, disabled – each with layout, register, example text from the Sonntag family and its component.

A screen has more than one state. Without its states it is not finished. This chapter describes each one: how it looks, which register it speaks in and which component carries it.

StateComponentRegister
emptyLeererZustandfamily manager, with Albert as a character
loadingSkelett–
errorLeererZustand without the characterfamily manager or clerk
offlinenotice line (proposal)family manager
undoToastfamily manager
optimistic updatecard immediately, Toast afterfamily manager
conflictKonfliktKarte, BuendelKartefamily manager
no one responsiblecard with open outlinefamily manager
Albert’s suggestionVorschlagKarte, Karte art="vorschlag"family manager, clerk in the scan
disableddedicated roles instead of opacity–

Empty

An empty state says three things: what belongs here, why it is empty, what you can do. Plus exactly one button. Albert may appear here as a character, with a handwritten margin note. Never in lists, in the safe or in finances.

LeererZustand

components/zustaende/LeererZustand.jsx

Der leere Zustand – drei Dinge und ein Knopf.

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

Loading

Never a spinner, never a progress bar as decoration. The skeleton has the shape of what is about to arrive: seven columns for the week, three cards for Today. It breathes between --karte and --knopf-zweit over --dauer-atem. With reduced motion it stands still.

Skelett

components/zustaende/Skelett.jsx
Form des Boards
Liste und Karte

Das Skelett – die Form dessen, was gleich kommt.

<Skelett form="board" spalten={7} zeilen={3} />

Nie ein Kreisel, nie ein Fortschrittsbalken. Es atmet zwischen Karte und Zweitknopf-Beige und steht still, wenn das System weniger Bewegung will.

Types (.d.ts)
/** Skelett in der Form des Inhalts. Atmet 300 ms, steht bei reduzierter Bewegung, nie ein Kreisel. */
export interface SkelettProps {
  /** "karte" (eine Karte) | "zeile" (Liste) | "board" (Stundenraster). */
  form?: "karte" | "zeile" | "board";
  /** Zeilen je Spalte. */
  zeilen?: number;
  /** Spalten im Board. */
  spalten?: number;
  style?: React.CSSProperties;
}
export function Skelett(props: SkelettProps): JSX.Element;

The skeleton only appears when loading takes longer than a brief moment. Proposal: from 300 ms, so it does not flicker on fast responses.

Error

An error is an empty state with a different why. No “error”, no “oops”, no red, no exclamation mark. It says what did not work and offers a step. Albert does not appear as a character in errors.

Errors in safe, vault and finances speak in the clerk register (Sachbearbeiter): sober, complete, stating what is saved and what is not.

Offline

Undo

Every change reports with a toast and “Rückgängig” (Undo). Albert states facts, he does not praise. Without a way back there is no toast. Anything that cannot be undone is asked first.

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;

The toast sits above the bar on the iPhone and bottom centre on the Mac. It stays until you carry on. Proposal: at least five seconds, so that Oma Grete can read it too.

Optimistic update

Whoever changes something sees the result immediately. The app does not wait for the server. If it fails, the previous state returns and a toast says what happened. When another person changes something, it says who: “Anna hat die Fahrt übernommen.”

Conflict

Two things collide. The card shows it and holds nobody up. No red, no error. The card states it and names a step.

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;

When three things run at the same time, one card bundles them into three rows instead of stacking three cards.

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;

No one responsible

Everything has exactly one person responsible. Something without one is not a neutral state but a problem, and it is shown loudly: a dashed outline in open-pink, --rand-offen, and the line “Noch niemand” (no one yet). Never as a fill, never silently empty. The only exception is the task inbox.

Albert’s suggestion

Albert suggests and never acts alone. A suggestion is one observation, one question, two buttons. Yellow with shapes on the iPhone, dashed grey without fill on the Mac. Always with “Vorschlag von Albert” (suggestion from Albert) below.

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;

In the inbox, the suggestion speaks as the clerk: sender, amount, deadline, filing place, plus “Übernehmen” (accept) and “Rückgängig”.

Disabled

Disabled is a dedicated, computed role, not opacity: --text-inaktiv, --linie-inaktiv, --knopf-inaktiv-flaeche, --knopf-inaktiv-text. A disabled button stays readable and says why it does not work.

#E9E2D4#2B2620
#A29B8F#7A7469→ text-inaktiv
#A29B8F#7A7469gerechnet, keine Deckkraft
#E4DDCE#2B2620gerechnet, keine Deckkraft