> For the complete documentation index, see [llms.txt](https://tinyhumans.gitbook.io/openhuman/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tinyhumans.gitbook.io/openhuman/zh/kai-fa-zhong/theming.md).

# 主题化（Token 系统）

OpenHuman 可在运行时完全重新换肤。颜色和字体由 CSS 变量（即“令牌”）驱动，因此主题只是这些变量的一组值。此页是令牌系统的贡献者参考。

## 工作原理

1. **令牌**: `app/src/styles/tokens.css` 定义每一种可主题化颜色为一个以空格分隔的 **RGB 通道三元组** （例如 `--surface: 255 255 255;`）以及字体角色变量（`--font-title/heading/body/mono/serif`）。浅色调色板位于 `:root`；深色调色板位于 `:root.dark`.
2. **Tailwind 绑定**: `app/tailwind.config.js` 通过 `rgb(var(--token) / <alpha-value>)`。该 `<alpha-value>` 形式可确保不透明度修饰符正常工作（`bg-surface/50`, `bg-primary-500/10`）。因此必须使用通道格式：切勿把令牌存成十六进制字符串。
3. **运行时应用**: `app/src/providers/ThemeProvider.tsx` 解析当前激活的 `主题` 并将其覆盖值写成内联 `--token` / `--font-<role>` 变量写到 `<html>`，切换 `.dark` 由 `theme.isDark`。主题未覆盖的变量会沿用 tokens.css 中的默认值；切换主题时，会移除上一个主题遗留的变量。
4. **状态**: `app/src/store/themeSlice.ts` 保存 `activeThemeId` 和 `customThemes`。内置预设位于 `app/src/lib/theme/presets.ts`。用户在以下位置编辑主题： **设置 → 主题工作室** (`app/src/components/settings/panels/ThemeStudioPanel.tsx`).

## 令牌分类

| 分组  | 令牌                                                                                                                   | Tailwind 工具类                                          |
| --- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| 表面  | `surface`, `surface-canvas`, `surface-muted`, `surface-subtle`, `surface-strong`, `surface-hover`, `surface-overlay` | `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`，……          |

旧版 `--cmd-*` 和 `--color-*` 变量集只是这些规范令牌的薄别名。不要在那里新增颜色。

## 编写组件

* 使用语义化工具类（`bg-surface`, `text-content`, `border-line`）来处理中性表面/文本/边框，而不要使用 `bg-white dark:bg-neutral-900` 等。对于这些，你几乎从不需要 `dark:` 变体，因为令牌会自动为你切换。
* 使用强调色调色板（`primary`/`sage`/`amber`/`coral`）作为语义颜色；它们无需额外工作即可主题化。
* 避免在 `className` 或内联 `style`中硬编码十六进制值，因为那会绕过主题化。

## 颜色即身份：四条色阶的上限

这个代码库里反复出现的一种结构，是用一种颜色来回答“这是什么东西？”的查找表——技能类别、事件日志域、通知提供方、目录来源。那些表格之所以总会不断回到默认 Tailwind 色阶，是因为九行表想要九种色相，而应用只提供四种。

**恰好只有四条可主题化色阶： `primary`, `sage`, `amber`, `coral`.** Tailwind 默认调色板中的其他一切（`emerald`, `violet`, `sky`, `teal`, `indigo`, `cyan`, `rose`, `pink`, `purple`，……）都会解析为固定的 oklch 值，完全无视用户当前的主题。基于这些色相构建的表格，在默认皮肤下看起来没问题，但在其他皮肤里就会分崩离析。

### 规则

1. **将默认色阶映射为同一明度步进下可主题化的对应色阶：** `red → coral`, `绿色`/`emerald` → `sage`, `orange → amber`, `blue → primary`. `bg-emerald-50 text-emerald-700` 变成 `bg-sage-50 text-sage-700`.
2. **没有对应项的色相，就不会再硬配一个。** `violet`, `teal`, `sky`, `cyan`, `indigo`, `pink` 和 `purple` 并不是“几乎 primary”或“几乎 sage”。不要凭空发明第五条色阶，不要以新名字复制现有色阶，也不要去使用 `--accent-lavender` 以及类似项——那些是固定的十六进制值，不是色阶。
3. **当一张表需要超过四种不同色相时，把多余的行交给该表已经定义好的中性色对** (`bg-surface-subtle text-content-secondary`，或者该表的“未知”/“其他”行所用的配色）。绝不要让两行在同一条色阶上发生冲突：两个领域若渲染得一模一样，就会破坏这张表本应编码的精确区分；这比把其中之一渲染为中性色要糟糕得多。
4. **决定哪些行保留颜色，要看读者会依据哪种区分采取行动。** 徽章几乎总会打印自己的标签，因此颜色只是帮助快速扫读的辅助，而不是信息本身。把四条色阶用在会改变人们行为的那些读取上，其余的就交给中性色。顺手把语义保持诚实： `coral` 会被读作失败，因此把普通行涂成 coral 会让常规状态看起来像坏掉了。让一条色阶保持未分配，是完全合理的结果。

树中的示例：

| 表格                                                        | 行         | 保留了色相                                                        | 原因                                                                |
| --------------------------------------------------------- | --------- | ------------------------------------------------------------ | ----------------------------------------------------------------- |
| `skills/skillIcons.tsx` `CATEGORY_META`                   | 9         | `内置` （primary）， `生产力` （sage）， `社交` （coral）， `工具与自动化` （amber） | `渠道`, `聊天` 和 `平台` 共享中性的色调 `全部` / `其他`                             |
| `skills/SkillsExplorerTab.tsx` `SOURCE_COLORS`            | 6         | `内置` （sage）， `可选` （primary）                                  | 这四个远程目录都会打印自己的名称；真正重要的区分是来源层级                                     |
| `skills/SkillsExplorerTab.tsx` `FORMAT_MAP`               | 5 行，3 种色调 | Hermes 家族（primary）、ClawHub 家族（sage）， `旧版` （amber）            | 三种色调足以容纳在上限内，因此不会丢失任何内容                                           |
| `settings/panels/EventLogPanel.tsx` `DOMAIN_BADGE_COLORS` | 11        | `工具` （primary）， `代理` （sage）， `审批` （amber）                    | 谁执行了操作，以及什么在等待人工介入。coral 保持未分配——没有域就意味着失败                         |
| `notifications/NotificationCard.tsx` 提供者徽章                | 6         | 无                                                            | 同一行中的重要性徽章已经把 coral/amber/sage 分别用于高/中/低；如果把提供者涂成 coral，就会被读成通知失败 |

### 品牌色调是另一个问题

少数几个面板使用的是第三方品牌色，而不是应用色相——Telegram 的 `#249CD8`、Discord 的 `#5865F2`、iMessage 的 `#34C759` 在 `skills/skillIcons.tsx`。把这些压平为 `bg-surface-subtle` 会把它们抹成旁边通用徽章的一部分，因此它们被刻意保留为十六进制值。要给它们一个可主题化的归宿，就意味着 **添加品牌令牌**，这属于产品决策而不是清理工作。对 NotificationCard.tsx 里的提供者徽章也是如此： `NotificationCard.tsx`：回头去找默认色阶并不是解决办法。

### 不要重涂基础组件的变体

`<Button variant="primary" className="bg-violet-500">` 就是同一个 bug 换了顶帽子：该变体已经在绘制强调色阶，而这个覆盖既把颜色锁死，又让 hover、focus 和 disabled 状态不同步。应该改为给它周围的表面重新上色，并移除这个覆盖。

## 迁移用 codemod

`scripts/theme-codemod/` 将已审计的 `light dark:` 把 Tailwind 配对折叠为语义化工具类。默认情况下它是幂等的，并以 dry-run 方式运行：

```bash
node scripts/theme-codemod/migrate.mjs            # dry-run + report
node scripts/theme-codemod/migrate.mjs --write    # apply
node scripts/theme-codemod/migrate.mjs --selftest # fixture assertions
```

它只会改写相邻配对，绝不会触碰带透明度后缀的工具类或测试文件。映射表： `scripts/theme-codemod/map.mjs`.
