主题与语言
原生感的一半来自”跟得上宿主”:用户切暗色、换主题色、改界面语言,你的面板要立即跟着变。宿主为这两件事都提供了推送桥,插件不用轮询、不用猜。
主题:装一次桥,用语义变量写 CSS
Section titled “主题:装一次桥,用语义变量写 CSS”iframe 入口最前面装主题桥:
import { installThemeBridge } from '@xinghan/plugin-sdk/ui'installThemeBridge()装好之后宿主会推 theme/init(iframe 加载时)与 theme/update(用户切主题时),桥自动做两件事:
- 把宿主的全套 oklch 语义色变量注入你页面的
:root; - 亮/暗模式同步到
<html>的darkclass(Tailwind 的dark:variant 直接可用)。
于是你的 CSS 只写语义变量,永远不写死颜色:
.panel { background: var(--background); color: var(--foreground); border: 1px solid var(--border);}.panel .hint { color: var(--muted-foreground); }.panel .cta { background: var(--primary); color: var(--primary-foreground); }.panel .card { background: var(--muted); border-radius: 8px; }宿主推送的全部 24 个变量(与宿主 shadcn 语义一致,oklch 色值):
| 变量 | 用途 |
|---|---|
--background / --foreground | 页面底色 / 正文色 |
--card / --card-foreground | 卡片容器底色 / 其上文字 |
--popover / --popover-foreground | 弹出层(下拉/浮窗)底色 / 其上文字 |
--primary / --primary-foreground | 主色(主按钮/选中态)/ 主色上的文字 |
--secondary / --secondary-foreground | 次级按钮底色 / 其上文字 |
--muted / --muted-foreground | 弱底色(禁用/占位区)/ 次要文字 |
--accent / --accent-foreground | 悬停/激活高亮 / 其上文字 |
--destructive / --destructive-foreground | 危险动作(删除)/ 其上文字 |
--success / --success-foreground | 成功态 / 其上文字 |
--warning / --warning-foreground | 警告态 / 其上文字 |
--border | 边框 |
--input | 输入框边框 |
--ring | 聚焦环 |
--radius | 圆角基准(长度值,如 0.5rem —— 想跟宿主一样圆就用它) |
成对的 -foreground 变量是”那个底色之上该用的文字色”——按对使用(--primary 配 --primary-foreground),对比度就永远是对的,亮暗主题切换零操心。
三条纪律:
- 不写死 hex。写死的颜色在暗色主题下必然对比失衡 —— 这也是宿主对
fileTypes.color只收色板槽名/色相角、拒收裸 hex 的同一理由。 - 给未装桥的瞬间留兜底:
var(--background, #fff)的 fallback 让 iframe 首帧不闪黑。 - 状态语义色(成功/警告/错误)可用固定绿/橙/红系,与宿主惯例一致。
壁纸:宿主报状态,插件自己决定
Section titled “壁纸:宿主报状态,插件自己决定”用户可以给整个应用挂一张壁纸(图片 / 动图 / 视频),再拉一根「界面通透度」滑杆让界面的面给它让路。宿主自己的表格、看板、日历、甘特、分组视图都会跟着透。
你的面板不会被自动改掉 —— iframe 里跑的是你的排版,同一张照片在多维表格里是”纸变玻璃”,在一个满屏深色图表的面板里可能直接把图洗没。所以宿主只把状态推给你,跟不跟、跟到哪一档由你定。
装了主题桥之后,挂壁纸时你的 <html> 上会多出两个属性:
| 属性 | 含义 |
|---|---|
data-xh-backdrop | 宿主底下有张壁纸 |
data-xh-seethrough | 用户真的把通透度拉起来了 —— 宿主界面的面已经在让路 |
两个是分开的:只挂壁纸意味着”底下有张图”,而”哪些面让路”是另一个决定。data-xh-backdrop 有、data-xh-seethrough 没有时,壁纸只从界面四周露出来,面照旧不透,你也不该动。
同时 tokens 里会多出三个变量(没挂壁纸时它们不存在):
| 变量 | 用途 |
|---|---|
--xh-backdrop-surface | 宿主表格的纸面色,带 alpha —— 想跟宿主同一档通透度,拿它当底 |
--xh-backdrop-ink | 正文墨色 |
--xh-backdrop-halo | 文字光晕色:字压在照片亮处时描一圈同色极窄边,不糊图、不加底 |
纯 CSS 就能跟,一行 JS 都不用改 —— 变量不存在时自动落到兜底:
.panel { background: var(--background);}/* 宿主让路时,我也让路,而且是同一档通透度 */:root[data-xh-seethrough] .panel { background: var(--xh-backdrop-surface, var(--background)); text-shadow: 0 0 3px var(--xh-backdrop-halo, transparent);}要在 JS 里判,读 theme/init / theme/update 消息的 backdrop 字段(ThemeBackdrop,字段同上,另带 surface / ink / halo 的字面色)。旧宿主不带这个字段,按”没挂壁纸”处理即可。
两条纪律:
- 弹层、菜单、下拉别跟着透。半透明的浮层压在一张照片上是读不了的 —— 宿主自己也只让大块的面让路,
--popover一律保持实底。 - 别自己去兑 alpha。
--xh-backdrop-surface已经是宿主算好的那一档,用户拉滑杆时它自己会变;你另兑一档,用户就会看到”别的地方都跟着滑杆动,唯独这个面板不动”。
语言:声明侧与运行侧
Section titled “语言:声明侧与运行侧”语言有两层,机制不同:
声明侧:manifest 文案用 LocalizedText
Section titled “声明侧:manifest 文案用 LocalizedText”菜单项、字段类型名、节点名这些由宿主渲染的文案,写多语言对象,宿主按当前语言取(缺当前语言回退第一个):
"title": { "zh-CN": "翻译选中 {count} 格", "en-US": "Translate {count} cells" }运行侧:iframe 里读 context.locale
Section titled “运行侧:iframe 里读 context.locale”你自己的 iframe UI 语言,从宿主上下文拿 —— 别用 navigator.language(桌面端那是操作系统语言,与应用内语言设置可能不一致):
import { installContextBridge, getContext, onContextChange } from '@xinghan/plugin-sdk/ui'
installContextBridge()
const messages = { zh: { save: '保存', empty: '还没有内容' }, en: { save: 'Save', empty: 'Nothing here yet' },} as const
function t(key: keyof (typeof messages)['zh']): string { // 宿主当前给的值是 'zh' / 'en'。用**前缀匹配**做防御 —— 未来出现 // 'zh-CN' 这类带地区的变体也不会突然回退错语言 const locale = getContext().locale ?? 'zh' const lang = locale.startsWith('en') ? 'en' : 'zh' return messages[lang][key]}
onContextChange(render) // 用户切语言 → 宿主推 context/update → 重渲染小插件一个字典对象就够;上了规模再引 i18n 库,数据源同样是 context.locale。
一次配齐的入口模板
Section titled “一次配齐的入口模板”面板入口把两座桥一起装上,是所有内置插件的通行开头:
import { installContextBridge, installThemeBridge } from '@xinghan/plugin-sdk/ui'
installThemeBridge() // 颜色与亮暗installContextBridge() // projectId / activeNode / locale …json-field的ui/src/lib/host.ts:两座桥 +waitForContext的标准封装,值得整段借走;- 宿主偏好设置 → 外观:切主题/切语言时打开你的面板,肉眼验证跟随。