# Wie wir entwickeln

> Eine Quelle, viele Oberflächen. Wie Tokens, Bauteile und Prüfer zusammenhängen, welche Regeln im Code gelten und wann eine Ansicht fertig ist.

Quelle: https://brand.heyalbert.family/entwickeln/ · Hey Albert Marke und Designsystem

Hey Albert läuft im Browser, auf iPhone, iPad, Mac und Watch. Alle Oberflächen zeigen dieselben Zeilen aus derselben Datenbank. Und alle tragen dieselbe Gestaltung aus derselben Quelle: `packages/design-system`.

Dieses Kapitel ist für alle, die Code schreiben. Das gilt für Menschen im Team, für externe Fachleute und für Agenten.

## Eine Quelle

Jeder Wert der Marke steht genau einmal im Repository. Farben, Maße, Schriften und Bewegungen liegen in `packages/design-system/tokens/quelle/*.json`. Alles andere verweist darauf.

| Was | Wo | Wird zu |
|---|---|---|
| Tokens | `tokens/quelle/*.json` (Format der W3C Design Tokens Community Group) | `tokens/*.css`, `tailwind/theme.css`, `swift/Tokens.swift` |
| Bauteile | `components/<gruppe>/<Name>.jsx` mit `.d.ts` und `.prompt.md` | Vorlage für `apps/web/src/bausteine` und für SwiftUI |
| Web-Primitive | `bauteile/ui/*.tsx` (shadcn auf unseren Rollen) | Formulare, Dialoge, Tabellen im HQ und auf der Website |
| Website-Blöcke | `bloecke/*.tsx`, eins zu eins zu `packages/content-model` | die Seiten von hey-albert.app aus Payload |
| Regeln | `regeln/regeln.json` | diese Seite, Brand-QA, Agenten |
| Schriften, Zeichen, Logos | `assets/` | alle Oberflächen, lokal ausgeliefert |

> **Warum nur eine Quelle**
>
> Zwei Quellen für dieselbe Sache sind keine Redundanz, sondern eine Gabelung. Die erste Brand-Seite vom 30. August wurde am 4. September gelöscht, weil sie eigene Werte trug. Auch diese Seite enthält keinen Wert. Alles hier wird beim Bau aus dem Paket gelesen.

## Die Reihenfolge

Jeder Bereich entsteht in derselben Folge. So steht das Fundament, bevor jemand eine Fläche sieht.

1. **Schema und RLS.** Die Tabelle mit `household_id`, Index und vier Policies, in einer Migration.
2. **Tests.** pgTAP für alle fünf Rollen: `owner`, `adult`, `child`, `guest`, `service`.
3. **Domänenlogik.** Reine Funktionen ohne Oberfläche, mit eigenen Tests.
4. **Oberfläche.** Aus Bauteilen des Systems, gegen den Seed von Familie Sonntag.

Gegen leere Screens wird nie gebaut. Der Seed-Haushalt hat eine echte Woche: Mo 1. bis So 7. September, heute ist Mittwoch.

## Der Stack je Oberfläche

### Web-App und HQ

`apps/web` ist Next.js 16 mit App Router. Die Tokens kommen als CSS-Variablen direkt aus dem Paket, im Layout per Import von `tokens/colors.css` und den übrigen Token-Dateien. Tailwind dient nur Layout-Hilfen wie Raster, Abstände und Umbrüche. Farbe, Schrift und Radius laufen über Rollen.

Die Bauteile in `apps/web/src/bausteine` sind die Produktionsfassungen der Bauteile im System. `Rahmen.tsx` ist `LeisteMac` und `Kopfzeile`, `Zeichen.tsx` ist `Icon`. Weicht eine Fassung ab, ist das ein Fehler, kein Stil.

### Website

`apps/site` baut hey-albert.app aus Payload. Jeder Block im Inhaltsmodell hat genau ein Bauteil in `packages/design-system/bloecke`. `RenderBloecke` wählt nach `blockType`. `pnpm test` im Paket prüft, dass kein Block ohne Bauteil bleibt.

### Apple

`apps/apple` wird SwiftUI Multiplatform mit Swift 6 und strikter Nebenläufigkeit. Die Rollen liegen in `swift/Tokens.swift`: `AlbertFarbe`, `AlbertRaum`, `AlbertSchrift`, `AlbertForm`, `AlbertBewegung`. Jede Farbe trägt hell und dunkel und entscheidet zur Laufzeit.

> **Offen**
>
> Die Swift-App zieht erst mit ihrer Phase in `apps/apple` ein. Bis dahin ist `Tokens.swift` erzeugt, aber in keinem Xcode-Projekt eingebunden.

## Tokens als Code

Die Quelle ist JSON im Format der W3C Design Tokens Community Group, ein Satz je Datei: `farben-hell`, `farben-dunkel`, `schrift`, `raum`, `form`, `bewegung`. Style Dictionary 5 erzeugt daraus drei Ausgaben, ohne Werte umzurechnen.

```bash
cd packages/design-system
pnpm tokens        # erzeugen und gegen die eingefrorene Referenz prüfen
pnpm tokens:diff   # nur prüfen
```

`tokens-diff` vergleicht jedes Paar aus Name und Wert mit `tokens/referenz/`. Weicht ein Wert ab, bricht der Lauf ab. Neue Rollen stehen mit Begründung in `tokens/referenz/ergaenzungen.txt`.

Die erzeugten Dateien tragen in der ersten Zeile „Erzeugt aus tokens/quelle“. Niemand ändert sie von Hand. Der GitHub-Lauf `design.yml` erzeugt sie neu und prüft, dass nichts abweicht.

## Regeln im Code

- Kein Hex-Wert in einem Bauteil. Farbe läuft über Rollen (--grund, --karte, --tinte, --akzent).
- Keine Schatten, nirgends. Tiefe kommt aus Schleier, stärkerer Kontur und Kippung.
- Bedeutungen werden nicht getauscht: Rosa = Akzent und Jetzt, Gelb = Familie und Ritual, Blau = Fahrt, Offen-Rosa nur als gestrichelter Rand für „ohne Zuständigen“. Rosa heißt nie „Fehler“.
- Farbe ist nie der einzige Träger. Initiale oder Name steht dabei.
- Inaktiv ist eine gerechnete Rolle, keine Deckkraft.
- Text und Bedienelemente mindestens 4,5:1, Flächen und Konturen mindestens 3:1.
- Zielflächen: Zeiger 28, Finger 44, Kind 56 px — nie darunter.
- Fokus immer über :focus-visible: 2 px Ring in Offen-Rosa mit 2 px Abstand.
- 120 ms Zustände, 200 ms Einblendungen, 300 ms Atem. Kein Federn, kein Kreisel. Bei reduzierter Bewegung steht alles still.
- Skelett statt Kreisel. Das Skelett hat die Form des Inhalts.
- Jede Änderung ist optimistisch und hat „Rückgängig“.
- Schriften werden lokal ausgeliefert, nie von Google-Servern.
- Kein Emoji, keine Unicode-Zeichen als Icon-Ersatz.
- Fehlt ein Zeichen, wird es aus dem Satz geholt, nicht erfunden.

### Farbe nur über Rollen

> **So**
>
> ```css
> .karte {
>   background: var(--flaeche-karte);
>   color: var(--text-koerper);
>   border: var(--strich-haar) solid var(--linie-haar);
>   border-radius: var(--radius-karte);
>   padding: var(--karte-innen);
> }
> ```

> **Nicht so**
>
> ```css
> .karte {
>   background: #EEE8DD;
>   color: rgba(0, 0, 0, 0.87);
>   border-radius: 10px;
>   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
> }
> ```
> Ein Hex-Wert, eine freie Deckkraft, ein freier Radius, ein Schatten. Vier Verstöße in vier Zeilen.

### React mit Tailwind

Das Tailwind-Theme kennt nur unsere Rollen. Tailwinds eigene Palette, Schatten und Radien sind abgeschaltet. `bg-red-500` oder `shadow-md` gibt es deshalb gar nicht.

> **So**
>
> ```tsx
> <button className="bg-knopf-primaer-flaeche text-knopf-primaer-text rounded-pille h-ziel-finger px-raum-2
>   hover:bg-knopf-primaer-flaeche-zeiger focus-visible:outline-2 focus-visible:outline-rand-fokus">
>   Ich fahre
> </button>
> ```

> **Nicht so**
>
> ```tsx
> <button className="bg-black text-white rounded-lg shadow-md h-8 opacity-50 focus:ring" disabled>
>   OK!
> </button>
> ```
> Fremde Farbe, freier Radius, Schatten, Zielfläche unter 44, Deckkraft für inaktiv, `:focus` statt `:focus-visible`, „OK“ als Knopftext und ein Ausrufezeichen.

Inaktiv ist eine gerechnete Rolle: `disabled:bg-knopf-inaktiv-flaeche disabled:text-knopf-inaktiv-text`. Deckkraft multipliziert sich mit dem Untergrund. Dann stimmt keine gerechnete Kontrastzahl mehr.

### SwiftUI

> **So**
>
> ```swift
> Text("Schwimmen mit Lena")
>   .foregroundStyle(AlbertFarbe.textKoerper)
>   .padding(AlbertRaum.karteInnen)
>   .background(AlbertFarbe.flaecheKarte,
>               in: RoundedRectangle(cornerRadius: AlbertForm.radiusKarte))
>   .overlay(RoundedRectangle(cornerRadius: AlbertForm.radiusKarte)
>     .stroke(AlbertFarbe.linieHaar, lineWidth: AlbertForm.strichHaar))
> ```

> **Nicht so**
>
> ```swift
> Text("Schwimmen mit Lena")
>   .foregroundColor(colorScheme == .dark ? .white : Color(hex: "#1A1A1A"))
>   .background(Color.yellow.opacity(0.4))
>   .cornerRadius(10)
>   .shadow(radius: 4)
> ```
> Dunkler Modus als Verzweigung statt als Rolle, Hex-Wert, Systemgelb mit Deckkraft, freier Radius, Schatten.

### Schriften lokal

Die vier Schriften liegen als woff2 für das Web und als TTF für die App im Paket. Nichts wird von Google-Servern geladen. Sonst ginge die IP-Adresse jedes Besuchers in die USA. Das LG München I hat das 2022 als Rechtsverletzung gewertet.

### Fokus

Fokus zeigt sich immer über `:focus-visible`, nie über `:focus`. Der Ring ist 2 px breit, in `--rand-fokus`, mit 2 px Abstand. So sieht ihn, wer mit der Tastatur arbeitet, und niemand sonst.

## Prüfer

| Prüfer | Was er findet | Befehl |
|---|---|---|
| Brand-QA | Hex-Wert außerhalb der Quelle, Schatten, freie Radien, verbotene Wörter, Ausrufezeichen, Bild ohne `alt` | `pnpm qa` im Paket |
| Token-Diff | Werte, die von der eingefrorenen Referenz abweichen | `pnpm tokens:diff` |
| Blöcke | Block im Inhaltsmodell ohne Bauteil | `pnpm test` |
| Storybook | Tokens, Primitive, jeder Block in de, en, tr, hell und dunkel, Handy und Tisch | `pnpm storybook` (Port 3112) |
| Bildvergleich | jede Story hell und dunkel, Handy und Tisch, Schwelle 0,1 % | `pnpm vr`, Referenzen neu: `pnpm vr:update` |
| Kontrast | Pflichtpaarungen nach WCAG in beiden Modi | `python3 tooling/check-kontrast.py` |
| Sprache | Satzlänge, Register, Wortlisten | `python3 tooling/check-sprache.py` |
| Agent `design-pruefer` | Token-Verstöße und Definition of Done einer Ansicht, nur lesend | in Claude Code vor jedem „fertig“ |
| Kapitel dieser Seite | Direktiven, Argumente, Sprache | `node apps/brand/skripte/pruefen.ts` |

> **Offen**
>
> `tooling/check-kontrast.py` und `tooling/check-sprache.py` brechen zurzeit ab. Beide suchen Module unter Pfaden des Vorgängers. Bis sie umgestellt sind, rechnet diese Seite die Kontraste selbst (Kapitel Farbe) und `pnpm qa` prüft die Wortliste.

Der GitHub-Lauf `design.yml` startet bei jeder Änderung unter `packages/design-system`. Er erzeugt die Tokens, prüft die Referenz, testet die Blöcke, läuft Brand-QA und Typen, baut Storybook und vergleicht die Bilder.

## Fertig ist eine Ansicht erst mit

- [ ] leerer Zustand
- [ ] Skelett statt Kreisel
- [ ] Tastatur und :focus-visible
- [ ] Handy und Tisch
- [ ] hell und dunkel als Token-Umschaltung
- [ ] optimistisches Update mit „Rückgängig“
- [ ] richtiges Register
- [ ] kein Hex-Wert
- [ ] Zielflächen 28/44/56

Auf dem Mac gehört Tastaturbedienung dazu, auch für Ziehen und Ablegen. In der App gehören Dynamic Type und VoiceOver dazu. Konflikte sind sichtbar, aber sie blockieren nicht.

## Ein neues Bauteil

Kein Bauteil entsteht nur in einer App. Was in `apps/web`, in der Swift-App oder auf der Website gebaut wird, kommt zuerst oder spätestens im selben Commit ins System.

1. `components/<gruppe>/<Name>.jsx` mit allen Zuständen: Standard, gedrückt, fokussiert, inaktiv, leer, hell und dunkel, Kind-Größe, wo Kinder bedienen.
2. `<Name>.d.ts` mit deutschen Props.
3. `<Name>.prompt.md`: wofür, welches Register, ein Beispiel mit Familie Sonntag, die Grenzen.
4. Eine `@dsCard`-Karte im Ordner und ein Eintrag in `_ds_manifest.json`.
5. Die Produktionsfassung in der App, mit gleichem Namen und gleichen Props.
6. `pnpm qa`, Storybook-Story, Bildvergleich.

Ein bestehendes Bauteil ändern heißt: erst `.prompt.md` lesen, dann das JSX, dann die Fassung in der App.

## Einen Token ändern

1. Den Wert in `tokens/quelle/*.json` ändern, bei Farben hell **und** dunkel.
2. `pnpm tokens` laufen lassen. Der Diff bricht ab und zeigt die Abweichung.
3. Die Änderung mit Datum, Kontext und Grund in `docs/entscheidungen.md` eintragen.
4. Die Referenz in `tokens/referenz/` neu einfrieren.
5. Kontrast und Bildvergleich prüfen, dann die erzeugten Dateien mit einchecken.

Eine neue Rolle braucht keine Entscheidung, aber eine Zeile in `ergaenzungen.txt` mit Grund. Eine bestehende Rolle ändert sich nur mit Entscheidung.

## Git und Veröffentlichen

Commits schreibt das Konto `guido77-EnP`, auf Deutsch, der Betreff als Satz. Vor jedem `git push` wird gefragt. Danach prüft `tooling/deploy-pruefen.sh` die Bauten von albert-web, albert-site und den GitHub-Lauf. Ein roter Bau bleibt nie stehen.

Screens, Stories und Beispiele zeigen immer Familie Sonntag. Echte Familiendaten gehören nie ins Repository. Der Seed ist zugleich der Zugang für Apples App-Review.
