# Brainz Komponenten-Architektur — START HIER (Agenten & Plugin-Devs)

> Wie **Compose-Engine** (Eingabe) und **render_ui** (Anzeige) zusammenspielen — und
> wie du in wenigen Minuten ein neues Element anlegst, das **überall** verfügbar ist.
> Maschinenlesbarer Vertrag: [`manifest.json`](./manifest.json). Lebende Referenz:
> [Styleguide](./index.html) (live: https://xitami.github.io/assistant-kit/ ·
> https://app.brainz.plus/app/design-system/) · **[Mini-App-Beweis](./app-shell-demo.html)** —
> eine echte App auf `createAppShell` (~60 Zeilen: Looks live, forms-Zustandsautomat,
> Primitive) · Regenerieren: `node gen-app-shell-demo.mjs`.

---

## TL;DR (das mentale Modell in 6 Zeilen)

1. Es gibt **zwei getrennte Kataloge** — nicht vermischen: **A = Eingabe** (Compose-Felder), **B = Anzeige** (render_ui).
2. **A** lebt in `apps/mobile/src/compose-sheet.ts`, hat **ein** Surface (das Eingabe-Sheet), nutzt `--bzc-*`-Tokens, schreibt in `ComposeState`.
3. **B** lebt in den **Packs** (`packages/*`), hat **drei** Surfaces (live DOM · Lexical-Editor · PDF), nutzt `--bz-*`/`--as-*`-Tokens, ist reine **Daten** (`{ type, props }`).
4. Der **Zod-Katalog in `@brainz/doc-model`** (brainz-doc-core; `packages/`@brainz/doc-model`` ist seit der MIGRATION eine reine Shim-Facade) **ist die Single Source für B** — Server, Widget, Editor, PDF und das Design-System konsumieren ihn alle.
5. Das **Design-System wird generiert**, nicht handgepflegt (aus compose-sheet.ts + aus den Packs). Du pflegst nur Code + eine Kurzbeschreibung.
6. „Überall verfügbar" heißt: **B**-Element → alle drei Display-Surfaces; **A**-Feld → überall, wo das Compose-Sheet öffnet.

> **Mechanik im Detail** (das *Warum* hinter render_ui — render_ui ≠ Web Components, der
> Action-Rückkanal, Moleküle end-to-end an echtem Code): [`specs/render-ui-deep-dive.md`](../specs/render-ui-deep-dive.md).
> Dieses Dokument zeigt das mentale Modell + die Schritt-für-Schritt-Anleitung; die Deep-Dive-Spec
> erklärt die Mechanik dahinter.
> **Die Ebene darüber** (render_ui als vollständiges Interface-System: Pack-Kompat-Vertrag,
> Theme-Vertrag, App-Shell, Interaktion — Gap-Analyse + Fahrplan):
> [`specs/interface-system.md`](../specs/interface-system.md) · Skill `/interface-design`.

---

## 1 · Die zwei Kataloge

| | **Katalog A — Compose (Eingabe)** | **Katalog B — render_ui (Anzeige)** |
|---|---|---|
| Quelle | `apps/mobile/src/compose-sheet.ts` (`ComposeField`-Union) | `@brainz/doc-model` (+ `widget`, `cards`, `pdf-renderer`) |
| Daten | `{ kind: "text", key, … }` → schreibt `ComposeState` | `{ type: "Callout", props: {…} }` (deklarativ, serialisierbar) |
| Surfaces | **1** — das Compose-Sheet (Formular) | **3** — live DOM · Lexical-Editor · PDF-Export |
| Tokens | `--bzc-*` (Compose-Namespace) | `--bz-*` (App) / `--as-*` (Widget-Shadow) |
| Verteilung | Discriminated Union + `renderField`-Switch | Zod-`ComponentDef[]` + Renderer-Map je Surface |
| Styleguide | `#ref-a` (28 Felder) | `#ref-b` (53 Komponenten, **live gerendert**) |

**Faustregel:** Soll der User etwas **eingeben** → Katalog A. Soll ein KI-/Daten-**Ergebnis angezeigt** werden (im Stream, im Editor, im Chat, als PDF) → Katalog B.

### 1b · Die andere Hälfte: App-Bedienelemente (`@assistant-kit/app-controls`)

render_ui (Katalog B) beschreibt, was eine App **zeigt** (Inhaltsblöcke). Womit man sie **bedient** – Knöpfe, Tabs vs.
Segment, Chips, Status-Pillen, Felder, Toast, Banner, Tab-Leiste, Leerzustand, Skeleton, Fokusrahmen – liefert
`packages/app-controls` auf denselben `--bz-*`-Tokens, mit einer Akzentfarbe je Produkt (crews, trips, maps).
**Vor jeder UI-Arbeit lesen:** [`packages/app-controls/UI-REFERENZ.md`](../../../packages/app-controls/UI-REFERENZ.md);
maschinenlesbar [`ui-reference.json`](../../../packages/app-controls/reference/ui-reference.json) (Gegenstück zu
`manifest.json`), für Menschen die erzeugte Seite `packages/app-controls/reference/index.html`.

---

## 2 · Datenfluss

```
 EINGABE (A)                         ANZEIGE (B) — eine Quelle, drei Surfaces
 ──────────                          ────────────────────────────────────────
 compose-sheet.ts                    @brainz/doc-model  ← SINGLE SOURCE (Zod)
   ComposeField-Union (28 kinds)       coreComponents (18) + brainzCardsComponents (35)
   renderField(f, ctx) → DOM                 │  validateNode() …
   --bzc-* Tokens                            ├─→ packages/widget        → live DOM  (RenderingEngine, --as-*)
   setState → ComposeState                   ├─→ descriptor-to-lexical  → Editor-Roundtrip (Lexical-Node)
        │                                     └─→ packages/pdf-renderer  → PDF (pdfmake)
        │                                            │
        └───────────────┬──────────────────────────-┘
                         ↓  (introspektiert, nicht handgepflegt)
              docs/brainz-mobile/design-system/  →  #ref-a · #ref-b · manifest.json
              gen-compose-catalog.mjs (A)  ·  gen-catalog.mjs + gen-examples.mjs (B)
```

Ein **render_ui-Descriptor** ist nur Daten. Der **Orchestrator/Server** validiert ihn gegen den injizierten Katalog (`opts.catalog`); das **Widget** rendert ihn (`engine.render`); der **Editor** serialisiert ihn als Lexical-Node und zurück; der **PDF-Renderer** projiziert ihn. Alle vier sprechen denselben `type`-String.

### Zwei Granularitäten der Komposition (Home/Detail) — wer baut was?

Das Layout-System arbeitet auf **zwei Ebenen** (live + generiert dokumentiert unter `#layout-system`):

1. **Grob — der Layouter (Home-Chefredakteur, free-layout C2b):** komponiert FREI, emittiert aber nur eine **kleine Palette**: vier **Prosa-Blöcke** (`SectionHeader`, `BodyText`, `Callout`, `Divider`) + **Daten-`{ref}`** aus dem Inventar (ein ganzes Element/eine Karte). Er ordnet, gruppiert, textet — wählt **keine** Atome/Moleküle direkt. Erzwungen vom Server (`home-digest-shape.sanitizeLayout`, `LAYOUT_PROSE_BLOCKS` in `@brainz/doc-model`).
2. **Fein — die Karten-/Detail-Komposition (datengetrieben):** nimmt ein `{ref}` (ein Inhalt mit `structured_data`) und baut **automatisch Moleküle**, wenn passende Feld-Keys erkannt werden — `images`→`Gallery`, `ingredients`→`IngredientList`, `steps`→`StepList`, `rating`→`Rating`, `tips`→`Tip`. Quelle: `MOLECULES` in `@brainz/doc-model` (`genericCardProps` + `composeDetail`/`promoteMolecules`). Neue Moleküle = ein `MOLECULES`-Eintrag, der überall greift.

Kurz: der Layouter platziert **fertige Komponenten + Prosa**; die Komponente baut ihr **Inneres (Moleküle) selbst** aus den Daten. Beide Listen sind Single-Source in `@brainz/doc-model` und speisen `#layout-system` + `manifest.layoutSystem` (auto-current).

### Reaktivität: Entity-Signale (2026-07) — „Signals außen, Renderer-Purity innen"

Wie kommt eine **Änderung** zurück auf den Bildschirm? Nicht durch reaktive Renderer —
Renderer bleiben **pur** (Descriptor rein, DOM raus; die Multi-Surface-Invariante hängt daran).
Stattdessen abonniert der **Host** einer Fläche seine Entität und re-komponiert über den
existierenden Render-Pfad:

```ts
import { touchEntity, subscribeEntity, ANY_ENTITY } from "@assistant-kit/widget";
touchEntity(uuid, source)        // Feeder: NACH Persistenz-Erfolg, nie optimistisch
subscribeEntity(uuid, cb)→unsub  // Konsument: debounced re-komponieren (uuid | `ke:<id>` | ANY_ENTITY)
```

Coalescing 150 ms, Multi-Fenster (BroadcastChannel), Fehler-Isolation. Adoptiert: Detail-Sheet,
Entity-Profil, Kommentar-Thread, Home-Puls (Wildcard). **Beim Bauen einer neuen Fläche mit
Entity-Daten: abonnieren statt Polling/„nach Save neu laden".** Plugins (`detail.compose`)
erben den Refresh gratis — ihre Compose-Funktion läuft einfach erneut.
Spec + Guards + Adoptions-Tabelle: [`specs/entity-signals.md`](../specs/entity-signals.md).

---

## 3 · Wo was liegt

| Datei | Rolle |
|---|---|
| `catalog-core.ts` in `@brainz/doc-model` | **`coreComponents`** — Zod-`ComponentDef`s (Single Source für B) |
| `descriptor.ts` in `@brainz/doc-model` | `ComponentDef`-Interface, `buildCatalog`, `validateNode`, `refable` |
| `packages/`@brainz/doc-model`/src/descriptor-to-lexical.ts` | render_ui ↔ Lexical (Editor-Surface); `RENDER_UI_TYPES`-Default-Zweig |
| `packages/widget/src/renderers-core.ts` | **`coreRenderers`** — live-DOM-Renderer `(node, ctx) => HTMLElement` |
| `packages/widget/src/styles.ts` | `widgetStyles` — CSS der Core-Komponenten (`.as-*`) |
| `packages/widget/src/engine.ts` | `RenderingEngine` (`registerPack`, `render`) |
| `packages/component-packs/cards/` | **`brainzCardsComponents` + `brainzCardsRenderers` + `brainzCardsStyles`** (Entity-Cards) |
| `packages/pdf-renderer/` | PDF-Projektion (`printComponents`, `mapNode`) |
| `apps/mobile/src/compose-sheet.ts` | **Katalog A** — `ComposeField`-Union + `renderField`-Switch |
| `packages/`@brainz/doc-model`/src/field-registry.ts` | **Field Registry (A↔B)** — serialisierbare FieldDefs: ein Feld = Compose-Eingabe + Molekül-Anzeige + Wert-Validierung (`#field-registry`) |
| `docs/brainz-mobile/design-system/` | Styleguide + Generatoren + `manifest.json` (Agent-Vertrag) |

> **Interaktions-Primitive** (Toast, Bottom-Sheet, Menü-Sheet, Back-Stack …) sind ein **dritter** Bereich —
> seit 31.07. als **Package `@assistant-kit/app-primitives`** (Katalog C; Theme-Quelle via
> `configurePrimitives({ themeSource })`, Default `.shell`) —
> **weder Katalog A noch B** (kein Compose-Feld, keine render_ui-Karte), sondern App-Bausteine in
> `apps/mobile/src/`. Es gilt dieselbe Regel wie für die Kataloge: **eine Quelle, nicht nachbauen.**
> Diese Primitive sind als **Katalog C** im Styleguide dokumentiert (`#ref-c` — API-Referenz, kein
> Live-Render; Quelle: `primitives-overrides.mjs` → `gen-primitives-catalog.mjs`). Bewusst **nicht** in
> der Figma/design-sync-Bridge (imperative API ist nicht statisch renderbar). Die übrigen Coverage-Lücken
> (Plugin-Laufzeit-Packs etc.) stehen in [`specs/render-ui-deep-dive.md`](../specs/render-ui-deep-dive.md) §8.
>
> - **Toast** → `@assistant-kit/app-primitives` (`packages/app-primitives/src/toast.ts`): `showToast(msg, dur?, onClick?)` = Info (mit `onClick` ganz
>   tappbar) · `showUndoToast(msg, {onUndo, actionLabel?, …})` = mit Aktions-Button (Default „Rückgängig",
>   z. B. „Anzeigen"). Immer **einer** sichtbar (ein neuer committet den offenen), **dock-aware**
>   (`--bz-dock-h`), body-Overlay über Sheets, weicher Fade. **Keine eigenen Toasts** bauen — frühere
>   Eigenbauten (`.bzca-toast` in capabilities, `.bzs-flash`-Look) sind hier konsolidiert.
> - **Sheets/Modale** → `openBottomSheet` + `back-stack` (Android-Back-Geste, Drag-to-Dismiss,
>   Omnibar-Inset, Card→Sheet-Morph) — NICHT rohes `<dialog>` (siehe Build-Ladder im brainz-feature-Skill).

---

## 4 · Neues **render_ui**-Element anlegen (Katalog B)

> Generisches Primitive → **`@brainz/doc-model`-Core**. Brainz-Entity-Karte → **Cards-Pack** (gleiche Schritte, andere Dateien: `brainzCardsComponents`/`brainzCardsRenderers`/`brainzCardsStyles`).

**① Vertrag** — `packages/`@brainz/doc-model`/src/catalog-core.ts`, in `coreComponents`:
```ts
{
  type: "Foo",
  description: "Was Foo anzeigt — wird Doku + Agent-Vertrag.", // PFLICHT (Generator warnt sonst)
  props: z.object({ label: z.string(), tone: z.enum(["info","ai"]).optional() }),
  // children?: true        // wenn Container
  // events?: ["press"]     // wenn klickbar (Action-gebunden)
  lexicalNodeType: "render-ui", // ← gibt den Editor-Roundtrip GRATIS (s. Schritt ③)
  // Kompositions-Metadaten (optional, empfohlen): speisen die generierten
  // LLM-Tool-Descriptions + manifest.json — WOZU (intent), Platz-Klasse (footprint),
  // Nachbarschaft (pairsWith/conflictsWith/mergesWith), Paletten (wo emittierbar).
  meta: { intent: "warn", footprint: "block", columnSafe: true },
  print: "native", // PDF-Coverage: "native" (Schritt ④) | "fallback" | "omit"
}
```

**② Live-Renderer** — `packages/widget/src/renderers-core.ts`, in `coreRenderers`:
```ts
Foo: (node) => {
  const el = document.createElement("div");
  el.className = `as-foo as-foo-${node.props.tone ?? "info"}`;
  el.textContent = String(node.props.label ?? ""); // .textContent = XSS-sicher
  return el;
},
```
+ CSS in `packages/widget/src/styles.ts` (`.as-foo { … }`) — **nur `--as-*`-Tokens**.

**③ Editor (Lexical):** mit `lexicalNodeType: "render-ui"` **automatisch** — der `RENDER_UI_TYPES`-Default-Zweig in `descriptor-to-lexical.ts` macht den Roundtrip. (Nur native Strukturtypen wie Table/List brauchen einen eigenen `case` + Markdown-Projektion.)

**④ PDF (optional):** `packages/pdf-renderer/src/catalog.ts` (`printDef`) + `index.ts` (`mapNode`-`case`). Weglassen = Komponente hat eben keine PDF-Fläche.

**⑤ Beispiel (optional, Politur):** kuratiertes Beispiel in `docs/brainz-mobile/design-system/catalog-overrides.mjs` (sonst auto-gesampelt aus dem Zod-Schema).

**⑥ Ableiten:** `node docs/brainz-mobile/design-system/gen-catalog.mjs && gen-examples.mjs` (oder einfach pushen → CI). → `#ref-b`, `manifest.json`, Live-Beispiel aktualisieren sich.

**→ Warum „überall":** Schritt ① schreibt in den **einen** Katalog, den Server (Validierung), Widget (live), Editor (Lexical) und das Design-System alle konsumieren. Schritt ② liefert die Pixel. Das war's — kein N-faches Registrieren.

**Cards-Pack zusätzlich (Pflicht):** Typ in `packages/component-packs/cards/src/families.ts`
`FAMILY_BY_TYPE` einer der 17 Token-Familien zuordnen (evt·tsk·med·cap·fin·ppl·plc·vlt·ai·sug·pro·cont·mta·ing·wx·trn·gen) — sonst bekommt er keine Fabrik-Garantien (§4b). Dispatcht der Renderer selbst Actions → `emits` an der Def deklarieren (Konformanz-Test wacht darüber). Beispiel in `catalog-overrides.mjs` ist bei Renderer-Typen Pflicht (axe-Gate rendert es).

---

## 4b · Fabrik-Garantien (Cards-Pack) — was JEDE Karte gratis bekommt

Ein zentraler Wrapper (`withFamilyTag` in `renderers.ts`) veredelt jeden Renderer-Root — **ein Hebel statt 92 Einzelimplementierungen** (Konzept: [`specs/cards-next.md`](../specs/cards-next.md)):

| Garantie | Mechanik |
|---|---|
| **Token-Familie** | `data-fam="<fam>"` am Root; darauf definieren `[data-fam]`-Blöcke die Kette `--<fam>-x-override → --tc → --crd-* → --bz-* → Hex`. BEM liest **nur** `--<fam>-*`/`--crd-*`. Familie `ai` = fest KI-Violett (kein Override, kein `--tc`). Themebar via `ThemeTokenContribution` (`--crd-` + 16 Familien-Prefixe; `--ai-` bewusst nicht). |
| **Fokus-Grundgesetz** | `press`-gebundene Karten sind EIN fokussierbares Element: `role="button"` (oder `article` bei inneren Interaktiva — kein nested-interactive), `tabindex="0"`, Enter/Space = `click()`-Parität, Ring via `--crd-focus`. |
| **Gesten** | In den 10 Karten-Familien (`GESTURE_FAMILIES`) liefert die Fabrik den Swipe nach, wenn der Host `pin`/`dismiss` bindet und der Renderer nicht selbst wickelt; Delete/Backspace = Verwerfen (Tastatur-Äquivalent). |
| **View-Transition** | `data-vt="card-<uuid>"` als **Marker** — den Live-`view-transition-name` setzt der Host erst im Morph-Moment (Duplikat-Namen brechen sonst die ganze Transition). |
| **Motion** | `motion.ts`-Primitive (`crd-rise/enter/press/breathe/ring/drop/settle`); `--crd-spring-linear` wird aus der echten Gesten-Physik (`gesture-physics.spring()`) berechnet → CSS federt identisch zur JS-Geste. Alles im reduced-motion-Gate. |

**Verben-Idiom (Action-Vertrag):** Die Action-Union bleibt GESCHLOSSEN. Familien-Verben (tsk `complete·snooze`, ai `feedback·boost`, sug `accept·reject`, med `setStatus`, vlt `reveal·copy`, trn `refresh`) sind **Event-Namen**: Renderer zeigt die Affordance **nur wenn `node.on.<verb>` gebunden ist** (ungebunden = unsichtbar, kein totes Ende) und dispatcht die gebundene Action (Host-Idiom: `sendMessage`-Textkommando). Vertragsregel `events ⊇ dispatch` · `selbst-dispatchte Arten ≡ emits`.

**Drei Gates sichern das ab** (alle brechen den Build/Test):
1. **Token-Lint-GATE** (`scripts/gen-packs.mjs`, exit 1; Übersteuern `PACKS_LINT=warn`): keine nackten Farb-Literale außer Ratchet-Budget (nur Abbau), kein Fremd-Namespace-Read; `bzNative`-Packs (brainz-cards) dürfen `--bz-*`.
2. **axe-Gate** (`packages/brainz-render-html/test/a11y-axe.test.ts`): alle Store-Beispiele durch axe-core; Baseline = `design-system/a11y-audit.json` (Refresh: `A11Y_UPDATE=1 … test:a11y`), neue critical/serious = rot.
3. **Konformanz-Test** (`cards/test/action-contract.test.ts`): jedes deklarierte Event hat einen Dispatch-Pfad, jede selbst-dispatchte Action-Art steht in `emits`.

---

## 5 · Neues **Compose-Feld** anlegen (Katalog A)

> **Erst prüfen: reicht ein `FieldDef` (Field Registry)?** Für ein einfaches
> `structured_data`-Feld (Text/Zahl/Bool/Chips/Select/Stepper/Datum) ist der bevorzugte Weg
> **EINE serialisierbare Deklaration** in `packages/`@brainz/doc-model`/src/field-registry.ts`
> (bzw. `Capability.fields` für Plugins): sie speist Compose-Eingabe
> (`compose-fields-bridge` → bestehende Kinds), das Eigenschaften-Sheet
> (`structured-data-editor`), die Molekül-Anzeige (`display.molecule`) **und** die
> Server-Validierung — ohne neuen Code. Trait-scoped via `appliesTo`. Referenz:
> [`#field-registry`](./index.html#field-registry) · `manifest.fields`.
> Ein neues Compose-**Kind** (eigene Interaktion, Closures, Async) braucht weiterhin die
> Schritte unten.

**① Typ** — `apps/mobile/src/compose-sheet.ts`, Member zur `ComposeField`-Union (passende `── Gruppe ──`):
```ts
| { kind: "foo"; key: string; label: string; placeholder?: string }
```

**② Renderer** — selbe Datei: `case "foo": return renderFoo(f, ctx);` im `renderField`-Switch + `renderFoo(f, ctx)` implementieren. `ctx` = `{ state, setState, subscribe }`; schreibt `ComposeState[f.key]`; **nur `--bzc-*`-Tokens**, Icons **nur** via `bzIcon`.

**③ Beschreibung** — `docs/brainz-mobile/design-system/compose-overrides.mjs`: `foo: "Kurz, was das Feld tut"` (sonst warnt der Generator).

**④ Ableiten:** `node docs/brainz-mobile/design-system/gen-compose-catalog.mjs` → `#ref-a` + `manifest.catalogs.composeInput`.

**→ „Überall" (A):** Compose ist **ein** Surface. Das Feld erscheint überall, wo das Compose-Sheet für den jeweiligen Content-Type öffnet. (Anders als B: kein Editor/PDF.)

---

## 6 · Das Design-System ist **generiert**

Nichts an den Katalog-Listen ist handgepflegt — die Generatoren **leiten aus der Quelle ab**, deshalb kann die Doku nicht von den Packs abweichen:

| Generator | leitet ab aus | erzeugt |
|---|---|---|
| `gen-compose-catalog.mjs` | `compose-sheet.ts` (`ComposeField`-Union) | `#ref-a` + `manifest.catalogs.composeInput` |
| `gen-primitives-catalog.mjs` | `primitives-overrides.mjs` (kuratiert, Katalog C) | `#ref-c` + `primitives-catalog.generated.json` + `manifest.catalogs.interactionPrimitives` |
| `gen-catalog.mjs` | `coreComponents` + `brainzCardsComponents` | `#ref-b`-Liste + `catalog.generated.json` + `manifest.catalogs.renderUiDisplay` |
| `gen-examples.mjs` | `catalog.generated.json` + echte Renderer | `examples.js` (53 **live** gerenderte Beispiele) |
| `gen-icons.mjs` | `cards/src/icons.ts` | `icons.js` (70 Icons) |

Handgepflegt bleibt nur: **Code** (die Komponenten/Felder selbst) + **Kurzbeschreibungen** (`*-overrides.mjs`). Der Generator **warnt**, wenn ein neuer Typ keine Beschreibung hat → fängt undokumentierte Elemente.

**Deploy:** CI (`.github/workflows/design-system.yml`) generiert frisch bei jedem relevanten Push → GitHub Pages. Zusätzlich liegt eine Kopie unter `app.brainz.plus/app/design-system/` (PB-appshell). → keine generierten Artefakte sind „Wahrheit", Drift ist unmöglich.

---

## 7 · Verbindliche Prinzipien (Kanon: `specs/brainz-ink.md`)

- **Namespaces nicht mischen:** ein Plugin liest `--bzc-*` (Compose) bzw. seinen eigenen Namespace, **nie** `--bz-*` direkt → themt automatisch + läuft standalone (Hex-Fallbacks). Karten lesen ihre **Familien-Token** `--<fam>-*`/`--crd-*` (§4b) — `--bz-*` ist dort nur die Terminal-Schicht der Kette.
- **Zwei-Akzent:** Violett (`--bzc-ai` / `--bz-accent-ai`) **nur** für KI-Momente. Alles Gefüllte/Primäre = `--bzc-accent`.
- **Motion** nur `transform`/`opacity` (Compositor), mit reduced-motion-Guard.
- **Performance (butterweich):** scrollbare Listen **virtualisieren** ODER `content-visibility:auto`+`contain-intrinsic-size`; **Layout-Reads nie in Mount-/Update-Schleifen** (Read/Write-Phasen trennen); Position via `transform` (nicht `top`); `contain:layout` je Row; große `box-shadow`-Blurs meiden; Bilder `decoding=async`+`loading=lazy`. Playbook + Checkliste: [`specs/performance.md`](../specs/performance.md).
- **A11y:** ≥44px Touch-Targets, sichtbares `:focus-visible`.
- **Icons** nur via `bzIcon()` — nie inline-SVG.
- **`description` ist Pflicht** auf jedem `ComponentDef`.
- **„Einmal deklarieren, ableiten":** Verhalten aus dem Vertrag ziehen (wie `lexicalNodeType` → `RENDER_UI_TYPES`), nicht N-fach von Hand registrieren.

---

## 7b · Detail-Card erweitern — die VIER Andockpunkte (nicht neu erfinden!)

Die Detail-Card ist über bestehende Slots erweiterbar — **alle vier existieren**, bitte keinen fünften bauen:

| Slot | Wo | Wofür | Beispiel |
|---|---|---|---|
| `detail.compose` (Capability) | `capability.ts` → `detail: { contentTypes, compose }` | Der **Haupt-Body** als Katalog-B-Nodes — ERSETZT die Default-Komposition komplett (Body-Fallback selbst durchreichen!) | `composeStory`, `composeTrip`, `renderSchemaView` (location) |
| `detailThemes` (Capability) | `capability.ts` → `detailThemes: { <type>: DetailTheme }` | **CSS-Scope + enhance-Hook** (bekommt `ctx {title, data}`) — Ambient, Zutaten-Bilder, Karten | `bzdt-story`, `bzdt-recipe`, `bzdt-place`, `bzdt-trip` |
| `registerDetailSection` (global) | `detail-sections.ts` (specs/detail-sheet-v2.md §1) | **Zusatz-Sektionen** unter dem Body: rank-geordnet, klappbar, Badge, `contentTypes`-Filter (fehlt → alle Typen). Plugin-Gate via `isPluginEnabled` im `render` | Verlauf (Host), „Verbindungen“-Vorschau (nexus-path) |
| `actions` (Capability) | `surface: "bar"` (Top-2 nach rank als Pills, Rest im „…“) · `surface: "section"` (AKTIONEN-Grid) | **Verhalten** | „Routine starten“, „Verbindungen“ |

Stolperfalle Prosa: `data` mit **echtem Lexical-root** = Dokument (Editor-Read übernimmt, `skipBody` filtert Markdown-Nodes). Plain-Object-`data` (Feld-Bags wie trip/routine/story) ist KEIN Dokument — die Composition rendert voll, kein Editor-Read. **EditorJS (`data.blocks`) ist Legacy** (alte App): nur lesen/migrieren, nie neu schreiben.

---

## 7c · Sheet- & Overlay-Klassen (fünf — nicht neu erfinden!)

Jedes modale UI gehört in **genau eine** der fünf Klassen. **Kein eigenes `position:fixed`-Overlay** daneben, **kein `<dialog>`/`showModal`**.

| Klasse | Einstieg | Wofür | z / Back |
|---|---|---|---|
| **App-Sheet** | `openAppSheet()` → Adapter auf `openBottomSheet()` (`@assistant-kit/app-primitives`) | Der generische Content-Host (Titel-Kopf + ✕, scrollbarer Body, Safe-Area). Die ~35 Feature-Sheets nutzen `openBottomSheet` direkt | `layer:"top"` (9600) für jederzeit/über-Dock; `layer:"content"` (1000) unter der Omnibar |
| **Detail-Sheet** | `openDetailSheet()` (`detail-sheet.ts`) | Entity-Detail (eigenständig) | Back-Stack |
| **Menu-Sheet** | `openMenuSheet()` (`menu-sheet.ts`) | Aktions-/Optionsliste (`MenuItem`: icon/tone/`trailing`/`badge`) | erbt `openBottomSheet` |
| **Fullscreen-Overlay** | eigenes `position:fixed` **+ `pushBackLayer` Pflicht** | Kamera/Scanner/immersiv (barcode-, camera-hub-, doc-scan-overlay, transit-Compose) — **kein** Sheet | z ≥ `IMMERSIVE_OVERLAY_Z` (8500); Back-Geste schließt |
| **Editor-Surface** | `editor-toolbar` Insert-Sheet · `embed-picker` | Editor-eigene Flächen: **Selektion-Halten** (`pointerdown`-preventDefault), z über der Toolbar, Kachel-Grid — bewusst KEINE Sheet-Migration | eigenverantwortlich |

**Invarianten:**
- **Back-Geste schließt** die oberste Schicht — für Sheets automatisch via Shell (`pushBackLayer`); Fullscreen-Overlays + Editor-Surfaces registrieren selbst.
- **z-Order nur über den `layer`-Kontrakt** (`app-primitives/back-stack`: `"content"` 1000 · `"top"` 9600), keine Magic-Numbers im Konsumenten.
- **Folge-Sheets nach dem Schließen nur via `closeThen`** (sonst Back-Stack-Race: der `popstate` des Schließens würde die frische Schicht sofort poppen).
- **Dock-/Tastatur-Contract kommt aus der Shell** — Konsumenten reservieren nie selbst `--bz-dock-h`.
- **Icons nur `bzIcon()`** (§7); die einzigen erlaubten Inline-`<svg>` sind echte Grafiken (weather-glyphs/-sky, slippy-map, confetti, demo-images, transit-Kompass/Pfeil) — abgesichert per `scripts/check-inline-svg.mjs`.

---

## 7d · Namensraum: wem gehört eine CSS-Klassen-Familie?

**Gilt für JEDEN Konsumenten des render_ui-Systems** — Brainz ist nur eine
Anwendung darauf; über brainz-doc-core, den MCP-Server und weitere Hosts teilen
sich mehrere Codebasen dieselben Pakete und denselben `as-`-Namensraum.

Alles CSS landet zur Laufzeit in **einem** `<head>`. Bei gleicher Spezifität
gewinnt die zuletzt eingespritzte Regel — quer durch Positionierung, `display`
und Hintergrund. Daraus folgen zwei Regeln:

| | Regel |
|---|---|
| **R1** | Eine Familie hat **genau einen Besitzer**. Besitz zählt nur die **Basis-Regel** (`.fam { … }`); Erweiterungen (`.fam__x`, `.fam--y`, `.wrap .fam`) sind erlaubt und normal. Besitz-Einheit ist das **Paket** (ein Pack darf sein CSS über `styles/*.ts` verteilen) bzw. im Host die **Datei**. |
| **R2** | Host-Code definiert **keine ungescopte Regel** im render_ui-Namensraum (`as-*`). `.meine-flaeche .as-markdown { … }` ist richtig — der Host themt render_ui **innerhalb seiner eigenen Fläche**. `.as-markdown { … }` ohne Anker wirkt in **jeder** render_ui-Fläche, auch in denen, die ein Pack mitbringt. |

Bewusst geteilte Familien stehen mit Begründung in
`scripts/css-namespace-baseline.json` — jeder Eintrag ist eine Ausnahme, die
jemand vertreten muss.

**Prüfen** (argument-getrieben, jeder Konsument mit SEINEN Verzeichnissen):

```bash
node scripts/check-css-namespace.mjs render-ui:packages host:apps
```

> **Anlass (11.08.):** `.bzsl` gehörte zwei Modulen — dem Skills-Sheet und dem
> `/`-Menü der Omnibar. Das Sheet reichte seine Klasse an `openBottomSheet`
> weiter und erbte `position:absolute; bottom:calc(100% + 6px)` vom Menü: der
> Backdrop lag korrekt über der Seite, das Panel stand über dem Bildschirm.
> Kein Fehler, kein Log, nichts im Netzwerk. Die Lehre ist nicht „bessere Namen
> wählen" — das hatten beide Autoren vor. Sie ist: **solange niemand misst, wer
> welchen Präfix besitzt, ist die nächste Kollision eine Frage der Zeit.**

---

## 8 · Tiefer (Referenz-Specs)

- [`specs/render-ui-deep-dive.md`](../specs/render-ui-deep-dive.md) — **render_ui-Mechanik**: Daten-UI vs. Web Components, eine Quelle → mehrere Surfaces, Action-Rückkanal (`submitForm` end-to-end), Moleküle (ein Renderer, zwei Eingänge)
- [`specs/component-rebaseline.md`](../specs/component-rebaseline.md) — die zwei Kataloge im Detail
- [`specs/component-pack-organization.md`](../specs/component-pack-organization.md) — Pack-Komposition, Subsets, ein-File-pro-Komponente
- [`specs/plugin-architecture.md`](../specs/plugin-architecture.md) — Plugin = Capability, render_ui-Descriptor, Trust-Tiers
- [`specs/brainz-ink.md`](../specs/brainz-ink.md) — Design-/CSS-Kanon (3-Schicht-Tokens)
- [`specs/performance.md`](../specs/performance.md) — Render-Performance („butterweich"): Listen-Muster (virtualisiert vs `content-visibility`), Layout-Thrash-Regel, Containment, Compositor-Position, Chrome-MCP-Profiling + Checkliste
- [`specs/entity-signals.md`](../specs/entity-signals.md) — **Reaktivität**: `touchEntity`/`subscribeEntity` (widget-Paket), Feeder-Regeln, Konsumenten-Muster (Debounce + Edit-Guards), `ke:`-Namensraum, Wildcard
- [`manifest.json`](./manifest.json) — maschinenlesbarer Vertrag (Regeln, Tokens, Icons, beide Kataloge mit Props)
