UI 套件
@openchamber/sdk/ui 是一组现成的控件,采用应用的颜色、字体和间距,让你的扩展无需复制样式就能与 OpenChamber 的其余部分保持一致。每个控件都是一个函数:你给它一个你拥有的元素和它的设置,它就在那里绘制自己并返回 { update, dispose }。你把控件组合成一个界面,并自己管理数据。套件从不调用你的提供方或 OpenChamber;host.request、host.attach 等仍由你自己调用。套件没有的东西你可以自己编写 HTML 和 CSS,但请先用套件:它的控件会自动跟随用户的主题、间距和键盘习惯,所以用它们搭出的面板像是 OpenChamber 的一部分,而不是嵌在里面的网站。
先调用 applyHostReady
控件从 applyHostReady 写入页面的 CSS 变量中读取颜色。它还会把应用的字体和文字颜色设置到页面根元素上,所以你自己添加的普通 HTML 看起来也正常。在 host.onReady 内、第一次挂载之前调用它,这样用户切换主题时颜色会跟随应用的主题。
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.});UI kit 在浅色按钮、徽章、提示标题、输入校验消息和链接中使用宿主计算的文字颜色。无需自行计算对比度。
对于自定义 CSS,applyHostReady 会提供 --primary-text、--success-text、--warning-text、--error-text 和 --info-text,以及对应的 --oc-*-text 别名。请将它们用于中性或浅色背景上的文字。填充仍使用基础颜色,实心 --oc-primary 背景上的文字使用 --oc-primary-fg。应用每次 onReady 快照,让颜色保持最新。
面板打开期间,onReady 可能执行多次,包括会话上下文变化时。每次都应用主题,但只挂载一次控件、注册一次监听器。把草稿值保存在渲染函数之外。切换标签页时隐藏现有面板,或从状态恢复值,不要清空输入。
同步选择和输入状态
mountTabs、mountSelect、mountCheckbox 和 mountSwitch 通过 onChange 通知变化,但不会自行修改 activeId、value 或 checked。保存新值后,通过 update 传回。文本和搜索输入框会立即显示输入,但仍需 update({ value }),否则后续更新可能恢复旧值。按钮的 onClick 不会改变 variant。模式选择建议使用 mountTabs。
在面板现有的 root 中只挂载一次这些控件:
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 }); },});交换格式时,先交换状态中的两个值,再对两个现有 select 调用 update({ value })。不要为了改值重新挂载控件。移除控件时调用 dispose()。
可以挂载的控件
| 函数 | 绘制的内容 |
|---|---|
mountButton | 一个按钮。variant 为 default、secondary、outline、ghost 或 destructive;size 为 default、sm 或 xs。loading 显示加载指示并阻止点击。 |
mountTextField | 带标签的输入框,或使用 multiline 的多行文本框。可选的 helper 或 error 文本、password 掩码,以及用于令牌和 id 的 mono。 |
mountSearchField | 带放大镜和清除按钮的搜索框。Escape 清空它。 |
mountSelect | 一个下拉框。searchable 在列表顶部添加筛选框。方向键移动,Enter 选择,Escape 关闭。 |
mountCheckbox、mountSwitch | 带标签和可选 description 的复选框或开关。 |
mountTabs | 药丸式标签页,每个可带可选计数。左右方向键在它们之间移动。 |
mountBadge | 一个小药丸标记。tone 为 neutral、primary、success、warning、error 或 info。 |
mountList | 键盘友好的列表。每行可以有前置键、标题、副标题、右对齐的附加文本和一个标记。 |
mountEmpty | 居中的空状态,带标题、一行正文和可选按钮。 |
mountSpinner | 带可选标签的加载环。 |
mountBanner | 带色调的通知,含标题、正文和可选操作。 |
mountSeparator | 一条细线,中间可带标签。 |
mountProgress | 0 到 100 的进度条。 |
mountMenu | 打开操作列表的按钮。菜单项可以是危险操作、禁用项或分隔线。 |
mountText | 来自你的提供方的文本。Markdown 风格的图片和 http(s) 链接会变成真正的图片和链接;其他内容保持纯文本,因此可以安全显示不受信任的内容。 |
控件旁还附带三个纯函数辅助工具。filterSelectOptions(options, query) 是 mountSelect 使用的匹配逻辑。splitTextMedia(text) 是 mountText 在绘制前做的处理。moveListSelection 是列表、下拉框和菜单共用的键盘步进逻辑,供你构建自己的列表时使用。
带搜索的列表
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 只接受发生变化的设置。只要高亮的行仍然存在,列表就会在更新之间保持该高亮。
提供方文本中的链接
你的页面运行在沙箱中,无法自行打开新标签页。使用 mountText 时,传入 onOpenUrl,把链接交给 OpenChamber:
import { mountText } from "@openchamber/sdk/ui";
mountText(root, { text: comment.body, onOpenUrl: (url) => void host.openUrl(url),});