Pular para o conteúdo
Navegar Abrir escFechar

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çãoO que desenha
mountButtonUm botão. variant é default, secondary, outline, ghost ou destructive; size é default, sm ou xs. loading mostra um spinner e bloqueia cliques.
mountTextFieldUm campo com rótulo, ou um textarea com multiline. Texto opcional de helper ou error, mascaramento com password, e mono para tokens e ids.
mountSearchFieldUma caixa de busca com lupa e botão de limpar. Escape limpa.
mountSelectUm dropdown. searchable adiciona uma caixa de filtro no topo da lista. Setas movem, Enter escolhe, Escape fecha.
mountCheckbox, mountSwitchUma caixa de seleção ou um interruptor com rótulo e uma description opcional.
mountTabsAbas em formato de pílula, cada uma com um contador opcional. As setas esquerda e direita movem entre elas.
mountBadgeUma pílula pequena. tone é neutral, primary, success, warning, error ou info.
mountListUma 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.
mountEmptyUm estado vazio centralizado com título, uma linha de texto e um botão opcional.
mountSpinnerUm anel de carregamento com rótulo opcional.
mountBannerUm aviso com tom, título, texto e uma ação opcional.
mountSeparatorUma linha fina, opcionalmente com um rótulo no meio.
mountProgressUma barra de progresso de 0 a 100.
mountMenuUm botão que abre uma lista de ações. Os itens podem ser destrutivos, desabilitados ou um separador.
mountTextTexto 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.

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