Kit de UI
@openchamber/sdk/ui é um conjunto de controles prontos que usam as cores, fontes e espaçamentos do app, para sua extensão ficar com a cara do resto do OpenChamber sem copiar os estilos dele. Cada controle é uma função: você entrega um elemento que é seu e as configurações, ele se desenha ali e devolve { update, dispose }. Você junta os controles em uma tela e fica com os dados. O kit nunca chama seu provedor nem o OpenChamber; você continua chamando host.request, host.attach e o resto por conta própria. Você pode escrever seu próprio HTML e CSS para tudo que o kit não tem, mas comece pelo kit: os controles dele seguem automaticamente o tema, os espaçamentos e os hábitos de teclado do usuário, então um painel montado com eles parece parte do OpenChamber, e não um site dentro dele.
Chame applyHostReady primeiro
Os controles leem suas cores de variáveis CSS que applyHostReady escreve na página. Ele também aplica a fonte e a cor de texto do app na raiz da página, então o HTML simples que você adicionar por conta própria também fica certo. Chame-o dentro de host.onReady antes da primeira montagem, e as cores acompanham o tema do app quando o usuário troca.
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.});O kit usa cores de texto calculadas pelo host em botões com tons suaves, badges, títulos de avisos, mensagens de validação e links. Não é necessário calcular o contraste.
Para CSS próprio, applyHostReady fornece --primary-text, --success-text, --warning-text, --error-text e --info-text, além dos aliases correspondentes --oc-*-text. Use essas variáveis para texto sobre fundos neutros ou levemente coloridos. Mantenha as cores base nos preenchimentos e use --oc-primary-fg para texto sobre um preenchimento sólido --oc-primary. Aplique cada snapshot de onReady para manter as cores atualizadas.
onReady pode executar várias vezes enquanto o painel está aberto, inclusive quando o contexto da sessão muda. Aplique o tema em cada chamada, mas monte controles e registre listeners apenas uma vez. Guarde rascunhos fora das funções de renderização. Ao trocar de aba, oculte os painéis existentes ou restaure seus valores a partir do estado.
Sincronize a seleção e os campos
mountTabs, mountSelect, mountCheckbox e mountSwitch informam mudanças por onChange, mas não alteram activeId, value ou checked sozinhos. Salve o valor e devolva-o com update. Campos de texto e busca mostram a digitação imediatamente, mas também precisam de update({ value }) para que uma atualização posterior não restaure um valor antigo. onClick não muda a variante do botão. Para escolher um modo, prefira mountTabs.
Monte estes controles uma vez no root existente do painel:
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 }); },});Para inverter formatos, troque os dois valores no estado e chame update({ value }) nos dois selects existentes. Não monte controles novos para mudar seus valores. Chame dispose() ao remover um controle.
O que você pode montar
| Função | O que desenha |
|---|---|
mountButton | Um botão. variant é default, secondary, outline, ghost ou destructive; size é default, sm ou xs. loading mostra um spinner e bloqueia cliques. |
mountTextField | Um campo com rótulo, ou um textarea com multiline. Texto opcional de helper ou error, mascaramento com password, e mono para tokens e ids. |
mountSearchField | Uma caixa de busca com lupa e botão de limpar. Escape limpa. |
mountSelect | Um dropdown. searchable adiciona uma caixa de filtro no topo da lista. Setas movem, Enter escolhe, Escape fecha. |
mountCheckbox, mountSwitch | Uma caixa de seleção ou um interruptor com rótulo e uma description opcional. |
mountTabs | Abas em formato de pílula, cada uma com um contador opcional. As setas esquerda e direita movem entre elas. |
mountBadge | Uma pílula pequena. tone é neutral, primary, success, warning, error ou info. |
mountList | Uma lista navegável pelo teclado. Cada linha pode ter uma chave à esquerda, um título, um subtítulo, texto meta alinhado à direita e um badge. |
mountEmpty | Um estado vazio centralizado com título, uma linha de texto e um botão opcional. |
mountSpinner | Um anel de carregamento com rótulo opcional. |
mountBanner | Um aviso com tom, título, texto e uma ação opcional. |
mountSeparator | Uma linha fina, opcionalmente com um rótulo no meio. |
mountProgress | Uma barra de progresso de 0 a 100. |
mountMenu | Um botão que abre uma lista de ações. Os itens podem ser destrutivos, desabilitados ou um separador. |
mountText | Texto vindo do seu provedor. Imagens e links http(s) no estilo Markdown viram imagens e links de verdade; todo o resto fica como texto puro, então é seguro mostrar conteúdo não confiável. |
Três helpers simples acompanham os controles. filterSelectOptions(options, query) é a busca que mountSelect usa. splitTextMedia(text) é o que mountText faz antes de desenhar. moveListSelection é o passo de teclado que a lista, o select e o menu compartilham, caso você monte sua própria lista.
Uma lista com busca
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 recebe só as configurações que mudaram. A lista mantém a linha destacada entre atualizações enquanto essa linha continuar existindo.
Links no texto do provedor
Sua página roda em um ambiente isolado e não consegue abrir uma nova aba sozinha. Ao usar mountText, passe onOpenUrl e entregue o link ao OpenChamber:
import { mountText } from "@openchamber/sdk/ui";
mountText(root, { text: comment.body, onOpenUrl: (url) => void host.openUrl(url),});Relacionado
- Crie uma extensão para a pasta, o manifesto e a instalação
- API do host para
connectHost, attach e os códigos de erro