Tinycast 设计令牌系统:ramp 与 adaptive 两个助手函数搞定深色浅色双主题
【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast
Tinycast 是一款小巧、全原生(fully native)的 macOS 启动器、热键与剪贴板历史工具。它的所有界面颜色都来自一套**设计令牌(Design Tokens)**系统,而撑起深色/浅色双主题的,其实只有两个助手函数:ramp和adaptive。这篇文章带你快速看懂这套令牌系统如何用最少的代码,让整款应用在深色和浅色外观下自动换色、零闪烁、零手工维护。
一、设计令牌:把"魔法数字"关进一个文件
在大型项目里最头疼的问题之一是:颜色、间距、圆角散落在全网代码里,想统一换肤就要满世界找。Tinycast 的解法很直接——所有视觉常量集中在一个枚举里:Theme.swift。
Theme就是唯一的"设计令牌来源"(single design-token source),内部按维度分组:
| 令牌组 | 管什么 | 例子 |
|---|---|---|
Theme.Spacing | 间距刻度 | xxs 2·md 8·xxl 20 |
Theme.Radius | 圆角 | 面板26、行10、对话框20 |
Theme.Size | 尺寸 | 面板宽750、底部栏高52 |
Theme.Duration | 动画时长 | 面板进入0.18s、退出0.12s |
Theme.Typography | 字体 | 只用语义字体样式,支持动态字体 |
Theme.Colors | 颜色 | 本文主角:ramp与adaptive |
项目规则也很硬气:视图里禁止硬编码任何有令牌对应的颜色、间距、圆角(详见 docs/ui.md)。想加新颜色?先加令牌,再引用——这是整套系统能保持视觉一致的关键。
二、ramp 函数:一条"透明度斜坡"搞定文字与控件换色
先看ramp的完整定义,只有几行(Theme.Colors):
static func ramp(dark: Double, light: Double) -> Color { adaptive(dark: .srgbInk(1, alpha: dark), light: .srgbInk(0, alpha: light)) }它的思路非常巧妙,一句话总结:深色主题下用"白色墨水",浅色主题下用"黑色墨水",透明度(alpha)就是颜色强度。
dark: 0.10表示深色界面里 10% 透明的白色light: 0.09表示浅色界面里 9% 透明的黑色- 墨水颜色由 srgbInk 统一构建,保证两种主题下的"同一个透明度档位"落在完全相同的像素逻辑上
于是每个令牌只需要两个透明度数字,深浅主题就自动配对了。docs/ui.md 里维护着这张核心对照表:
| 令牌 | 深色(白) | 浅色(黑) | 用途 |
|---|---|---|---|
selection | 0.10 | 0.09 | 选中行的填充 |
rowHover | 0.05 | 0.045 | 鼠标悬停(永远比选中更淡) |
textPrimary | 1.00 | 1.00 | 搜索文字、光标 |
textSecondary | 0.60 | 0.60 | 次要标签 |
border | 0.20 | 0.18 | 描边式快捷键芯片 |
cardFill | 0.05 | 0.04 | 设置/计算器卡片底色 |
为什么浅色档位的数字和深色不完全一样?因为白墨和黑墨在视觉上的"重量"不同——设计师会微调浅色值,让两个主题的观感完全一致,而代码结构一行都不用改。
三、adaptive 函数:当"简单反转"不够用时
ramp的公式是固定的"白色↔黑色反转",但有些颜色不是简单反转就能成立的。这时用更底层的adaptive(Theme.swift#L336-L340):
static func adaptive(dark: NSColor, light: NSColor) -> Color { Color(nsColor: NSColor(name: nil) { $0.isDark ? dark : light }) }它接收两个显式的颜色,并包装成一个"动态颜色"——颜色会跟随所在窗口的effectiveAppearance自动解析和重绘。典型使用场景:
panelScrim(面板压暗层):ramp 的"反向"——深色主题里用 40%黑色压暗毛玻璃,浅色主题里反而要用 55%白色提亮。同白同黑的反转在这里不成立。glassFrost(液态玻璃的霜感):两种主题都必须是白色,因为霜的作用是"把玻璃提亮",而不是"上墨"。layoutPreviewGround(窗口布局预览的底板):屏幕预览里画的"显示器"永远是深色的,所以两种主题都保持深色,用ramp反而会在浅色主题下错误地反转。
一句话判断法则:
🌗颜色是"白墨↔黑墨"的镜像关系 → 用
ramp;两个主题要各自独立选色 → 用adaptive。
四、真实代码里怎么用
令牌在视图中的引用非常克制,例如底部栏按钮 BarButton.swift 选中时填充Theme.Colors.selection,悬停时填充更淡的rowHover——选中永远压过悬停,所有列表因此看起来完全一致。
第三方扩展屏面也有自己的一套颜色,存放在 ExtensionColors.swift:它不放进Theme(第三方界面不能反过来牵动启动器界面),但复用同一个ramp机制,只是数值归功能自己管:
static let fieldFill = Theme.Colors.ramp(dark: 0.05, light: 0.04) static let checkboxStroke = Theme.Colors.ramp(dark: 0.26, light: 0.30)这种"机制共享、数值自治"的分层,正是整套设计令牌系统能长期不腐化的原因。
五、新手最容易踩的 3 个坑
- 在视图里直接写
Color.white.opacity(0.1)—— 深色主题下看起来完美,切到浅色主题就"消失"了。规则明确写着:永远走Theme.Colors.*。 - 该用
ramp的地方用了adaptive(或反过来)——ramp的价值在于"墨水自动反转 + 透明度档位语义清晰";滥用adaptive手写两个颜色,会让"档位表"失去意义。 - 改动深色主题的既有数值—— 项目的深色档位是"冻结"的设计基线(frozen baseline),浅色档位则是唯一允许自由微调的列。想改深色?那是另一个明确的任务,而不是一路顺手改。
六、小结
Theme.swift是唯一设计令牌来源,间距、圆角、尺寸、时长、字体、颜色各归其组,视图里禁止魔法数字。ramp(dark:light:)用"透明度斜坡 + 墨水自动反转"让 90% 的颜色一行声明即双主题就绪。adaptive(dark:light:)是为"非镜像"颜色保留的逃生舱:压暗层、玻璃霜感、恒暗预览底板都是它的用武之地。- 想深入了解更多规则(同心圆角、边缘溶解、液态玻璃),官方 UI 文档在 docs/ui.md,架构总览见 docs/architecture.md。
两个函数、一张档位表,就是 Tinycast 深色浅色双主题的全部秘密——简单,正是它能长期保持一致性的原因。
【免费下载链接】tinycastTinycast — a tiny, fully native macOS launcher, hotkeys, and clipboard history.项目地址: https://gitcode.com/GitHub_Trending/ti/tinycast
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考