Komponenten
Was eine zenit-Komponente verspricht, wann Sie eine Ebene tiefer gehen sollten, und die drei Verträge — Mounting, Overlays und Tests. Aufnahmen, Props und Ergebnistypen jeder Komponente finden Sie auf der Komponenten-Website.
Was „Komponente“ hier bedeutet
Eine zenit-Komponente ist kein Helper, der Farben und Eckradien zusammenklebt. Eine öffentliche Komponente bündelt typischerweise Knotenstruktur, persistenten Zustand, Event-Routing, Fokus- und Tastatursemantik, Barrierefreiheitseigenschaften, Theme-Tokens, Overlay-Lebensdauer und eine Testgrenze, die der Harness finden und zurücklesen kann. Alle werden aus ui.widgets exportiert.
Zuerst die richtige Ebene wählen
ui.widgets.*Geschäftszustand, Texte, Callbacks und Platzierungui.hooks · ui.interaction · ui.select_headlessZeichnen, Tokens, a11y-Labels und Kompositionsgrenzenui.Node + ui.eventsHit-Testing, Tastatur, Fokus, IME, a11y und TestvertragKatalog
Jede Komponente hat eine eigene Seite auf der Komponenten-Website: eine Aufnahme der echten zenit Storybook.app, aus dem Quellcode generierte Props und Ergebnistypen sowie das, was die Aufnahme belegt. Der Harness findet jede Story über ihre test_id, steuert einen virtuellen Cursor oder Eingaben und nimmt das Metal-Drawable direkt auf.
Mount, Zustand & Aufräumen
Die meisten visuellen Komponenten nutzen die Builder-Form Config.mount(scope, cx): ui.widgets.Button(props) gibt einen Builder zurück, und erst mount baut den Baum. Der Rückgabewert ist entweder der Wurzel-*Node oder eine Ergebnis-Struct mit Handles wie wrapper, trigger, body, panel und state.
const EditorActions = struct {
document: *Document,
fn save(self: *EditorActions) void {
self.document.save();
}
};
const bindings = try cx.bindState(EditorActions, .{ .document = document });
const save = try ui.widgets.Button(.{
.label = "Save",
.variant = .primary,
.on_click = cx.on(EditorActions, bindings, EditorActions.save),
}).mount(scope, cx);
const name = try ui.widgets.Input(.{
.label_text = "Project name",
.placeholder = "Untitled",
.required = true,
.width = 320,
}).mount(scope, cx);
try form.appendChild(cx.allocator, name);
try form.appendChild(cx.allocator, save);Einige Komponenten nutzen die Funktionsform und erhalten Props, Scope und cx in einem Aufruf: mountScrollArea, mountGrid sowie Select, ComboBox, TagsInput, NumberStepper, FileUpload und DataTable (das sind Aliase von mountX-Funktionen in ui.widgets, aufgerufen als ui.widgets.Select(props, scope, cx)).
// Function form: props, scope and cx in one call; returns a handle struct.
const area = try ui.widgets.mountScrollArea(.{ .height = 320 }, scope, cx);
try area.content.appendChild(cx.allocator, list);
try root.appendChild(cx.allocator, area.container);Button · Input*NodeModalModalResult{ overlay, dialog, body, portaled }TooltipTooltipResult{ wrapper, trigger, content }SelectSelectMount{ wrapper, trigger, panel, state, is_open }mountScrollAreaScrollAreaResult{ container, content, state }mountGridGridResult{ root, state }Behandeln Sie die Config nicht als Live-Props, in die Sie später schreiben können. Eine Komponente liest ihre Config beim Mounten, um Knoten und Zustand aufzubauen — ein signal.get() in der Config wird nur einmal als Snapshot gelesen. Sich ständig ändernde Daten fließen über den zurückgegebenen State, ein Signal (etwa .visible(sig) von Modal) oder die öffentlichen Methoden der Komponente ein; halten Sie nie einen Zeiger auf eine temporäre Config über Frames hinweg. Der interne Zustand einer Komponente liegt im Kind-Scope, den mount erzeugt, und wird freigegeben, wenn der Eltern-Scope disposed wird.
Warum Overlays Komponenten sein müssen
Tooltip, Popover, Menu, Modal, Sheet und der In-App-Notifier registrieren alle eine Ebene beim OverlayStack. Ebenen mit Barrier (Modal / Sheet) werden in das fensterweite Portal (WindowOverlayPortal unter cx.root) umgehängt und entkommen so dem Overflow-Clipping aller Vorfahren. Z-Werte stammen aus semantischen Tiers — overlay 100, dialog 1000, toast 2000 (wo der Notifier liegt), tooltip 3000, das Devtools-Overlay fest bei 32000 — und ein aus einem anderen Overlay geöffnetes Overlay liegt stets über seinem Host. Absolute Positionierung und ein hoher z-index auf einer Box liefern nichts davon.
const visible = try scope.createSignal(bool, false);
const modal = try ui.widgets.Modal(.{
.title = "Delete document?",
.width = 420,
}).visible(visible).mount(scope, cx);
try modal.body.appendChild(cx.allocator, confirm_content);
// modal.portaled == true: the barrier already lives under the window-level
// portal. Do not append modal.overlay to the current tree.
// Open it from any handler:
visible.set(true);- ✓
Klicks im Inneren bleiben im Dialog / Panel und lösen nie outside-dismiss aus; ein Klick auf die Barrier schließt gemäß
close_on_overlay. - ✓
Escape schließt nur das oberste Overlay; verschachtelte Ebenen werden eine nach der anderen abgebaut.
- ✓
Fokus: Ein offenes Modal fängt den Fokus ein und fokussiert automatisch; beim Schließen wird der Fokus an die vorherige Stelle zurückgesetzt.
- ✓
Scope-Dispose löst den Barrier-Teilbaum aktiv aus dem Portal, sodass beim Seitenwechsel nie eine unsichtbare Trefferebene zurückbleibt.
- ✓
Ausblend-Übergänge halten das Overlay sichtbar, bis die Animation abgeschlossen ist, und setzen es dann aus — kein Sprung zum Endzustand.
Für nicht blockierende, app-weite Benachrichtigungen verwenden Sie ui.widgets.Notifier (ersetzt den alten ToastManager): Notifier.init(scope, cx, .{}) registriert eine Ebene im toast-Tier und mountet, sofern das Fensterportal existiert, dort seinen fensterfüllenden Container (ist portaled false, hängen Sie container selbst an Ihre Wurzel). Danach gibt show(.{ .tone = .success, .title = "Saved" }) eine ID zurück, update ändert eine Karte an Ort und Stelle und dismiss entfernt sie. Schließen, Ablauf und Button-Aktionen kommen als Listener-Events zur App zurück; Verlauf, Nicht-stören und andere Logik einer Mitteilungszentrale gehören in die App.
Der Showcase ist zugleich eine Regressionssuite
e2e/storybook.test.ts wechselt über nav.<key> zu jeder Story, sammelt den Text des Teilbaums, prüft, dass jeder erwartete Datenwert vorhanden ist, und erstellt dann einen Screenshot; interaktive Komponenten prüfen zusätzlich Zustand, Geometrie oder Pixel nach der Interaktion. Die Aufnahmen der Komponenten-Website nutzen dieselbe App und dieselben semantischen IDs, sodass Demo und Test nie in zwei Implementierungen auseinanderlaufen.