主题与明暗模式

插件如何跟随宿主的明暗主题、主题色与窗口透明度,三步接入与底层机制。

设计令牌的两套值(亮 / 暗)靠 html 上的 class 切换。这一章讲清楚切换机制,以及插件接入主题联动的标准做法。

切换机制

体系支持三条通道,优先级从高到低:

通道触发方式效果
强制暗色html.dark应用整套暗色变量,color-scheme: dark
强制亮色html.light应用亮色变量,color-scheme: light
跟随系统无 class按系统 prefers-color-scheme 自动切换

color-scheme 同时让原生控件(<select> 弹出面板、滚动条、表单底色)跟随主题,这是免费获得的一致性。

宿主设置里选「跟随系统 / 浅色 / 深色」时,会向所有 WebView 广播 update-theme-info 事件;主题色(六预设 + 自定义)与窗口透明度也走同一事件。

三步接入(工程型插件)

第一步,入口引入全局样式(设计令牌都在里面):

import '@udhold/udhold-ui/styles/global.css'

第二步,入口调用一次 setupThemeSync():

import { setupThemeSync } from '@udhold/udhold-ui/utils'

setupThemeSync() // 拉取宿主当前主题 + 监听后续变更,宿主不可用时静默降级

它做三件事:向宿主查询当前 ThemeInfo 立即应用;监听 update-theme-info 事件实时跟随;每次变更同步三样东西——html.dark/light class(明暗)、body.theme-* class 或内联 --primary-color(主题色)、--window-opacity / --panel-opacity(透明度)。

第三步,组件里需要感知明暗做逻辑分支时(比如给代码编辑器换主题),用 useColorScheme:

<script setup lang="ts">
import { computed } from 'vue'
import { useColorScheme } from '@udhold/udhold-ui/composables'

const { isDark } = useColorScheme()
const editorTheme = computed(() => (isDark.value ? 'vs-dark' : 'vs'))
</script>

isDark 是响应式的:宿主切换主题的瞬间自动更新。样式层面则不需要它——令牌本身就是双份的,跟着 class 自动切换。

免构建插件的做法

没有 npm 工程的静态插件拿不到 setupThemeSync,用宿主桥直接暴露的 API 等价实现:

<script>
  // 初始快照:webview 创建时宿主注入
  const boot = window.__UDHOLD_BOOT__ || {}
  apply(boot.theme || {})

  // 跟随变更
  window.udhold.onThemeChange((theme) => apply(theme))

  function apply(info) {
    const dark = info.theme === 'dark'
      || (info.theme !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches)
    document.documentElement.classList.toggle('dark', dark)
    document.documentElement.classList.toggle('light', !dark)
  }
</script>

页面样式则按双通道写:

body { background: #f1f2f3; color: #1f2124; }
html.dark body { background: #1c1d1f; color: #f2f3f4; }

主题色的正确用法

  1. 永远用 var(--primary-color) 表达主色,不要抄某个预设的色值——用户切到紫色主题时,写死的蓝色会立刻变得刺眼。
  2. 选中、高亮、品牌氛围用派生令牌 --primary-light-bg(10% 底)与 --primary-hover(hover 加深)。
  3. 红 / 绿 / 黄等结果语义用 --danger-color / --success-color / --warning-color,与主色区分开:主色表达「这是谁」,语义色表达「发生了什么」。

检查清单

上线前过一遍:

  • 入口调用了 setupThemeSync(),宿主切明暗时插件实时跟随
  • 样式里没有写死的颜色(搜一遍 # 开头的色值)
  • 图片图标用 currentColor 或自适应反色(见图标体系与 AdaptiveIcon)
  • 暗色下检查过对比度:次级文字 --ink-2 在 --surface 上仍可读
  • 原生控件(下拉、日期选择)没有突兀地保持亮色(确认没有显式设置 color-scheme)

接下来看基础样式类:按钮、输入框、开关这些高频控件,引完样式文件就能直接写 class 用。