OpenHuman 主题系统与 Theme Studio 完全指南:运行时换肤、CSS Token 体系与源码解析
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
本文以 OpenHuman 桌面端(Mac / Windows / Linux)的主题能力为主线,系统讲解内置主题家族、Light/Dark/Auto 三态切换、可视化 Theme Studio 的完整操作,以及驱动这一切的 CSS 自定义属性(Token)体系与 Redux 持久化机制。读完本文,你将掌握如何在 OpenHuman 中挑选、定制、导出并分享主题,同时理解ThemeProvider、themeSlice与tokens.css之间的运行时协作原理,能够基于这套体系为组件编写符合规范的主题化代码。
内置主题家族
OpenHuman 出厂自带五个主题家族(Family),每个家族同时提供Light(亮色)与Dark(暗色)两个变体,覆盖从默认观感到高对比度终端的多种风格取向:
| 家族 | 风格定位 |
|---|---|
| Classic | OpenHuman 的默认外观,Light/Dark 均为空覆盖集,完全依赖tokens.css的默认调色板 |
| Ocean | 以#4A83DD蓝色为主色调的清爽冷色系 |
| Sepia | 温暖、纸张质感,护眼柔和 |
| Matrix | 黑底绿字的高对比度终端风格 |
| HAL 9000 | 深黑背景搭配红色强调色,致敬经典科幻 |
这五个家族在源码中定义于 presets.ts 的THEME_FAMILIES数组中。每个家族是一个ThemeFamily对象,持有light/dark两个变体主题与一个defaultVariant(未指定变体时的默认值)。从源码结构看,classic、ocean、sepia的默认变体是light,而matrix与hal9000默认变体是dark——因为这两个家族的核心身份就是深色系。
各家族的实现细节很有代表性(均位于 presets.ts):
- Ocean用一组
"R G B"通道三元组覆盖surface-canvas(233 242 252)、surface-chrome、line、content等基础面,并把primary-500设为74 131 221,即文档所述的#4A83DD。 - Sepia除了暖色表面(
surface: 250 244 233)外,还把body与heading字体角色指向衬线字体栈'Newsreader', Georgia, Cambria, ...,营造纸书质感。 - Matrix覆盖整个
primary色阶(50~950)为一套荧光绿 ramp,并把body/heading换成等宽字体栈'JetBrains Mono', ...。 - HAL 9000用一套红色 ramp 替换
primary全套色阶,暗色变体下content-inverted被调深以保证白字按钮标签的对比度。
注意:预设中整条
primary-*ramp 的覆盖是有意为之——组件大量使用dark:text-primary-300、bg-primary-600等不同色阶,若主题只覆盖 500~700 三档,其余色阶会回落为默认蓝,导致换肤不完整。这一点在 presets.ts 的注释中有明确说明。
Light / Dark / Auto 三态切换
每个家族都可以以Light、Dark或Auto三种方式应用:
- Light / Dark:固定使用该家族的亮色或暗色变体;
- Auto:跟随操作系统的
prefers-color-scheme设置,系统在亮/暗之间切换的瞬间,应用无需刷新即实时跟随。
Auto 的实时跟随由 ThemeProvider.tsx 实现:当themeVariant === 'system'且当前选中了某个主题家族时,组件通过window.matchMedia('(prefers-color-scheme: dark)')注册change监听器,OS 切换时立即用resolveFamilyVariant(family, dark|light)重新解析并应用对应变体;同时保留了addListener回退以兼容 Safari 14 以下的旧实现。
在状态层,themeVariant与简单的 Appearance 开关通过setThemeVariant/setThemeMode两个 reducer 保持同步(二者互相镜像),见 themeSlice.ts。
Theme Studio:可视化主题编辑器
打开Settings → Theme Studio(面板组件位于 ThemeStudioPanel.tsx),你可以获得一个完整的可视化主题编辑器,能力包括:
- 挑选家族:从主题瓷砖画廊中选取内置家族或你自己的自定义主题;
- 调整每一个颜色 Token:surface(表面)、text(文本)、border(边框)与 accent ramp(强调色阶)都配有原生取色器;当文字与背景的亮度对比跌破可读阈值时,界面会给出实时的对比度警告;
- 按角色切换字体:title、heading、body、mono、serif 五种角色可分别使用不同字体族;
- 配置背景层(Backdrop):可选动画 WebGL 网格、纯色,或自定义图片,并可叠加可选的点阵覆盖层;
- 管理自定义主题:创建、编辑、重置、删除,以及导出 / 导入 JSON与他人分享。
颜色 Token 编辑器
Theme Studio 的颜色编辑分组定义在 tokens.ts 的COLOR_GROUPS中,共四组:
| 分组 | 编辑的 Token | 对应 Tailwind 工具类 |
|---|---|---|
| Surfaces(表面) | surface、surface-canvas、surface-muted、surface-subtle、surface-strong、surface-hover、surface-overlay、surface-chrome | bg-surface、bg-surface-muted、… |
| Text(文本) | content、content-secondary、content-muted、content-faint、content-inverted | text-content、text-content-muted、… |
| Borders(边框) | line、line-strong、line-subtle、line-chrome | border-line、border-line-strong、… |
| Accents(强调色) | primary/sage/amber/coral四个家族的-500基色(更多色阶通过“高级”展开器访问) | bg-primary-500、text-coral-600、… |
UI 边界处的取色器使用 hex,而存储使用"R G B"通道三元组,二者通过 color.ts 的hexToChannels/channelsToHex转换;channelLuminance则计算 WCAG 相对亮度,供对比度警告使用。
字体角色
FONT_ROLES = ['title', 'heading', 'body', 'mono', 'serif']是五种可独立设置的字体角色(tokens.ts)。Theme Studio 的字体选择器基于FONT_CHOICES预设项:Inter、Cabinet Grotesk、System UI、Newsreader(衬线)、Georgia(衬线)、JetBrains Mono,每项对应一组完整的 CSSfont-family回退栈,写入--font-<role>变量。
Backdrop 背景层
背景层支持三种BackdropKind(types.ts):
- mesh:启用动画 WebGL 网格渐变。实现位于 MeshGradient.tsx——它的四个渐变停靠点从活动主题的
primaryramp 与surfacetoken 派生(读取解析后的 CSS 变量转 hex),因此背景会跟随 Matrix 绿、HAL 红、Ocean 蓝等主题自动变色;同时它会优雅捕获 WebGL 错误(Tauri WebView 可能缺少 GPU 上下文),并只在窗口可见且聚焦时运行动画。 - solid:纯色/渐变画布(默认)。
- image:以 cover 方式铺满自定义图片,需要提供
imageUrl。
此外,主题还可携带gradient.canvas(整段 CSSbackground值),通过--app-gradient变量作用于应用背景。注意:内置预设目前不再携带画布渐变(源码注释记录了这一决策——渐变与半透明surface-chrome蒙层叠加后会导致窗口上下颜色不一致),但能力本身保留,用户自定义主题仍可设置。
编辑预设自动 Fork(Auto-Fork)
在 Theme Studio 中修改任意内置预设的 token,会透明地派生出一个全新的自定义主题:原预设保持原样不被改动。你可以从 Ocean 出发微调,同时保留 Ocean 与你的定制版本。
该机制在状态层由ensureEditableCustom实现(themeSlice.ts):当activeThemeId指向一个内置预设(或旧版 id)时,它会把“当前生效主题”复制为custom-<sourceId>命名的自定义主题、写入customThemes并设为活动主题,再返回给编辑操作。基于同一源主题重复编辑是幂等的(复用已存在的custom-<sourceId>),不会产生重复副本。派生主题会记录basedOn: base.id,因此“重置覆盖”时可以恢复预设的原始调色板(resetActiveThemereducer),而不是泛泛的 Light/Dark 默认值。
导出与导入 JSON
自定义主题以 JSON 形式导出/导入(ThemeStudioPanel的导出区会把当前生效主题序列化为美化 JSON 供复制;导入区接受粘贴的 JSON,解析失败会给出错误提示)。Theme类型本身是“部分覆盖集”——colors与fonts只记录被改动的 token(types.ts),因此导出的 JSON 体积小、可读性强,导入后自动成为一个新的自定义主题。分享主题的完整链路就是:导出 JSON → 发送 → 对方粘贴导入。
状态存储:Redux + redux-persist
主题状态(活动主题、Light/Dark/Auto 变体、全部自定义主题)存放在 Redux 的themeslice 中,并通过redux-persist持久化到localStorage,因此应用重启后主题依然保留,且按用户作用域隔离。REHYDRATE时会做一次旧版本兼容迁移:把早期持久化的mode字段映射为新的themeVariant。
themeSlice.ts 的核心状态字段包括:
| 字段 | 含义 |
|---|---|
activeThemeId | 当前选中的家族 id(classic/ocean/matrix/hal9000/sepia)或自定义主题 id |
themeVariant/mode | light/dark/system三态,二者互为镜像 |
customThemes | 用户创作的主题数组(部分或全部 token 覆盖) |
fontSize | 全局字号预设:small(14px) /medium(16px) /large(18px) /xlarge(20px) |
customFontSizePx | 字号微调(px),范围 12~28,非空时覆盖预设(issue #4246) |
tabBarLabels、agentMessageViewMode、developerMode、hideAgentInsights | 其余外观/调试偏好 |
selectEffectiveTheme负责把状态解析为要应用的具体Theme:自定义主题直接返回;否则解析家族 + 变体,system变体通过resolveTheme咨询prefers-color-scheme(非 DOM 环境如 SSR 回落为 light)。旧版持久化的ocean/midnight等 id 也会被规范化(ocean→ 按变体映射到 Ocean 家族、midnight→ Ocean Dark),保证老用户升级后主题选择依然生效。
底层原理:CSS Token 体系
Token 与 RGB 通道三元组
一切换肤都建立在 CSS 自定义属性(变量)之上。app/src/styles/tokens.css是所有可换肤颜色与字体的唯一事实来源:
- 每个颜色 token 以空格分隔的 RGB 通道三元组存储,例如
--surface: 255 255 255;、--primary-500: 47 110 244;; - Light 调色板定义在
:root,Dark 调色板定义在:root.dark; - 字体角色变量为
--font-title/--font-heading/--font-body/--font-mono/--font-serif。
通道三元组格式是强制要求而非风格选择:Tailwind 通过rgb(var(--token) / <alpha-value>)包装这些变量,正是这个格式让bg-surface/50、bg-primary-500/10这类透明度修饰符继续工作。tokens.css头注释明确指出,一旦换成 hex/var 混写,会静默破坏全部约 640 个带透明度后缀的工具类。旧版--cmd-*与--color-*变量集合只是这些规范 token 的薄别名,新增颜色不应再写入它们。
token 的完整分类与 Tailwind 工具类对应关系(亦见 gitbooks/developing/theming.md):
| 组 | Token | Tailwind 工具类 |
|---|---|---|
| 表面 | surface、surface-canvas、surface-muted、surface-subtle、surface-strong、surface-hover、surface-overlay、surface-chrome | bg-surface、bg-surface-muted、… |
| 文本 | content、content-secondary、content-muted、content-faint、content-inverted | text-content、text-content-muted、… |
| 边框 | line、line-strong、line-subtle | border-line、border-line-strong、… |
| 强调色 | primary-*、sage-*、amber-*、coral-*(色阶 50…950) | bg-primary-500、text-coral-600、…(变量支撑、可换肤、名字不变) |
| 字体 | font-title、font-heading、font-body、font-mono、font-serif | font-title、font-heading、font-body、… |
Tailwind v4 接线
本仓库当前使用 Tailwind v4(package.json中为tailwindcss ^4.3.3),接线位于 index.css 的@theme块:例如--color-surface: rgb(var(--surface))、--color-primary-500: rgb(var(--primary-500))、--font-body: var(--font-body)等,把 token 暴露为bg-surface、text-primary-500、font-body等工具类;同时用@custom-variant dark (&:is(.dark *))定义暗色变体。这样,组件里写bg-surface、text-content、border-line时,换肤只需改 token 值,组件代码零改动。
ThemeProvider:运行时应用
ThemeProvider.tsx 负责把“选中的主题”落到 DOM:
- 解析出当前生效的
Theme(通过selectEffectiveTheme); - 把每个颜色 token 覆盖写成
<html>上的内联--<key>变量、每个字体角色写成--font-<role>; - 根据
theme.isDark切换<html>的.darkclass,并同步设置root.style.colorScheme; - 清理机制:记录上一次写入的变量清单,切换主题时,新主题未包含的旧变量会被移除,避免“上一个主题的残留覆盖”泄漏;
- fall-through:主题未指定的 token 自动落到
tokens.css的 Light/Dark 默认值——因此内置 Light/Dark 预设(CLASSIC_LIGHT/CLASSIC_DARK)的colors和fonts是空对象,纯粹靠isDark生效; - 额外处理
--app-gradient(主题的画布渐变)以及根字号(customFontSizePx微调覆盖fontSize预设,写入<html>的font-size,所有基于 rem 的 Tailwind 文本工具类随之缩放)。
在写入前,withDerivedChrome(chrome.ts)会为“染色但未命名窗口边框”的主题补齐surface-chrome与line-chrome两个 token:亮色按画布亮度约 13% 变暗(214/245),暗色在画布基础上 +10 偏移,line-chrome直接跟随line-strong。显式指定值永远优先于派生值。这样即使主题只改了画布色,侧边栏所在的窗口边框(RootShellLayout以 /30 透明度铺在内容卡片外侧的蒙层)也能与主题同色系,而不是残留默认灰。
对比度门禁
仓库对每个内置暗色预设设定了 WCAG AA 门禁测试:presets.contrast.test.ts 会把每个预设的覆盖合并到:root.dark默认值之上(与 ThemeProvider 运行时分层一致),验证正文 4.5:1、大文本/UI 3:1 的可读性,且覆盖所有可能的文字落点表面(含 hover/pressed/overlay);同时有一个“DARK_BASE 一致性”测试解析tokens.css,确保 JS 中的镜像基准与 CSS 事实来源不漂移。测试还带有一份“允许清单”,记录了刻意豁免的 token 与原因。这也是 Theme Studio 中对比度警告的判定依据来源。
组件编写规范与“四 Ramp 天花板”
面向贡献者的规范(详见 gitbooks/developing/theming.md):
- 中性表面/文本/边框一律使用语义工具类(
bg-surface、text-content、border-line),而不是bg-white dark:bg-neutral-900之类的写死搭配——token 会替你翻转明暗,几乎不需要dark:变体; - 语义色使用四个可换肤 ramp(
primary/sage/amber/coral); - 避免在
className或内联style中写死 hex,那会绕过主题系统。
代码库中存在大量“用颜色回答这是谁”的查找表(技能分类、事件日志域、通知提供方、目录来源等)。规范有一条硬性约束:可换肤 ramp 恰好只有四个(primary、sage、amber、coral),Tailwind 默认调色板里的emerald、violet、sky、teal、indigo、cyan、rose、pink、purple等都会解析为固定 oklch 值,完全无视当前主题。因此:
- 同色阶映射到主题等价物:
red → coral、green/emerald → sage、orange → amber、blue → primary; - 没有等价物的色相不给新 ramp(
violet不是“接近 primary”,不要发明第五个 ramp,也不要复用--accent-lavender这类固定 hex); - 超过四色相时,多余行归入表格自带的“未知/其他”中性对(如
bg-surface-subtle text-content-secondary),绝不让两行撞同一个 ramp; - 按“读者会依据哪个区分采取行动”来分配颜色:徽章通常自带文字标签,颜色只是扫描辅助,把四个 ramp 花在改变行为的读取上,其余走中性;
coral语义上是“失败”,把普通行涂成 coral 会让正常状态看起来像故障。
仓库中的实操范例(均见 gitbooks/developing/theming.md):
| 表格 | 行数 | 保留色相 | 理由 |
|---|---|---|---|
skills/skillIcons.tsxCATEGORY_META | 9 | Built-in(primary)、Productivity(sage)、Social(coral)、Tools & Automation(amber) | Channels/Chat/Platform与All/Other共用中性 |
skills/SkillsExplorerTab.tsxSOURCE_COLORS | 6 | built-in(sage)、optional(primary) | 四个远端目录自带名称,来源层级才是关键区分 |
settings/panels/EventLogPanel.tsxDOMAIN_BADGE_COLORS | 11 | tool(primary)、agent(sage)、approval(amber) | 谁执行了动作、什么在等人;coral 刻意空缺 |
品牌色与 Primitive 变体
- 第三方品牌色(Telegram
#249CD8、Discord#5865F2、iMessage#34C759)刻意保留为 hex,因为压平为bg-surface-subtle会把它们抹进旁边泛型徽章里;给它们主题化归属意味着“新增品牌 token”,属于产品决策而非清理工作。 - 不要重绘 primitive 的变体:
<Button variant="primary" className="bg-violet-500">会冻结颜色并破坏 hover/focus/disabled 状态同步——应改染其周围的表面,并去掉覆盖。
迁移 Codemod:把旧的dark:配对折叠成语义工具类
仓库提供幂等的自动化迁移工具scripts/theme-codemod/,把已审计的light dark:Tailwind 配对折叠为语义工具类:
node scripts/theme-codemod/migrate.mjs # dry-run + 报告 node scripts/theme-codemod/migrate.mjs --write # 实际应用 node scripts/theme-codemod/migrate.mjs --selftest # 夹具断言它只重写相邻配对,绝不触碰带透明度后缀的工具类与测试文件;映射表位于scripts/theme-codemod/map.mjs。
更多参考
- Theming(贡献者参考):token 体系、Tailwind 接线、迁移 codemod 的完整规范;
- Realtime Mascot:OpenHuman“个性”的另一大块——实时吉祥物;
- 主题相关测试:ThemeProvider.test.tsx、MeshGradient.test.tsx、presets.contrast.test.ts,可作为理解运行期行为的可执行文档。
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考