语义化主题 Token:品牌色、深浅色与组件一致性
先描述一种熟悉的腐烂过程:第一个页面里写了'#FF6B35',第二个页面复制了它,第十个页面里有人凭记忆写成了'#FF6835'。半年后产品说"填充词的橙色微调一下",你打开全局搜索,发现这个色值有四种写法、三个变体,其中两个还藏在三元表达式里。再叠加深色模式适配——恭喜,进入还债期。
我们的解法是把"颜色"拆成三层,每层都有明确的住处和禁令,然后用脚本守住边界。
1. 三层结构:HEX 的法定住处只有 color.json
resources/{base,dark}/element/color.json ← HEX 字面量唯一合法住处 ↓ 按资源名引用 common/theme/SpeakLabThemeTokens.ets ← 语义角色 → 资源的映射(唯一映射层) ↓ 只许调 token 函数 pages / sheets / common/components ← 业务 UI,零 HEX、零裸尺寸第一层,资源文件。base/element/color.json里是全部色值,dark/element/color.json是深色模式覆盖。HEX 只允许出现在这里:
{ "name": "sl_color_transcript_filler", "value": "#FF6B35" }, { "name": "sl_color_transcript_hesitation", "value": "#FFD000" }, { "name": "sl_color_transcript_vague", "value": "#FFC107" }, { "name": "sl_color_transcript_encouragement", "value": "#45A020" }第二层,token 文件。SpeakLabThemeTokens.ets是唯一允许碰资源名的地方,把稳定资源名映射成语义角色:
export function SpeakLabTranscriptFiller(): ResourceColor { return $r('app.color.sl_color_transcript_filler'); } export function SpeakLabPageBackground(): ResourceColor { return $r('sys.color.ohos_id_color_sub_background'); }第三层,业务 UI。组件里只出现SpeakLabTranscriptFiller()这样的调用——读代码的人看到的是"填充词色"而不是一个色值,改色值时只动 color.json 一处。
这个结构的关键设计是:token 文件里没有任何状态。文件头写明No theme state, no business state——它不是主题引擎,只是一张纯映射表。深浅色切换不由它处理,而是交给资源系统本身。
2. 深浅色:能借系统的就不自己造
注意到上面两个例子的区别:$r('app.color.…')和$r('sys.color.…')。这是我们的第二条规则:结构色优先用系统 token,品牌/语义色才用自定义资源。
页面背景、卡片面、正文/次要/三级文字、分割线——这些"结构性"颜色直接映射到sys.color.ohos_id_color_*。好处是深浅色适配零成本:系统 token 跟随系统色彩模式自动解析,dark/color.json 里甚至不需要为它们写覆盖。
只有两类颜色进 app 自己的 color.json:
冻结的品牌/语义色:品牌主色
#7BB358(开口练的品牌绿)、逐字稿四色。它们在 base 和 dark 里是同一个值——填充词的橙红在深色模式下依然是那个橙红,因为它是业务语义不是界面氛围。确需自定义的结构补充:品牌色按钮前景、卡片阴影、遮罩等系统 token 覆盖不到的角色,在 base/dark 两份 color.json 里分别取值。
固定黄色系语义色带来一个真机问题:犹豫词的#FFD000在浅色页面上几乎不可读。解法不是动摇语义色,而是给它一个沉浸底色——逐字稿区域固定用近黑底#0A0A0A衬白字,语义色在两种系统模式下都保持可读:
{ "name": "sl_color_transcript_surface", "value": "#0A0A0A" }, { "name": "sl_color_transcript_on_surface", "value": "#FFFFFF" }token 注释里写明了设计意图:Immersive dark surface so fixed yellow transcript colors stay readable in light mode。语义色的可读性问题,用承载面解决,而不是修改语义色本身——这条思路在任何"彩色标注"型产品里都通用。
3. 语义隔离:四套色族,谁也不许客串谁
token 文件里其实有四个独立的色族,这是最容易被忽略、也最值得抄的设计:
色族 | 用途 | 例子 |
|---|---|---|
transcript | 逐字稿高亮(业务语义,冻结) | filler 橙红 / hesitation 黄 / vague 黄 / encouragement 绿 |
report | AI 报告批注 | highlight-soft / positive / underline / dashed |
feedback | 操作反馈状态 | info / success / warning / error(各有 soft 变体) |
structure | 页面结构 | 背景 / 卡片 / 文字 / 边框 / 阴影 |
为什么要分开?因为"绿"在不同语境下语义完全不同:逐字稿里的绿是"这句说得好,鼓励",反馈里的绿是"操作成功"。如果共用一个SpeakLabGreen(),哪天设计师调整成功色的绿,逐字稿的鼓励语义就跟着变了。
这条红线有一个真实的反例教训:词库数据里情绪词有polarity=positive字段,开发时有人顺手想用"正向情绪词"染鼓励绿。被拦下了——本地文本分析不能推导"好句子",只有 AI 返回的显式ENCOURAGEMENT标注才允许用绿色。色族的边界本质是业务语义的边界:颜色是产品语言,不是视觉糖。
4. 尺寸同理:float.json + 禁裸数字
颜色之外,尺寸走同样的三层:float.json存数值资源,token 函数按角色引用(SpeakLabSpacingM()这类),UI 里禁止.fontSize(14)、.padding(8)裸数字。理由和颜色一致——裸数字没有语义,无法统一调整,也无法审查"这里为什么比设计稿大了 2vp"。
5. 五条门禁:把规范从"共识"变成"强制"
规范写在 Wiki 里等于没有规范。scripts/check-theme-boundaries.sh用 ripgrep + python 实现了五条硬规则,每次验收必跑:
正式 ArkTS 拒绝结构性 HEX。扫描
common/ pages/ sheets/ entryability/下所有.ets,出现#RRGGBB字面量即违规(spike 目录在扫描范围之外,故意放一马)。拒绝裸数字尺寸,包括多行对象形式。
.fontSize(14)会抓,拆成多行写的{ fontSize: 14 }也抓——这条是吃过"换行绕过单行正则"的亏之后补强的。冻结色精确校验。品牌色和逐字稿四色在 base 和 dark 两份 color.json 里必须同时存在且值精确相等——防止有人"顺手优化"深色模式下的填充词颜色,把冻结语义改了。
float 尺寸资源必须齐备。token 引用的尺寸档位必须真实存在于 float.json,防止 token 引用悬空资源运行时炸。
token 映射锁定 + 语义样本锁定。token 文件必须映射到冻结资源名;另有一个专门的样本组件
SpeakLabTranscriptSemanticSample.ets把"哪种词用什么颜色、什么装饰(实色/虚线下划线)"写成活的规范,门禁校验它与 token 一致——文档会被遗忘,能被校验的代码不会。
脚本本身还有一个值得一提的细节:资源契约永远对真实仓库解析,即使SCAN_ROOT指向 mutation 测试的临时夹具树。这是为了配合 B18 会讲的变异测试——变异体会故意"破坏"源码来验证门禁能否抓住,而 color.json 这种基线数据必须锚定真仓库,否则变异测试就在跟自己玩过家家。
6. 落地清单
如果你要给自家项目建这套体系,按这个顺序来:
把全工程现有 HEX 收敛进
color.json(base + dark 两份),起稳定资源名建一个零状态的 token 文件,按语义角色命名函数,不按颜色命名(要
SpeakLabTranscriptFiller不要SpeakLabOrange)结构色能映射
sys.color.*的全映射过去,深色模式适配瞬间减半按业务语义分色族,写下来"什么才允许用这个色"(我们的鼓励绿 = 仅显式 ENCOURAGEMENT)
写门禁脚本:禁 HEX、禁裸尺寸、冻结色精确校验、资源存在性、样本锁定
把门禁挂进验收流程,让违规在本地就炸
7. 小结
三层:color.json 是 HEX 唯一住处 → token 纯映射(零状态)→ UI 只用语义函数。
结构色借系统 token,深浅色零成本;冻结语义色 base/dark 同值。
固定语义色的可读性问题用承载面解决,不改语义色。
色族按业务语义隔离;颜色是产品语言,边界即语义边界。
规范必须配门禁脚本,且门禁自身要被变异测试验证。
