跳转到内容

主题与语言

原生感的一半来自”跟得上宿主”:用户切暗色、换主题色、改界面语言,你的面板要立即跟着变。宿主为这两件事都提供了推送桥,插件不用轮询、不用猜。

主题:装一次桥,用语义变量写 CSS

Section titled “主题:装一次桥,用语义变量写 CSS”

iframe 入口最前面装主题桥:

import { installThemeBridge } from '@xinghan/plugin-sdk/ui'
installThemeBridge()

装好之后宿主会推 theme/init(iframe 加载时)与 theme/update(用户切主题时),桥自动做两件事:

  1. 把宿主的全套 oklch 语义色变量注入你页面的 :root;
  2. 亮/暗模式同步到 <html> 的 dark class(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 首帧不闪黑。
  • 状态语义色(成功/警告/错误)可用固定绿/橙/红系,与宿主惯例一致。

用户可以给整个应用挂一张壁纸(图片 / 动图 / 视频),再拉一根「界面通透度」滑杆让界面的面给它让路。宿主自己的表格、看板、日历、甘特、分组视图都会跟着透。

你的面板不会被自动改掉 —— 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 已经是宿主算好的那一档,用户拉滑杆时它自己会变;你另兑一档,用户就会看到”别的地方都跟着滑杆动,唯独这个面板不动”。

语言有两层,机制不同:

声明侧:manifest 文案用 LocalizedText

Section titled “声明侧:manifest 文案用 LocalizedText”

菜单项、字段类型名、节点名这些由宿主渲染的文案,写多语言对象,宿主按当前语言取(缺当前语言回退第一个):

"title": { "zh-CN": "翻译选中 {count} 格", "en-US": "Translate {count} cells" }

你自己的 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。

面板入口把两座桥一起装上,是所有内置插件的通行开头:

import { installContextBridge, installThemeBridge } from '@xinghan/plugin-sdk/ui'
installThemeBridge() // 颜色与亮暗
installContextBridge() // projectId / activeNode / locale …
  • json-field 的 ui/src/lib/host.ts:两座桥 + waitForContext 的标准封装,值得整段借走;
  • 宿主偏好设置 → 外观:切主题/切语言时打开你的面板,肉眼验证跟随。