语义化主题 Token:品牌色、深浅色与组件一致性

发布时间:2026/7/24 2:19:28
语义化主题 Token:品牌色、深浅色与组件一致性 先描述一种熟悉的腐烂过程第一个页面里写了#FF6B35第二个页面复制了它第十个页面里有人凭记忆写成了#FF6835。半年后产品说填充词的橙色微调一下你打开全局搜索发现这个色值有四种写法、三个变体其中两个还藏在三元表达式里。再叠加深色模式适配——恭喜进入还债期。我们的解法是把颜色拆成三层每层都有明确的住处和禁令然后用脚本守住边界。1. 三层结构HEX 的法定住处只有 color.jsonresources/{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 绿reportAI 报告批注highlight-soft / positive / underline / dashedfeedback操作反馈状态info / success / warning / error各有 soft 变体structure页面结构背景 / 卡片 / 文字 / 边框 / 阴影为什么要分开因为绿在不同语境下语义完全不同逐字稿里的绿是这句说得好鼓励反馈里的绿是操作成功。如果共用一个SpeakLabGreen()哪天设计师调整成功色的绿逐字稿的鼓励语义就跟着变了。这条红线有一个真实的反例教训词库数据里情绪词有polaritypositive字段开发时有人顺手想用正向情绪词染鼓励绿。被拦下了——本地文本分析不能推导好句子只有 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.jsonbase dark 两份起稳定资源名建一个零状态的 token 文件按语义角色命名函数不按颜色命名要SpeakLabTranscriptFiller不要SpeakLabOrange结构色能映射sys.color.*的全映射过去深色模式适配瞬间减半按业务语义分色族写下来什么才允许用这个色我们的鼓励绿 仅显式 ENCOURAGEMENT写门禁脚本禁 HEX、禁裸尺寸、冻结色精确校验、资源存在性、样本锁定把门禁挂进验收流程让违规在本地就炸7. 小结三层color.json 是 HEX 唯一住处 → token 纯映射零状态→ UI 只用语义函数。结构色借系统 token深浅色零成本冻结语义色 base/dark 同值。固定语义色的可读性问题用承载面解决不改语义色。色族按业务语义隔离颜色是产品语言边界即语义边界。规范必须配门禁脚本且门禁自身要被变异测试验证。