UI-Kit
@openchamber/sdk/ui ist ein Satz fertiger Bedienelemente, die Farben, Schriften und Abstände der App verwenden. So sieht deine Erweiterung aus wie der Rest von OpenChamber, ohne dass du dessen Styles kopierst. Jedes Element ist eine Funktion: Du gibst ihr ein Element, das dir gehört, und seine Einstellungen; sie zeichnet sich dort und liefert { update, dispose } zurück. Du setzt die Elemente zu einem Bildschirm zusammen und behältst die Daten bei dir. Das Kit ruft nie deinen Provider oder OpenChamber auf; host.request, host.attach und den Rest rufst du weiterhin selbst. Für alles, was das Kit nicht bietet, kannst du eigenes HTML und CSS schreiben, aber greif zuerst zum Kit: Seine Elemente folgen automatisch dem Theme, den Abständen und den Tastaturgewohnheiten des Nutzers, sodass ein daraus gebautes Panel wie ein Teil von OpenChamber wirkt und nicht wie eine Website darin.
Zuerst applyHostReady aufrufen
Die Elemente lesen ihre Farben aus CSS-Variablen, die applyHostReady auf die Seite schreibt. Es setzt außerdem Schrift und Textfarbe der App auf der Seitenwurzel, sodass auch selbst hinzugefügtes einfaches HTML richtig aussieht. Ruf es in host.onReady vor dem ersten Mount auf, dann folgen die Farben dem Theme der App, wenn der Nutzer es wechselt.
import { applyHostReady } from "@openchamber/sdk/ui";
let mounted = false;host.onReady((ctx) => { applyHostReady(ctx, document.documentElement); if (mounted) return; mounted = true; // Mount controls and register listeners once here.});Das Kit verwendet vom Host berechnete Textfarben für getönte Buttons, Badges, Hinweistitel, Validierungsmeldungen und Links. Du musst Kontraste nicht selbst berechnen.
Für eigenes CSS stellt applyHostReady die Variablen --primary-text, --success-text, --warning-text, --error-text und --info-text sowie passende --oc-*-text-Aliase bereit. Nutze sie für Text auf neutralen oder leicht getönten Hintergründen. Füllungen behalten die Basisfarben; Text auf einer vollen --oc-primary-Füllung verwendet --oc-primary-fg. Wende jeden onReady-Snapshot an, damit die Farben aktuell bleiben.
onReady kann bei geöffneter Seite mehrfach laufen, etwa wenn sich der Sitzungskontext ändert. Wende das Theme jedes Mal an, aber mounte Controls und registriere Listener nur einmal. Bewahre Entwürfe außerhalb der Renderfunktionen auf. Blende beim Tabwechsel vorhandene Panels aus oder stelle ihre Werte aus deinem Zustand wieder her.
Auswahl und Eingaben synchron halten
mountTabs, mountSelect, mountCheckbox und mountSwitch melden Änderungen über onChange, ändern activeId, value oder checked aber nicht selbst. Speichere den Wert und gib ihn mit update zurück. Text- und Suchfelder zeigen Eingaben sofort, brauchen aber ebenfalls update({ value }), sonst kann ein späteres Update alte Werte wiederherstellen. onClick ändert die Variante eines Buttons nicht. Nutze für eine Moduswahl lieber mountTabs.
Mounte diese Controls einmal im vorhandenen root des Panels:
import { mountSelect, mountTabs } from "@openchamber/sdk/ui";
let activeId = "convert";const tabs = mountTabs(root, { items: [{ id: "convert", label: "Convert" }, { id: "format", label: "Format" }], activeId, onChange: (next) => { activeId = next; tabs.update({ activeId }); // Show the matching panel without discarding its draft values. },});
let format = "json";const select = mountSelect(root, { options: [{ id: "json", label: "JSON" }, { id: "yaml", label: "YAML" }], value: format, onChange: (next) => { format = next; select.update({ value: format }); },});Tausche für einen Formatwechsel beide Werte im Zustand und rufe update({ value }) an beiden vorhandenen Select-Handles auf. Mounte dafür keine neuen Controls. Rufe beim Entfernen dispose() auf.
Was du mounten kannst
| Funktion | Was sie zeichnet |
|---|---|
mountButton | Ein Button. variant ist default, secondary, outline, ghost oder destructive; size ist default, sm oder xs. loading zeigt einen Spinner und blockiert Klicks. |
mountTextField | Ein beschriftetes Eingabefeld, oder mit multiline eine Textarea. Optionaler helper- oder error-Text, password-Maskierung und mono für Tokens und IDs. |
mountSearchField | Ein Suchfeld mit Lupe und Löschen-Button. Escape leert es. |
mountSelect | Ein Dropdown. searchable setzt ein Filterfeld oben in die Liste. Pfeiltasten bewegen, Enter wählt, Escape schließt. |
mountCheckbox, mountSwitch | Eine Checkbox oder ein Schalter mit Beschriftung und optionaler description. |
mountTabs | Pill-Tabs, jeder mit optionalem Zähler. Pfeil links und rechts wechseln zwischen ihnen. |
mountBadge | Eine kleine Pill. tone ist neutral, primary, success, warning, error oder info. |
mountList | Eine tastaturfreundliche Liste. Jede Zeile kann einen Schlüssel vorne, einen Titel, einen Untertitel, rechtsbündigen Meta-Text und ein Badge haben. |
mountEmpty | Ein zentrierter Leerzustand mit Titel, einer Textzeile und optionalem Button. |
mountSpinner | Ein Ladering mit optionaler Beschriftung. |
mountBanner | Ein eingefärbter Hinweis mit Titel, Text und optionaler Aktion. |
mountSeparator | Eine dünne Linie, optional mit Beschriftung in der Mitte. |
mountProgress | Ein Fortschrittsbalken von 0 bis 100. |
mountMenu | Ein Button, der eine Liste von Aktionen öffnet. Einträge können destruktiv, deaktiviert oder ein Trenner sein. |
mountText | Text von deinem Provider. Bilder im Markdown-Stil und http(s)-Links werden zu echten Bildern und Links; alles andere bleibt reiner Text, deshalb lässt sich auch nicht vertrauenswürdiger Inhalt sicher anzeigen. |
Neben den Elementen kommen drei einfache Helfer mit. filterSelectOptions(options, query) ist der Abgleich, den mountSelect verwendet. splitTextMedia(text) ist das, was mountText vor dem Zeichnen tut. moveListSelection ist der Tastaturschritt, den Liste, Select und Menü teilen, falls du eine eigene Liste baust.
Eine Liste mit Suche
import { applyHostReady, mountEmpty, mountList, mountSearchField } from "@openchamber/sdk/ui";
let mounted = false;host.onReady((ctx) => { applyHostReady(ctx, document.documentElement); if (mounted) return; mounted = true; const root = document.querySelector("#root")!; const searchRoot = root.appendChild(document.createElement("div")); const listRoot = root.appendChild(document.createElement("div")); const emptyRoot = root.appendChild(document.createElement("div"));
let query = ""; let empty: { dispose: () => void } | null = null;
const list = mountList(listRoot, { items: [], onSelect: (id) => { const task = tasks.find((item) => item.id === id); if (task) void host.attach({ providerId: "acme-hello", id, title: task.title, url: task.url }); }, });
const paint = () => { const rows = tasks .filter((task) => task.title.toLowerCase().includes(query.toLowerCase())) .map((task) => ({ id: task.id, leading: task.key, title: task.title, meta: task.updated })); list.update({ items: rows }); empty?.dispose(); empty = rows.length === 0 ? mountEmpty(emptyRoot, { title: "No tasks match", body: "Try a shorter search." }) : null; };
const search = mountSearchField(searchRoot, { value: query, placeholder: "Search tasks", onChange: (next) => { query = next; search.update({ value: next }); paint(); }, }); paint();});update nimmt nur die Einstellungen, die sich geändert haben. Die Liste behält ihre markierte Zeile über Updates hinweg, solange diese Zeile noch da ist.
Links aus Provider-Text
Deine Seite läuft in einer Sandbox und kann nicht selbst einen neuen Tab öffnen. Wenn du mountText verwendest, gib onOpenUrl mit und reiche den Link an OpenChamber weiter:
import { mountText } from "@openchamber/sdk/ui";
mountText(root, { text: comment.body, onOpenUrl: (url) => void host.openUrl(url),});Verwandt
- Eine Erweiterung bauen für Ordner, Manifest und Installation
- Host API für
connectHost, Anhängen und Fehlercodes