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