跳转到内容
导航 打开 esc关闭

UI 套件

@openchamber/sdk/ui 是一组现成的控件,采用应用的颜色、字体和间距,让你的扩展无需复制样式就能与 OpenChamber 的其余部分保持一致。每个控件都是一个函数:你给它一个你拥有的元素和它的设置,它就在那里绘制自己并返回 { update, dispose }。你把控件组合成一个界面,并自己管理数据。套件从不调用你的提供方或 OpenChamber;host.requesthost.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 可能执行多次,包括会话上下文变化时。每次都应用主题,但只挂载一次控件、注册一次监听器。把草稿值保存在渲染函数之外。切换标签页时隐藏现有面板,或从状态恢复值,不要清空输入。

同步选择和输入状态

mountTabsmountSelectmountCheckboxmountSwitch 通过 onChange 通知变化,但不会自行修改 activeIdvaluechecked。保存新值后,通过 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一个按钮。variantdefaultsecondaryoutlineghostdestructivesizedefaultsmxsloading 显示加载指示并阻止点击。
mountTextField带标签的输入框,或使用 multiline 的多行文本框。可选的 helpererror 文本、password 掩码,以及用于令牌和 id 的 mono
mountSearchField带放大镜和清除按钮的搜索框。Escape 清空它。
mountSelect一个下拉框。searchable 在列表顶部添加筛选框。方向键移动,Enter 选择,Escape 关闭。
mountCheckboxmountSwitch带标签和可选 description 的复选框或开关。
mountTabs药丸式标签页,每个可带可选计数。左右方向键在它们之间移动。
mountBadge一个小药丸标记。toneneutralprimarysuccesswarningerrorinfo
mountList键盘友好的列表。每行可以有前置键、标题、副标题、右对齐的附加文本和一个标记。
mountEmpty居中的空状态,带标题、一行正文和可选按钮。
mountSpinner带可选标签的加载环。
mountBanner带色调的通知,含标题、正文和可选操作。
mountSeparator一条细线,中间可带标签。
mountProgress0 到 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),
});

相关页面