熔铸 Smelt — Ferris × Yaru for VS CodeRust 品牌结构 × Ubuntu 26.04 Yaru,做成一套 VS Code 配色主题、图标主题与动效预设。 四套情绪(mood),每套明暗两版,共 8 个配色主题 + 4 个图标主题。所有颜色由 3 个种子色在 OKLCH 空间推导而来,并对 744 组对比度关系逐条审计通过。 安装与使用
安装后
也可以直接在 四套情绪情绪不是换皮。种子色是唯一能改变真实观感的杠杆,所以「情绪」= 用同样三个数字换一间屋子。
四套的种子色与设计依据都来自上游设计系统( 动效VS Code 的配色主题不能携带动效 —— 强度分四档,每套情绪有各自的节奏:
动效默认写入用户级设置。要让某个项目单独固定动效,打开 覆盖范围配色主题设置 981 个工作台颜色键,覆盖 VS Code 1.140 注册表的全部 997 个中的 981 个(其余 16 个是继承得到的值,见下)。包括:
关于「完整」这件事
完整性检查是双向的:
第二条曾经是「只提示不报错」,理由是主题可能超前于用户的 VS Code 版本。实测推翻了这条理由:当时 63 个未注册键全部是错的(凭记忆的名字 19 个、第三方扩展的键 1 个、照邻居家族类推的 43 个),没有一个是 VS Code 后来才加的。而容忍它们的代价是真实的 —— 只要未知键被允许,真键里的打字错误就和杜撰无法区分,主题会静默少掉一个表面。所以现在它是硬失败。 图标主题734 个扩展名 / 298 个文件名 / 298 个文件夹名 → 212 个 kind → 230 个 SVG。 种类(kind)是身份,图标是 (色调家族, 字形) 这一对。212 个 kind 落在 88 个字形上,共用同一对 (family, glyph) 的 kind 共用同一个文件 —— 这既是 230 而不是 734 的原因,也让「悬空引用」在生成器里无法写出来: id 由画的时候用的同两次查表算出。 同一个字形出现在不同家族里是刻意的,不是重复: 一段历史:「家族」曾经是谎话调色板声明了十个家族,但只有六个真的发运。 后果是 而没有任何检查失败:图标定义的总数看着合理,清单里每个 id 都指向一个真实存在的文件, 现在颜色归属由 设计语言(第三版)这一版之前有两版,都被推翻,原因值得写下来。 第一版:整块实色方板 + 52% 处的深色横带 + 近白字形。 能看清,但难看:
第二版:浅色柔和底板 + 深色字形。 明度关系反过来了,字形也全部实测适配、居中、笔画统一 —— 几何上完全成立。依然不行,而且原因是这个仓库里没有任何度量看得见的:底板这个想法本身就是错的。 第三版,也就是现在:没有底板。
为什么去掉底板是关键。 Seti、Material、Symbols —— 公认做得好的图标集都是纯字形 + 透明背景。理由是资源管理器的一行本来就是「图标 + 文件名」,而眼睛该落在文件名上;260 个不透明小方块就是 260 个互相抢的小面板,读者每次扫视都要为它们付钱。颜色仍然区分家族,只是它现在长在字形上,不占面积。 「对底板解算」是第二版真正的病根。 它的字形色是解出来对浅色底板成立的,于是那些颜色偏浅、偏灰 —— 一旦画到深色侧边栏上就发白。第三版把解算基准换成侧边栏本身,同一个规则依然同时服务近黑和近白两种底色,而结果是墨色对侧边栏 5.97–7.39:1(正文底线 4.5:1)。 七个墨色,不是十个十九个是装饰。十个在去掉底板之后还是太多 —— 十个色相在一列文字里读起来是色彩抖动,而不是分组。所以按含义合并成七组,合并本身就是设计:两样读者会用同一句话描述的东西,共用一个墨色。
文件夹是实心的,文件是描边的这是层级能读出来的原因,也是上一版「文件夹和文件分不出来」的修复。 之前文件夹只是又一个描边字形,和文件同样粗细 —— 树里满是这样的一行时,分辨目录和文档要靠凑近看字形。而树里层级是第一信息。 现在文件夹是实心剪影(Yaru 的宽扁身形 + 小耳朵),文件是描边记号。这个差别在 16px 下成立,对一个不知道调色板的读者成立,而且是扫视而不是阅读时也成立。两者占用同一个 18 单位框并各自实测适配,所以展开一行时树不会跳。 同一个字形出现在不同墨色里是刻意的: 渐变只用在文件夹上,而且只往亮的方向走文件夹是这套图标里最大的一块面(约 18 × 17 单位,而文件是几条细线),所以它是渐变唯一值得花字节的地方,也是平涂唯一显得廉价的地方 —— 这么大一块纯色读起来是贴纸,一点自上而下的明暗过渡读起来是受光的表面。 实测每个情绪 230 个 SVG 里 56 个带渐变(7 families × 2 states × 4 个文件夹状态在每情绪展开),其余都是描边字形。理由很实在:一条 2.25 单位的线没有面积容纳渐变,描边上的渐变在 16px 下和纯色完全一样,只白增字节。 步长是 0.085 OKLCH 明度,而且只往亮的方向走:最暗的一端就是解算出来的墨色。这样渐变在构造上就不可能把图形压到 4.5:1 以下 —— 如果往暗的方向走,每一处下游解算都要重来。两端仍然各自被 双色调:第二层色是同一个颜色降透明度一个由多条笔画组成的字形需要层次,否则读起来是一团涂鸦:文档的轮廓和里面的文字线不是同等重要,在 16px 下用同一强度画出来就是一块糊。所以第二色确实存在 —— 但它是同一个颜色的低透明度,不是第二个色相:七色系统里再插一个双色相字形,就是第十三个不属于任何家族的颜色。 透明度每个家族各自解算,不是全局常数。实测固定 0.62 时
曲线:能保证的和保证不了的曲线的手感是这个仓库量不出来的东西。能保证的是:圆角用真圆弧、笔画圆头圆角统一、所有字形实测居中适配、粗细离散度 2.2%(16px 下 1.43–1.53 设备像素)。「这段曲线好不好看」需要眼睛,我给不了。 文件夹耳朵与本体之间那道凹圆角是个具体例子。最早用圆弧画,而弧有一个弦长约束:弦长 2.26 单位、直径 3.2 单位时,弧不会报错,它渲染成的是另一条曲线。改成控制点落在被切的那个角上的二次贝塞尔之后,它按构造就精确 —— 起于竖直切线、终于水平切线,唯一的参数是伸出多远。 一条值得单独记的结论:参数化的曲线比手写坐标更容易悄悄出错。手写的点画错了看得见;弧的参数不满足约束时,渲染器安静地给你另一条曲线。 动效:三个真实缺陷(形状层)VS Code 的颜色主题带不动效 —— 在这一层里修掉两个缺陷,都在同一个文件里,而且都不影响渲染,所以看不见:
实测:没有任何动效档位设置过这五个键 —— 它们不只是放错了位置,而是死条目,唯一可能的作用就是让将来某次编辑借动效轴去写字体。所以删掉,键表从 42 项收敛到 36 项。 六个家族的色相取自主题自己解算出的语法槽,所以一个 两个近中性是一对刻意的安排。 背景与侧边栏:VS Code 收不下渐变,所以做的是「阶梯」你的要求是背景和侧边栏用渐变色系。先说结论:VS Code 收不下渐变。 颜色键的契约是 但渐变之所以好看,原因是可以用别的办法拿到的。 一整块渐变在窗口上真正做成的事,是让各个面处在递进的明度上 —— 于是窗口读起来像从一个方向被照亮,而不是一整片平地上面画了几个矩形。而一条 40 像素宽的侧边栏上的真渐变,本来就是 40 像素的效果,没人看得见;横跨整个 chrome 的阶梯才是整个窗口。 现在的阶梯是一个方向、每级等距:
实测四级各自相差 0.018,整条跨 0.054,每个面都是不同的十六进制值。 这修掉的是两个真实缺陷缺陷一:活动栏根本不是一个区域。 之前每个面各自挑自己的步长 —— 标题栏 缺陷二:浅色主题的阶梯方向是乱的。 步长是 修的过程中还撞上第三个,值得单独记。 第一版把阶梯写成「每个面各自相对画布的偏移」,结果面板和标题栏都取 方向为什么在明暗两种模式下都是「向下」: 浅色主题的画布已经在 OKLCH 明度 0.98,而 太糊的架构性根因:图标被两次小数缩放你说「太糊了」。前面我一直在调线宽(1.46 → 2.04px)和字形几何,但那些都不是根因。根因在缩放管线里,而它是架构问题:
在 0.667 这个小数缩放之上,又叠了一次每个字形各不相同的小数缩放。 任何一个图标集的边缘落在非整数像素上都会被抗锯齿摊开 —— 而这里每一个边缘都必然落在小数像素上,是构造上就注定的。138 个字形有 138 个不同的最终缩放。 这就是为什么我调了四轮线宽和几何都还是糊:我在调一个由缩放决定的结果。 修法:让 fit scale 尽量接近 1.0把 glyph box 从 18 单位(24 的 75%) 扩到 21.4 单位(89%):
90% 的字形,变换现在几乎是恒等变换 —— 只剩下那个干净的 24→16 映射。 82% 这个数字不是我拍的:Lucide、Feather 这类成熟图标集画在 24 单位网格里、占约 20 单位 = 83%。 我照着那个标准定的 box 尺寸。 顺带把描边目标改成 2.0 设备像素整( 排版也顺便对齐了字形现在从 box 的 75% 扩到 89%,边缘留白 2.16 单位 —— 之前扩大到 22 单位时留白只剩 0.8,圆头笔画顶到 viewBox 边,看起来像被裁掉(我实测了 0 个越界,但视觉上就是"显示不全")。现在既有呼吸空间又填得满。 这一轮的教训四轮参数调整都没解决,是因为问题不在参数里。 我应该先量管线(恒定缩放 × 变量缩放 × 目标尺寸),而不是先量结果(线宽、对比度、部分像素)。上一次「不看图」让我绕了四轮色系,这一次「不量管线」让我绕了四轮锐度 —— 两件事的共同点是:我一直在优化可测的量,而不是在找决定它的那个量。 「显示不全」的根因:我的光栅器把 evenodd 当成了「填充」你说多个图标显示不全。我以为是几何超出 viewBox,先量了 —— 0 个越界。然后量到真正的问题: 43 个字形在同色实心体上又画了一层同色实心细节。 墨叠墨还是墨,所以那层"细节"根本不存在 —— 在接触表上它们就是实心块,16px 下细节完全缺席。这就是"显示不全":不是几何被裁掉,是画上去却看不见的东西。
于是做了
|
| 检查 | 结果 |
|---|---|
GLYPH_NAMES |
188,与源表条目逐一对齐 |
| 任何 kind 引用的字形 | 0 个缺失 |
| 四个情绪的 manifest 定义 | 888 个,缺失文件 0 |
| 全部审计 | 全绿 |
也就是说丢的是一个没有任何文件类型引用的字形。我记录在这里而不是悄悄带过。
教训:按字节区间改写源码是错误工具。 正确做法是按行边界或解析后改写;我后来改用 edit 工具的精确字符串替换,两次都干净。
当前状态
| 项 | 值 |
|---|---|
| 图标 SVG | 888(4 情绪 × 222),定义缺失 0 |
| 字形 | 188,与源表一致 |
| 描边 | 125 个描边字形 2.03–2.06 设备像素,展开 1.4% |
| 球 | 6 家族 / 5 色相 + 2 中性 |
dist/ |
27 文件 / 2.94 MiB |
| 包 | 1.70 MiB / 912 项 |
| 测试 | tools 25/25、repo 714/714、真窗口 8/8 |
锐化第二轮:一个规则,三个字形 —— 「特征不能比画笔还小」
上一轮把线宽从 1.46px 提到 2.04px,治好了「糊」。但加粗的笔立刻暴露出另一个问题:有些字形里的特征比笔还小,于是被笔自己填掉了。
这不是审美问题,是算术问题:一根笔画是 3.04 排版单位宽。任何两个元素靠得比约 4.5 单位更近,它们之间的缝就比两侧的墨还窄 —— 在 16px 下合成一条粗带。
database 我改了四次,第四次才有用
| 版本 | 问题 |
|---|---|
| 三条隔板,间隔 4.5 | 缝比墨窄 → 合成一条带 |
| 两条隔板,间隔 5 | 同上,只是少一条带 |
| 把顶盖椭圆拉高 | 顶盖终于可见,隔板仍然不可见 |
| 改成三张叠起来的圆盘(填充) | 读出来了 |
关键测量:database 这个字形只占 11.7 单位宽(它是方形盒里的高形状,所以被缩得最狠),而笔是 3.2 单位 —— 超过它自身宽度的四分之一。
圆柱需要顶盖、筒身、隔板三个特征,而在 11.7 单位里用 3.2 的笔放三个特征,没有一种排布成立。 所以这个字形不再尝试:改成三张圆盘叠起来(从侧面看数据库),每张圆盘是一块实心形状而不是"两根笔画之间的缝"。
和 palette 是同一个决定:当一个形状的身份是"一条被量出来的小缝",就给这个字形换一个形状。
angle-brackets 和 braces:删掉放不下的细节
angle-brackets:我上一轮在两个尖括号之间加了斜杠来说"闭合标签"。在 1.46px 的笔下有位置,在 3.04 的笔下三个元素合成一个实心菱块。 尖括号之间的缝是 6 单位、笔是 3.04,要塞进第三个元素就必须把三个都缩到都读不出来。删掉。 两个尖括号自己就说清了它是尖括号。braces:包在里面的两行内容原来在 x 10.5,而两条曲线扫过 x 5.5 —— 只隔 5 单位。挪到 x 11、y 8.5/15.5,每个元素周围留出 6 单位净空。
删掉一个放不下的细节,和添加一个放得下的细节,是同一个决定。
顺带修掉两个静默的陈旧引用
这两个都属于**「不报错,只是少做一点事」**那类:
render-zoom.mjs里写着style--hash—— 家族合并后那个 id 已不存在。它不报错,只打印missing: style--hash.svg然后画另外七个。一次运行看起来成功,却少画一个图标。 已改成markup--palette。compare-chroma.mjs的探测清单里还留着style家族,于是inks['style']是undefined,一路传成ink: undefined才炸。现在六个家族各两个,而且我给它的composeIcon调用加了包装,报错时说清是哪个字形、哪个颜色,而不是一句裸抛。
这两个都是我自己上一轮改家族时留下的 —— 而它们正好说明为什么"改一个共享标识符"需要工具去追,而不是靠记性。
整理后的状态
| 项 | 值 |
|---|---|
| 图标 SVG | 888(四情绪 × 222),磁盘与 manifest 逐一对齐,0 孤儿 |
| 家族 | 6(language / markup / data / media / archive / doc) |
| 色相 | 5 + 2 中性,全部取自主题自己的令牌 |
| 描边 | 126 个描边字形 2.03–2.06 设备像素,展开 1.4% |
dist/ |
27 个文件 / 2.93 MiB —— 只有能给人看的 |
| 包 | 1.70 MiB / 912 项 |
图标色系:这一轮是规划,不是调参 —— 屎黄色是我用错了地方
你说「好好规划,不会就用成熟的」。这句话点中了要害:前四轮我都在调参,没有一轮在规划。
屎黄色的真正来源
archive 家族 —— 归档、包、构建系统、shell、基础设施、CI —— 负担 220 个扩展名,是所有家族里最大的。而它的色相是 85:金黄色。
它在这个主题里唯一的同伴是 number —— 而 number 给代码里的数字字面量上色。一个在几个数字上读起来像高亮的颜色,当它变成 16px 实心方块、铺满仓库三分之一的时候,就是一片芥末黄。 这正是你两次说的东西。
所以错不是"黄色不好看",是"最大的家族配了最挑的颜色"。
规划:五个色相,每个都有理由
前四轮的七家族其实是一个彩虹 —— 七个点在色环上大致等距。而有设计的调色板不是等距,是少数几个色相 + 每个都有说得出理由的角色。
| 家族 | 色相 | 理由 |
|---|---|---|
| language | accent(39) | 主题自己的强调色 —— 品牌在代码里 |
| markup | type(300) | 标记是结构,而结构在这个主题里是紫罗兰 |
| data | function(245) | 结构化数据落在冷端 |
| media | string(150) | 资源和其他"内容"共用一色 |
| archive | 中性 | 最大的角色,所以用最安静的处理 |
| doc | 中性(偏冷) | 同上 |
六个家族、五个色相、两个中性。 而且每个色相都取自主题已经在用的令牌 —— 图标不再是"贴上去的",它们是用主题本身做的。
金色没有消失:number 仍然给编辑器里的数字字面量上色,那才是它该在的地方。它只是不再出现在每一个 Cargo.toml 上。
| 家族 | 之前 | 现在 | 色度 / 文件名色度 |
|---|---|---|---|
| language | 0.150 | 0.058 | 1.71 |
| markup | 0.130 | 0.048 | 1.41 |
| data | 0.120 | 0.044 | 1.29 |
| media | 0.110 | 0.038 | 1.11 |
| archive | 0.110 | 0.012 | 0.37 |
| doc | 0.035 | 0.016 | 0.48 |
最初 3.3–4.4 倍 → 现在 0.37–1.71 倍。 最常用的两个家族(archive、doc)比文件名还安静。
顺带:style 家族合并进 markup
六个家族对一个五色相的调色板来说多了一个。一张样式表和一份模板,对扫视一棵树的读者来说是同一类东西 —— 所以 style 并入 markup。图标总数 920 → 888。
工作区整理
你说要整理好。这一轮做了:
| 项 | 之前 | 之后 |
|---|---|---|
dist/ 文件 |
57 个 / 4.71 MiB | 25 个 / 2.89 MiB |
| 我这一轮散在 TEMP 的备份 | 4 个(fd-backup.json、forge-dark-backup.json、glyphs-backup.mjs、families-old.txt) |
0 |
| 临时会话目录 | 残留 | 0 |
| 图标 SVG | 920(四个情绪共 368 个多余的) | 888,磁盘与 manifest 逐一对齐 |
| 包 | 1.77 MiB | 1.68 MiB / 911 项 |
dist/ 现在只放"能给人看的东西":8 个主题 × 明的 1:1 树渲染 + 4 个暗色 4 倍渲染、8 张真实窗口截图、图标总表、色度对照表、audit.json、以及包本身。中间过程的截图全部删掉 —— 它们是可再生的,而结论已经写进文档。
还修了一个静默的陈旧引用:render-zoom.mjs 里写着 style--hash,那个 id 在家族合并后已经不存在。它不报错,只打印 missing: style--hash.svg 然后画另外七个 —— 于是一次运行看起来是成功的,却少画了一个图标。现在改成 markup--palette,并把这份清单的说明写成「不该再有过期名字」的理由。
git 的规则本来就是对的:dist/*.png、dist/*.html、dist/*.vsix 被忽略,dist/audit.json 和 icons/**/*.svg 被跟踪。仓库自己的 scratch-hygiene 测试 9/9 通过。
图标色系:我不该靠数字猜,该摆在一起看 —— 第四次才做对
你三次说色系不对,我三次都在下调饱和度,三次都没解决。第四次我换了方法:做一个对照表,把同一棵树在四个饱和度下并排画出来,然后看。
新增 compare-chroma.mjs —— 同一棵树、同一批文件、四列,分别是声明色度的 x0.4 / x0.6 / x0.8 / x1.0,行是读者真正会遇到的九个文件。一次就定下来了。
我此前一直搞错的事
先量了你主题自己的色度预算:
| 令牌 | 色相 | 色度 |
|---|---|---|
accentDefault |
39 | 0.166 |
danger |
27 | 0.131 |
warning |
85 | 0.130 |
success |
150 | 0.130 |
info |
245 | 0.130 |
textPrimary(文件名) |
303 | 0.034 |
canvas(背景) |
300 | 0.034 |
关键在最后两行:主题的 UI 色几乎是无彩的(0.034)。 accentDefault 确实是 0.166,但它用在一个图标、一个徽标上 —— 大面积 UI 元素本身是紫灰色的。
而我的图标从最初到最后,色度一直贴着 0.166 那一档 —— 那是强调色的强度,不是大面积的强度。七个家族各自占满一整行颜色块,用的却是单点强调的饱和度量级。这就是「不契合」的真正内容,而我三次都在「往低调一点」,从没量过参照系是什么。
对照表给出的答案
| 列 | 观感 |
|---|---|
| x0.4 | 和主题很贴,但丢掉了色相信息 —— 接近灰,家族分不出来了 |
| x0.6 | 好一些,仍然偏灰 |
| x0.8 | 颜色清楚,略响 |
| x1.0 | 最响 —— 我此前实际发运的档位附近 |
x0.4 丢信息,x1.0 太响,所以落在 x0.7:
| 家族 | 最初 | 现在(x0.7) |
|---|---|---|
| language | 0.150 | 0.064 |
| markup | 0.130 | 0.056 |
| data | 0.120 | 0.050 |
| style | 0.115 | 0.046 |
| media | 0.110 | 0.042 |
| archive | 0.110 | 0.039 |
| doc | 0.035 | 0.016 |
相对文件名的饱和度:最初 3.3–4.4 倍 → 现在 1.1–1.9 倍。 色相仍然读得出来,但没有任何一个图标比文件名更响。
这一轮我学到的方法论
前三轮我都在用一个数字回答一个观感问题,而每一次「往某个方向调一点」都无法回答「哪种才对」——
一个数字不能告诉你几个候选里哪个好看,只有把它们摆在一起看才行。 而这个构建循环现在能看图(上一轮才发现),所以做一张对照表几乎不花成本,却一次就结掉了一个我猜了三轮的问题。
从今往后遇到「配色/观感不对」这类反馈,我的第一步是做对照表,不是改参数。
缩略图那个灰块,就是滑块本身
你截图里箭头指的那块灰,我找到了确切的数字。
| 键 | 之前 | 现在 |
|---|---|---|
scrollbarSlider.background |
alpha 20/255(8%) | 8% |
minimapSlider.background |
alpha 77/255(30%) | 10% |
minimapSlider.hoverBackground |
50% | 24% |
minimapSlider.activeBackground |
70% | 40% |
它看起来像个层级关系,其实是个缺陷 —— 因为两个滑块形状根本不同。滚动条是一条 10 像素宽的条,8% 的墨在上面只是一层淡雾。而缩略图滑块是一个矩形,高度等于视口占编辑器高度的比例 —— 正常窗口下 400 像素高,文件短的时候就是整个缩略图的高度。
30% 的淡墨铺在 400×100 像素上,就是一块实心灰面板。
这正是短文件下看到的东西:四行代码没有预览可画(所以缩略图是空的),而滑块以 30% 铺满整个缩略图 —— 于是整块读成"缩略图是个灰盒子"。
所以规则不是「缩略图滑块比滚动条强一点」,而是「面积越大,透明度越低」。 12% 铺在整高矩形上,滑块既清楚是视口指示器,又清楚不是一块面板。
顺带:VS Code 自己的暗色主题也在这个量级,这就是参照。
一个我必须承认的事实:那个键我真的验证错了两次
minimap.foregroundOpacity —— 我测过两次,两次都得出"它什么都不做":一次是 #00000000 对 22%,一次是键完全不存在(用默认 #000f)对 #000000e0(88% 深色)。两次都是逐像素完全相同的输出。
而且这个键现在根本不在我发的主题里(它在 INTENTIONALLY_UNSET 里),VS Code 用的是它自己的默认值。结论是硬的:缩略图预览的浓淡不可主题化。
顺带确认了缩略图本身是工作的:换成 slots.rs 这种长文件之后,缩略图区域里能测到 type/string/number/function 的语法色。短文件里测不到,是因为四行代码没有东西可画。
图标色系:又降了一档,而且现在有层级
按你说的「太丑」再降一次:
| 家族 | 最初 | 上一轮 | 现在 | 相对文字饱和度 |
|---|---|---|---|---|
| language(源码) | 0.150 | 0.115 | 0.092 | 2.7× |
| markup | 0.130 | 0.100 | 0.080 | 2.4× |
| data | 0.120 | 0.090 | 0.072 | 2.1× |
| style | 0.115 | 0.082 | 0.065 | 1.9× |
| media | 0.110 | 0.076 | 0.060 | 1.8× |
| archive | 0.110 | 0.072 | 0.056 | 1.6× |
| doc | 0.035 | 0.022 | 0.018 | 0.5× |
最初是 3.3–4.4 倍,现在是 0.5–2.7 倍,而且这个范围本身就是层级 —— 源码最响,配置最静。
三个字形重画,其中 image 有一个真 bug
| 字形 | 问题 | 修法 | 实测 |
|---|---|---|---|
image |
两个元素什么都没画:rrect(3,5,18,14,2,'none') 把字符串 'none' 当颜色传进去了,而第四个元素又把第三个已经描过的边框重画了一遍 |
一个边框、山在框内 | 30% → 17.4% 部分像素 |
palette |
见下 | 三个叠在一起的颜料 | 39.5% → 31.7% |
hash |
两条横线只隔 6 单位,而一根线宽 3.04 单位 —— 缝隙比线还窄 | 拉开到 9 单位 | 29% → 35.1%(反而更软,但形状对了:现在读得出是井号) |
palette 我试了三次,三次失败,而第三次最有教育意义:
- 描边轮廓 + 三个 1.4 单位圆点 —— 39.5% 部分像素,全套最差。比一根线宽还小的点就是污渍,而且它们还长在一个本身就是描边的环上。
- 填充身体 + 用
circleTone画洞 —— 看不见,而且按设计就该看不见:tone 是同色降透明度,它的用途是给开放形状画背后的结构。墨叠墨还是墨。洞直接消失了,整个字形变成一坨实心。 - 填充身体 +
fill-rule="evenodd"挖洞 —— SVG 是对的,但洞只占身体面积的 3%,在 16px 下就是几个点,而一坨上的几个点还是坨。
教训不是「调色板难画」,而是:一个靠「小洞」来表明身份的形状,在 16px 下活不下来,无论用什么机制挖那个洞。 所以现在它用形状本身说话:三块叠在一起的颜料 —— 前面一块主墨,后面两块 tone(这才是 tone 真正的用途)。没有任何东西需要被减掉,也没有任何东西比一根线还细。
我能看图了 —— 这是这一轮最重要的发现
在改任何东西之前我做了一件早该做的事:测试我到底能不能看到自己产出的图。
能。 之前每一轮我都写着「构建循环没有眼睛」,然后就只靠数字判断 —— 那是错的,而且这个错误让前面几轮都在摸黑。所以从现在起:改 → 渲染 → 看 → 再改。
第一眼看到的事,数字从来没告诉我
icons-crisp.png 打开的第一秒,我看到的和「审计全绿」完全不是一回事:
- 金色那排,
archive画成了一个方框套方框 —— 我在它上面加的「两层内容」被 3.64 单位的粗线糊成了一个实心块。 - 同一个字形有两种填充版本并排出现(两个实心一个描边),因为一个 kind 落进了两个不同的字形。
- 换句话说:线宽修好了「糊」,却把「细节」变成了「拥挤」。 这两个问题是耦合的,而我一直在分开调。
这就是不看图的代价:audit-glyph-detail 告诉我 archive 的元素数从 3 涨到 4(达标),而眼睛告诉我它变得更糟了。
「色系不契合主题」:量化之后是一个很具体的东西
你两次说颜色不搭。我把每个家族的色度和它旁边的文字放在一起比:
| 之前 | 现在 | |
|---|---|---|
| 每个家族的明度 | 0.702 – 0.704 | 0.702 – 0.704 |
| 家族色度范围 | 0.110 – 0.150 | 0.022 – 0.115 |
| 家族色度 / 侧边栏文字色度 | 3.3 – 4.4 倍 | 1.5 – 3.4 倍 |
每一个图标都比它旁边的文件名饱和 3.3 到 4.4 倍,而且七个家族的明度一模一样。
那才是「不契合」的全部内容。这个调色板单独看并不丑 —— 它比内容还响。图标是行上的装饰,而那一行的信息是名字;一个比内容还抢眼的装饰,让整棵树读起来像一盘糖果。另外还有一层:七个家族明度全等、色度接近,没有主次,所以色相环表达不出任何层级。
修法是一把色度阶梯(而不是七个等量的颜色):
| 家族 | 之前 | 现在 | 定位 |
|---|---|---|---|
| language(源码) | 0.150 | 0.115 | 读者要找的 |
| markup | 0.130 | 0.100 | 要找的 |
| data | 0.120 | 0.090 | 要找的 |
| style | 0.115 | 0.082 | 扫过去的 |
| media | 0.110 | 0.076 | 扫过去的 |
| archive | 0.110 | 0.072 | 扫过去的 |
| doc | 0.035 | 0.022 | 近乎中性 |
现在这棵树有上也有下,而不是七个等号。
顺带修掉一个我自己写的度量错误
色度降下来之后,audit-palette 开始报 media 和 archive 是「同一个颜色的两个名字」,分数 0.0276,门槛 0.030。
它们色相差 66 度。 没有人会把绿色和黄色搞混。问题在公式:(色相差/180) × 平均色度 —— 两个都很淡的颜色,乘积自然小,而公式量的是它们很淡,不是它们很像。
我第一版修法是把色度下限写成 Math.max(色度, 0.055) —— 那更糟,是把下限当上限用了(0.074 被夹到 0.055),分数反而更低。正确做法是相加:(色相差/180) × (色度 + 0.055)。66 度那一对从 0.0276 涨到 0.0475,而公式的意图(色相信息不随色度一起归零)也保住了。
这一轮之后,还没解决的
archive 的「两层内容」需要重画,因为粗线把它糊掉了 —— 眼睛看到的,数字没看到。同样需要复查的还有 angle-brackets(我加的斜杠在粗线下和两个尖括号挤在一起)和 palette(圆点糊成一团)。
这两个是明确的下一步:先在 64px 下看,再在 16px 下确认,而不是只看审计。
「糊糊的」是个数字,不是感觉 —— 线宽 1.46px 的问题
你说还是糊。我上一轮加了一堆细节,却没解决糊 —— 因为糊的原因不在画什么,在线有多粗。
量化
一个字形从设计单位到设备像素要走三段:设计单位 → 拟合缩放 → 16px 的渲染。我把每一段的系数都算出来:
设计描边 2.25 单位 × 拟合缩放 0.78 = 排版后 2.19 单位
× (16px / 24 单位) = 1.46 设备像素
1.46 像素。 一个渲染器拿到 1.46px,会在两侧都画上部分覆盖,而中间任何一点都到不了满墨 —— 在 16px 上,每一个字形都不是"画出来的一根线",而是"一团柔和的灰"。没有一个字形画错了,错的是那个常数。
新增 audit-stroke-crispness.mjs,它从发出的 SVG 里读出每个字形的实际权重和拟合缩放(而不是重算一遍),算出真正会被绘制的宽度。判决:128 个描边字形,1.42 – 1.53 设备像素。
代码里那句注释其实写对了数字、下错了结论:「2.25 排版单位是 16px 下的 1.5 设备像素,这是一条线在抗锯齿下存活的底线」。1.5px 确实是存活的阈值 —— 但它不是看起来像画出来的阈值,而这两个阈值之间的差,正好就是 1.46px 和 2px 之间的差。所有在 16px 下读起来干净整洁的图标集,画得都在 2 设备像素以上 —— 这就是它们比"朴素的一像素细线"更粗的原因。
修:常数,不是 189 张画
STROKE_PLACED 2.25 → 3.08(3.0 排版单位 × 16/24 = 2.0 设备像素)。
但只改常数不够。实测展开从 2.2% 涨到 9.8%,超过了 8% 的阈值 —— 因为那个拟合定点迭代是对权重一阶收敛的,更粗的线留下按比例更大的残差:在旧权重下看不见的机制,在新权重下露出来了。
所以加了第三遍:用第二遍已经产生的几何去量它蕴含的拟合缩放,再按那个缩放重算权重、重建。实测展开回到 1.6%。
结果:
| 之前 | 之后 | |
|---|---|---|
| 设备像素 / 描边 | 1.42 – 1.53 | 2.027 – 2.060 |
| 均值 | 1.46 | 2.039 |
| 展开 | 2.2% | 1.7% |
128 个描边字形,0 个落在 2 ± 0.1 之外。
我自己写的审计错了一次,顺手修掉
第一版 audit-stroke-crispness.mjs 把 61 个纯填充字形(heart、star、pin、shield 这些实心剪影)算进了统计。它们根本没有描边,stroke-width 是 0,于是把均值拖到 1.38,并把它们报成"比 2px 细"—— 而那是个对它们不适用的概念。
一个把"被测量量根本不存在"的个体算进去的统计,不是保守,是错的。 现在纯填充字形被排除在每一条统计之外,并且明确说明为什么。
这也解释了上一轮的一个矛盾
我上一轮往字形里加细节(开槽、折盖、三层隔板),而你说还是糊。现在明白为什么了:在 1.46px 的线上,每多一个细节就多一处需要被渲染的亚像素间隙 —— 细节越多越糊。在把线宽修到位之前,加细节是做负功。
顺序错了:先修线宽,再加细节。 这一轮两个都到位了,所以现在那份接触印相表才真的能看。
图标精修:按「实际会被看到多少次」排序,而不是按「多少个 kind 用它」
你说「对部分代码图标优化和细致化,文件夹之类的简单图标先不动」。这一轮修了 11 个 —— 都是代码图标,folder-* 一族完全没动。
排序依据换了,因为原来那个是错的
之前 audit-glyph-detail.mjs 按「多少个文件 kind 用它」排序。那个数字不是一个图标被看到的频率:wrench 服务 58 个 kind,而它们大多是冷门构建系统;angle-brackets 覆盖所有 C 系源文件,那是工作日里的大部分时间。把力气花在前者身上,就是花在没人看的图标上。
所以新增 rank-glyphs.mjs,按扩展名加权(再乘一个「这个扩展名在实践中多常见」的粗略系数,系数是判断、并且标明是判断):
| 图标 | 加权扩展数 | kind 数 |
|---|---|---|
| chip | 61 | 16 |
| wrench | 59 | 10 |
| archive | 55 | 2 |
| angle-brackets | 48 | 6 |
| wrench-screwdriver | 44 | 2 |
| database | 43 | 4 |
| braces | 40 | 8 |
| terminal | 36 | 10 |
剔除 folder-* 之后,这份榜单就是这一轮的清单。
「更精细」在这套词汇里只有两个含义
一个字形只有两种方式承载更多信息,而且两种都在既有设计里:
tone元素 —— 一个读起来在主体后面的结构件(书页的折角、盒子的后缘)。这是让字形读成实体而不是平面符号的机制,文件里较好的字形本来就在用。- 主描边里更多几何 —— 只在能活过 16px 时才加,那就排除了任何细于 2 单位下限、或与邻居近于约 1.5 单位的东西。
它不是装饰。 为了纹理加一个点,在 16px 就是一块污渍;一个只是更忙的字形并不更精细。
11 处改动
| 字形 | 之前 | 之后 | 为什么 |
|---|---|---|---|
chip |
对称的尖刺方块 | 加了 pin-1 缺口 | 引脚数不变,结构本来就对;对称让它只说"硬件" |
wrench |
单条轮廓(读起来像钥匙) | 加了钳口开槽 | 头是空心的,而空心的扳手头就是一把钥匙 |
angle-brackets |
两个光板尖括号 | 中间加斜杠 | 有斜杠才是闭合标签,没有就只是"某种标记" |
terminal |
一个尖括号 + 一横 | 加窗口边框 | 原来是"浮着的提示符",不是终端 |
braces |
两条镜像曲线(像波浪) | 加两行被包裹的内容 | 中间什么都没有的花括号是一道波 |
lambda |
悬空的降部 | 加基线 | 降部原来没有落脚点;基线同时给了它边界 |
archive |
空盒子正面 | 加两层内容 | 空的正面是"容器",成排的内容才是"归档" |
cube |
两个面(像折纸) | 三个面 + 棱线 | 两个面是折起来的卡片,三个面才是实体 |
wrench-screwdriver |
螺丝刀是一个菱形 | 杆 + 卡箍 + 刀头 | 一个菱形在扳手旁边读起来是污渍 |
database |
一圈环(像木桶) | 三层隔板 | 一圈是桶,等距三层才是"一堆记录" |
box |
光板立方体 | 加顶部折盖 | 折盖才区分"正在装的箱子"和"立方体" |
实测结构量(audit-glyph-detail.mjs):wrench 1→2 元素、archive 3→4、cube 2→4 元素/17 命令/3 填充、database 3→4/16 命令、box 2→3/13 命令。
我改的时候自己犯了一个错,值得记
chip 的第一版我加了两个 0.95 单位半径的圆当"四角标记"。错了两处:
- 低于本文件自己规定的 2 单位下限 —— 一个看不见的标记不是细节,是污渍,而这正是这一轮要避免的失效模式;
- 注释写着"四个",画了两个。
改成一个 1.5 单位的 pin-1 缺口,并把这段写进注释。一个"更精细"的任务里,最容易犯的错就是画上去了但看不见。
没动的
folder-* 一族、以及所有没有进入加权榜单的图标。你说先不动,我就没动 —— 而且 folder 的形状是另一个文件在管(文件夹是实心剪影,文件是描边记号),那是一条独立的线。
缩略图:我先诊断错了,然后 A/B 证明了真相
你报的是「缩略图与可跳转区域什么都看不见」。我第一眼看到一个完全透明的值,就下了结论 —— 而结论是错的,所以这一节既写修复,也写我怎么错的。
错在哪
minimap.foregroundOpacity(缩略图里代码预览的透明度)原本被设成 #00000000 —— 完全透明。这看起来就是一个决定性的 bug:零透明度 = 什么都不画。我把它改成 22%,写了注释,算好对比度(1.14:1 → 1.78:1),准备收工。
然后我做了个 A/B:把值改回 #00000000 抓一张,再改成 22% 抓一张。
两张图的缩略图区域:都是 1986 种颜色、6.96% 强像素,完全一致。
为什么 —— 在 bundle 里搜一下就明白了:
minimap.foregroundOpacity 出现次数:1 <- 就是它自己注册的那一次
getMinimapForegroundOpacity 出现次数:2 <- 存在,但……
这个键在 VS Code 1.141 里注册了,然后没有任何东西读它。 它出现在颜色注册表里,是旧版缩略图实现的残留。往里面写任何值都到不了任何一个像素。
所以我把它从主题里删掉,并记进 INTENTIONALLY_UNSET,理由那一栏写的是 'registered-but-unused' —— 这张表里唯一一条"这个键根本没用"的记录。两张 A/B 截图留在 dist/minimap-zero.png 和 dist/minimap-new.png,谁想复核都能自己看。
这就是我该在改之前做的实验,而不是改完再验证。
真正起作用的是哪个键
minimap.background,而且问题不是"没设",是设错了对象。
它原本是 R('panel') —— 侧边栏的颜色,比编辑器更暗。于是缩略图是一块比它所描绘的代码更暗的板子,贴在右边缘,而不是"文件缩小后的样子"。实测那个边界:侧边栏 #171122 对画布 #1B1526,是一道看得见的台阶 —— 而一个开头就有台阶的缩略图,读起来不是代码的延续。
现在它取编辑器自己的颜色。实测在同一行上,画布色 #1B1526 从 x 719 一路连续到 x 1451 —— 编辑器、装订线、缩略图之间没有断点。
这里有个值得说清楚的机制:预览内容的颜色是 VS Code 从文件的语法高亮里取的,不受任何透明度设置影响。所以我改不了预览的浓淡,但能改它坐的那个面 —— 而面正是让它读起来像连续体的东西。
顺带把 minimapSlider 的三个透明度独立出来(0.30 / 0.50 / 0.70):缩略图的滑块坐在预览之上,而不是代码旁边,所以它得在一个本身有内容的面上立住自己的边界,不能沿用普通滚动条的 ALPHA.subtle。
顺便:重复键检测器抓到了我自己
改 minimap.background 的时候我新加了一条,却忘了删旧的 —— 同一个键在文件里出现两次,运行时后一个赢。find-duplicates.mjs 直接报了出来:
minimap.background
line 263: R('panel'),
line 291: t.canvas, <-- WINS
这是那个工具存在以来第一次真的抓到东西,而且是抓到了一个我自己造的、会静默生效的错误。如果它没报,这个文件里就会永远躺着一条看起来还在生效、其实已经死了的配置行 —— 正是当初写这个检测器要防的那种腐烂。
这一轮我还剩下什么没做到
「可跳转区域」我做了能做的,但没法完整验证。 总览标尺(右边缘那一条)的颜色都是不透明的(#56ACF099 等,60% 不透明度,合成后依然清晰),minimap.selectionHighlight / findMatchHighlight 也都有 45–60% 的不透明度。这些是数字上成立的。
但我没法证明它们在你的屏幕上看不见的原因已经修好 —— 因为缩略图内容的浓淡根本不可主题化,而总览标尺的标记是 VS Code 按视口动态画的,截图里很难稳定定位。所以如果你刷新后还是看不见标尺标记,请告诉我具体是哪一种标记(选区?查找结果?错误?改动?),那是 editorOverviewRuler.* 里完全不同的一族键,我可以针对那一族改。
可读性:4.5:1 是底线,不是舒适线
你截图里「优化得看不见了」这句是对的,而且我能指出它对应哪个数字。
上一轮我把九个语法槽的对比度目标定成 4.6:1 —— 4.5 的无障碍底线再加一成,好让 8 位取整不会把及格变成不及格。这个决定是正确的,而且读起来很差:实测屏幕上注释槽在四个暗色主题里是 4.62–4.72:1,合法可读,同时是窗口里最细最小的字。暗色主题一个槽都没有余量。
4.5:1 是"开始能读"的阈值,不是"不再累"的阈值,而代码是人要盯几个小时的字。这件事在这里比在别处更重要,因为语法高亮故意让某些槽退到后面 —— 所以那些安静的槽的底线,决定了整个文件是清爽还是发灰,而我把每个槽都压在最低点上面一丝,结果整个文件都发灰。
现在目标按角色分:
| 槽 | 目标 | 为什么 |
|---|---|---|
| 八个承载语义的槽(keyword/string/number/function/type/operator/tag/variable) | 7.0:1 | 正文应该舒适,不是最低合法 |
comment |
5.2:1 | 注释应该比代码安静 —— 这才是区分它的意义。但仍然高于底线,因为读不了的注释比没有注释更糟 |
实测(屏幕像素):注释 4.47 → 5.25:1,keyword 6.83 → 7.01:1。八个正文槽现在全部落在 7.00–7.01:1 —— 这不是巧合,是解算器的目标,所以明暗两种极性不会再各自漂走。
解算只动明度:彩度保留 90–101%,色相不变。浅色主题需要压得更深(0.50 → 0.44–0.46),在浅纸上读起来更实,不是更脏。
渐变过渡:四级是楼梯,七级才是渐变
你说「渐变过度不够」,这个判断也准。上一轮的阶梯是对的 —— 单调、每级都过可见阈值、每个面都是自己的十六进制 —— 但它读起来是楼梯而不是渐变,因为侧边栏和标题栏之间什么都没有:两个相邻区域差整整 0.018,中间没有过渡值,眼睛看到的是边而不是过渡。
渐变不是"值不同",是小的差值排成序列。所以台阶级距减半到 0.009,并且往两个方向扩:
| 级 | 面 | 相对编辑器 |
|---|---|---|
| 0 | 编辑器(纸面,锚点) | — |
| 1 | 标签条 | 0.009 |
| 2 | 侧边栏 / 面板 | 0.018 |
| 3 | 状态栏 | 0.027 |
| 4 | 标题栏 | 0.036 |
| 5 | 空编辑器组(已经在后退) | 0.045 |
| 6 | 活动栏(最外框) | 0.054 |
八个位置、六个过渡、一个方向,实测每一级都是不同的十六进制,步长 0.0087–0.0090。
中间那三个面不是为了让数字好看而发明的:标签条、状态栏、空编辑器组都是真实区域,此前各自和邻居共用一级,所以它们没有自己的值,它们相接的地方就是一条硬边。其中两处值得单独说:
- 标签条此前和侧边栏同值,也就是说代码和它上方的条之间是一条硬边 —— 而那是眼睛停留最多的地方。给它半级,那条边就变成了渐变的第一个过渡。
- 状态栏此前是
const statusBar = panel(别名),理由是"两者永不相接,第三种明度是没有区分对象的区分"。这个理由站得住,结论仍然是错的:别名意味着状态栏没有自己的值,永远无法参与渐变,而它确实相接的那条边(面板布局里上方的标签条)就没有过渡。多花一个数字,窗口底部就加入了渐变。
还有一件:起草时我加了第八级 deepRecess(菜单/浮层),然后把它删掉了而不是实现它 —— VS Code 窗口里没有比最外框更暗的东西,为了让数字好看而发明一个面,是设计系统开始描述一个不存在的窗口的方式。
渲染精度:参考线此前是画了,但等于没画
「渲染的精度与特效」这一条,我在注册表里逐条查了渲染相关的键(226 个),找到一处又具体又严重的问题。
十二个 editorBracketPairGuide.* 键全部是同一个颜色 —— editorWhitespace 的 8% / 12% 透明度。所以 VS Code 提供的六个色相槽全被浪费,每一层嵌套画出的是一模一样的线。实测那个颜色合成到画布上是 1.19:1:技术上画出来了,同时在屏幕上几乎不存在。
没有任何审计能看见它,因为一个透明色不是对比度违规 —— 它是没有人做过的那个选择。而且它同时丢掉了"精度"和"特效"两样东西。
现在:
- 每一层从
bracketHues取自己的色相,所以参考线的颜色和它连接的那对括号一致。这才是六个槽存在的意义:参考线强化括号嵌套,而不是在旁边当装饰。 - 高亮态与常态分别在画布上解算,所以顺序在任何情绪下都成立。
- 实测:1.01–1.37:1 → 1.66–3.92:1。
过程中还修掉一个我自己刚写进去的缺陷:第一版让每一层的透明度逐级递减(0.34, 0.328, 0.316…),理由是深嵌套不该变成一堵线墙。实测这会让第 6 层成为窗口里最淡的线(约 1.2:1)—— 把刚修掉的"近乎不可见"在最难读的深度上重新引入了一遍。深度已经由色相承载,而色相才是能扩展的线索:六个不同颜色在任何深度都可辨,六级透明度不行。所以透明度改为统一,渐隐取消。
顺带修掉一个会静默发布的配色 bug
写参考线的时候我把 alpha(hex, 0.62) 传了进去 —— "62%"最自然的写法。但那个函数收的是字节:0.62.toString(16) 是 '0.9eb851eb851eb8',于是产出的值是 #E986660.9EB851EB851EB8 这种 17 个字符的东西。不是一个颜色,也不报错,会作为"参考线什么都不画"的主题发布出去。是我打印构建结果才抓到的 —— 所以这个函数现在会归一化(≤1 当分数,>1 当字节),两个调用方都写得自然,也都产不出非颜色。
验证
validate OK — 800 组审计(新增 6 条参考线对比度)
audit-surfaces OK — 七级渐变,每级可分辨
audit-motion OK — 6/10 覆盖
audit-palette OK — 墨色 5.97–7.39:1
audit-glyphs OK — 189 居中适配
check-glyph-paints OK — 无效色值归零
tools/test 23/23 repo/test 714/714
vscode-session 8/8 PASS(含写入断言)
实时像素 注释 5.25:1、keyword 7.01:1、参考线 1.66–3.92:1
关于特效我做不到的部分,还是那句: 抗锯齿、亚像素渲染、字体合成是 VS Code 的渲染器,主题改不了 —— 主题能给的"精度"是数字层面的(参考线、括号配对、空白的处理、层次的区分),能给的"特效"是颜色和动效层面的。真正的光标拖尾、平滑滚动曲线、面板滑动,需要 webview 或者一个完全不同的载体,那是另一个产物。所以这一轮我把力气放在了可度量的那部分,并把每一项都变成了实测数字。
动效这一轮:两个缺陷都在「写设置」那条路上
动效 不是主题文件能承载的东西 —— 颜色主题的契约里没有任何时间维度,所以它是独立的一根轴(smelt.motion),四个档位 × 四个情绪,写的是 VS Code 自己的设置。这一轮把它做成了一根完整的轴,过程中撞到两个真实缺陷,两个都只有在真窗口里跑过写入路径才会暴露。
缺陷一:扩展根本写不进自己的主题
这个最严重。writeWorkbenchSettings 里写的是:
const workbench = vscode.workspace.getConfiguration('workbench');
await workbench.update('workbench.colorTheme', value, target);
getConfiguration('workbench') 返回的已经是划到那个 section 的视图,update() 收的 key 是相对于它的。所以这句要求 VS Code 写 workbench.workbench.colorTheme —— 一个没有注册的配置项,于是被直接拒绝:
Unable to write to User Settings because workbench.workbench.colorTheme
is not a registered configuration.
读取那一侧有同一个错误,所以还原点(snapshotIfNeeded)记下的全是 undefined,restorePrevious 会把"什么都没有"当成要还原的值。
它躲过了所有检查,原因有两个,都值得记下来:
- 错误被吞了 —— catch 只弹了一个通知,外面什么都看不到;
- 唯一断言写入的检查,读的是 harness 自己种进工作区的那份 settings —— 扩展写没写成,它根本不知道。
而且这个缺陷只在写入路径被真正跑起来时才现形,而此前从来没有任何东西跑过它:harness 只验证了 motionLoaded 和键的数量,也就是"预设读进来了",从来没验证过"设置写出去了"。
修法是让两个 writer 一致 —— applyMotion 本来就是对的(indexOf('.') 切出 section),它就在几行之外。现在两个都切。
缺陷二:off 档什么都不做
applyMotion 开头是:
if (level === 'off') return { written: 0, scope: 'none' };
理由听上去合理:off 就是"没什么要打开的"。但off 的预设存在的意义就是把东西关掉,而这个 return 在任何读取或写入之前就返回了,所以它关不掉任何东西。实测:off 档报告 appliedMotion: { written: 0, scope: "none" },设置文件里一个动效键都没有。
具体的受害者是无障碍开关。workbench.reduceMotion 在这一档设成 'on',意思是"不要动效"这句话连操作系统一起遵守,而不是"我知道的那四个开关关了" —— 而被这个 early return 丢掉的正是它。现在 off 走和其他档位一样的路径,写它的 5 个设置。
动效面本身:从 4/10 补到 6/10
新增 audit-motion.mjs,它读装好的编辑器而不是读自己的源码 —— 因为动效覆盖面是关于编辑器的事实,VS Code 每个版本都在加动画设置,一份覆盖了 1.140 的清单对 1.141 什么也没说。
写这个审计的当天 VS Code 自己从 1.140 升到了 1.141,而且它一次就找出问题:1.141 注册了 10 个动效相关设置,这个扩展只写了 4 个。
补上的两个:
terminal.integrated.tabs.enableAnimation—— 终端标签条的动画,默认是开的,而没有任何预设管它。一个在动、并且默认开着的设置,是用户已经在享受但主题从没表过态的动效,所以它该进预设,由情绪来决定。workbench.reduceMotion—— 无障碍开关,见上。只有off档写它,其他三档不碰,这个不对称是决定而不是疏漏:在off之上,用户已经明确要了动效,在那里写off就是为了提供动画而去覆盖一个机器级的无障碍偏好。
刻意没碰的 4 个,各有理由:chat.experimental.incrementalRendering.animationStyle(实验性)、chat.upvoteAnimation(彩带彩蛋)、terminal.integrated.textBlinking(默认关闭,没人能开)、workbench.welcomePage.preferReducedMotion(不在范围内)。
还修了两件让这类缺陷不可能再静默的事
测试缝失败必须可见。 catch 里现在会把 seamFailed + seamError 写进报告。一个失败不可见的测试缝比没有测试缝更糟 —— 它"没有成功"和"没有运行"长得一模一样。这次就是它让排查绕道去了环境变量(SMELT_AUTO_APPLY 一直是好的,缝在抛异常)。
harness 加了写入断言。 --motion <level> 现在让 harness 启用测试缝,并在跑完后读回设置文件,断言两件事:文件存在,且里面的键都是 VS Code 注册的。另一个新检查是"缝报告了成功"。
还修了一个纯粹是读取时机的问题:activation.json 一个会话里会被写多次,而 harness 一见到文件能解析就认定完事 —— 于是它读到的是第一版,缝写的那版在后面才到。现在它会等到报告是定稿的(带 appliedBySeam 或 seamFailed)才算数。
关于「动效优化」我做不到的部分,说清楚
颜色主题给不了动效。 format: "color-hex" 没有任何时间维度,而 workbench bundle 里的动画全是 VS Code 自己写死的。所以这层能做的是配置 VS Code 已有的动画,而不是加新的动画 —— 想要真正的自定义动效,需要 webview 或者一个完全不同的载体,那是另一个产物。
所以"优化动效"的诚实体现在:覆盖面(10 个里的 6 个,剩下 4 个各有正当理由)、档位语义的自洽(off 现在真的全局关闭)、以及写入路径的可靠性(缺陷一、二)。
各档位写的设置数:off 5 项、subtle 5 项、full 30 项、cinematic 31 项;四个情绪合计 33 个不同设置。
语法色:审计底色选错了,而且只在暗色下暴露
这是这一轮最有价值的发现,而且只有把真窗口拉起来、读像素才抓得到。
server/palette.mjs 把九个语法槽全部对 codeBackground 求解 —— 那是 peek view 和内联代码块的底色。但编辑器把代码画在 editor.background 上,而它更亮。于是:一个槽可以通过上游求解、在屏幕上却不及格。
实测(真窗口读像素):注释槽对编辑器底色只有 4.47(forge)、4.42(ember)、4.38(dawn)、4.45(hush)—— 四个暗色主题全部低于 4.5:1 正文底线,而它们对 peek view 底色是 4.52–4.62,一路通过。
浅色主题从来没受影响。 浅底上更深的注释是增加对比度的,所以这个错误只在一种极性下咬人 —— 这正它躲过所有检查的原因。
修在扩展层,不动上游
codeSlots 被 MiniMax 插件的整个渲染器和它 714 项测试共用,而上游的底色对那个渲染器是对的。为一个渲染器改共享推导,会移动另一个渲染器依赖的颜色。所以和 activityBarIconActive、textDim 一样,编辑器拿到自己解算的一套(roles.syntaxOnCanvas):色相彩度不动,只移明度。
- 审计也修了:九个
syntax-*对的底色从codeBackground改成canvas(代码真正落笔的地方),另加一条syntax-comment-peek断言 peek view 那一侧 —— 两个面都断言,以后不能修好一个、弄坏另一个。 - 求解留了余量:目标 4.6:1 而不是 4.5:1。求解在 OKLCH 里走,结果落成 8 位十六进制,这个取整能吃掉百分之几 —— 实测
dawn/dark正好落在 4.50,再取整一步就掉下去。一成的余量不花任何可见代价,却让底线不再是掷硬币。 - 实测验证:屏幕上注释从 4.47:1 → 4.72:1。
只有注释槽移动了 —— 另外八个在编辑器底色上本来就过线。
截图工具:换成了 PrintWindow
上一轮发现 verify-screenshot.mjs 在这个问题上不可靠,但没修。现在修了,而且这次全对:四个 chrome 颜色全部出现在截图里。
老工具(capture-window.mjs 的旧版)复制的是窗口坐标上的屏幕像素 —— 那里有什么,取决于谁在上面、合成器那一刻画了什么。都不检查,于是一帧过期或被遮挡的画面和有效画面长得一模一样。
新工具用 PrintWindow 并带 PW_RENDERFULLCONTENT(标志 2):让窗口自己把自己画进一个设备上下文,拿到的是窗口自己的像素,和它前面压着什么无关。标志 2 是必需的 —— Chromium 窗口用标志 0 只会返回一张空白帧,那正是这个脚本要避开的陷阱。
配套加了 inspect-window.mjs(按面积列出平面区域、扫描行列,看边界)和 measure-text.mjs(对指定区域量真正渲染出来的文字对比度 —— 抗锯齿意味着很少像素带着完整的文字色,所以声明的比值是上限,不是眼睛收到的值)。
还发现:没有打开文件时,你截的是 Welcome 页
默认会话不打开编辑器,VS Code 显示的是空的编辑器组,底色是 editorGroup.emptyBackground。实测那张截图里最大的区域是 #0B0615,而真正的 editor.background 只在 120 万像素里占了 49 个。从那张图得出任何关于编辑器可读性的结论,都是在说用户开机时看一秒钟的那个画面。
所以 vscode-session.mjs 加了 --open <file>。工作区里也加了 src/slots.rs / slots.ts / slots.css —— 一个文件把每个语法槽都用一遍,因为 tokenColors 是没法在构建期断言的:一条 TextMate 规则是意图,语法拿它怎么办由语法决定。
权威来源,和每个工具能证明什么
| 问题 | 权威 | 工具 |
|---|---|---|
| 主题被解析成了什么 | state.vscdb 的 colorThemeData / iconThemeData —— VS Code 自己写下的 |
vscode-session.mjs |
| 窗口长什么样 | PrintWindow 拿到的窗口自身像素 |
capture-window.mjs + inspect-window.mjs |
| 屏幕上的文字对比度是多少 | 同一张截图里的字形像素 | measure-text.mjs |
| 声明的一对颜色够不够 | 主题文件本身 | audit-pairs.mjs |
verify-screenshot.mjs 的解码器还有用,但它的头部写明了它不能证明什么 —— 它曾经给出一张「四个 chrome 颜色一个都没有」的截图,而同一个会话的数据库里四个全在。
墨色是解算的,不是选的
- 颜色从侧边栏出发向外走,直到超过 4.5:1。家族决定色相,表面决定明度。
- 对着表面解算而不是对着固定明度,是同一条规则能同时服务近黑与近白侧边栏的原因 —— 也正是第二版丢掉的东西:它对着浅色底板解算,于是那些颜色在深色侧边栏上发白。
用的是 4.5:1 正文底线,而不是 3:1 非文本底线 —— 资源管理器的一行经常只有图标和名字两个线索,而眼睛先落在图标上。按图形底线要求它是守了标准的字面、错了标准的意思。实测 7 个墨色 × 4 情绪 × 2 模式,对侧边栏 5.97–7.39:1。
此外任意两个墨色之间必须够远:audit-palette.mjs 量每一对,近到「一个颜色两个名字」就失败。阈值 0.030 是从算术推出来的,不是选的 —— 七个色相在色环上平均相隔 51°,在这个彩度下能拿到的最大值就是 0.034,底线设在它上面就是一个永远不可能通过的检查(早先有一版正是如此,它在调色板完全没问题时让 8 个主题一起失败)。
每个字形都会被测量和适配
字形画在 24 单位空间里,但不会铺满它。每个字形先量出自己的包围盒,再居中并缩放到 18 单位的框里(留 4% 边距),外加笔画的一半 —— 不然斜线的圆头会越出边界。
为什么不是统一缩放:统一缩放假设每个字形都画到同样的边界,而它们没有 —— minus 是一条 13 单位高 0 的线,star 是 16 单位的星芒,database 直接画到了 22 单位。这一版之前试过两个常数,都留下一批字形要么越界要么偏心两个单位。
文件夹剪影走同一套适配。 它的原始几何是 21.29 × 19.38,比文件的 18 × 18 大五分之一 —— 照原样画出来就是「文件夹是另一种尺寸的东西」,而不是层级。适配到同一个框之后,它和文件一样大,展开时也不会跳。
笔画粗细是在放置后的单位里规定的(2.25),每个字形除掉自己的适配比例。修之前实测是 1.62–3.02,两倍的差距 —— 一套图标里某个图标的线比邻居粗一倍,读起来就是「糙」,但说不出糙在哪。
node tools/audit-glyphs.mjs 量 189 个字形:全部在框内、居中偏移 0.00、可见笔画 2.14–2.30 单位(16px 下 1.43–1.53 设备像素)。
审计
node tools/build-catalog.mjs # 重建 catalog / motion / 审计报告
744 组关系,8 个主题,全部通过(0 失败 / 0 未解析)。报告在 dist/audit.json,扩展里用 熔铸: 查看当前情绪 WCAG 审计报告 打开。
阈值按 WCAG 2.1:正文 4.5:1,非文本图形 3:1。少数几项不是 WCAG 项,单独标注:
- 侧边栏与编辑器的分色要求 ≥ 1.035:1。这里刻意不要求 3:1 的分隔线 —— VS Code 自己的 2026 主题把这条线画在 1.14:1,要求 3:1 的硬边会把窗口画成表格。真正区分两个区域的是面色不同,所以审计的是面色。
- 表面之间的可辨度(面板/标题栏/活动栏/代码块/终端各自对画布)各有一条最低要求。这类失败是 WCAG 形状的检查看不见的:两块面若解析成同一个值,上面所有审计照样全绿,而窗口丢掉了全部结构。
审计过程中修掉的真实缺陷(每条都由报告指出,不是目视发现):
| # | 缺陷 | 症状 |
|---|---|---|
| 1 | 16 色 ANSI 直接复用状态色 | 终端底色与画布不同,浅色主题下绿/黄/蓝只有 3.3–3.9:1,git status 和 ls --color 反而最暗 |
| 2 | 悬浮窗边框按画布取步长 | 悬停面本身已被抬起,边框白得负收益:暗色 1.22:1、浅色 1.11:1,一像素线看不见 |
| 3 | 弱化文字只对画布解算 | 浅色模式下面板比画布更深,git ignored 与弱化标签掉到 4.33:1 |
| 4 | 状态栏调试/错误项复用 textOnAccent |
Hush 浅色下只剩 3.42:1 / 3.61:1 —— 最需要一眼看清的两个状态最看不清 |
| 5 | 活动栏未选中图标用 textMuted |
4.16–4.22:1。活动栏图标没有文字标签,按正文标准要求 |
| 6 | 终端光标对画布解算 | 3.0 底线差 0.03,Dawn 浅色 |
| 7 | 图标字形色候选没做色域映射 | 求解器请求 L 0.97 却发出偏暗的颜色,19 个家族全部只到 3.3–4.0:1 |
| 8 | 图标板子共用同一个明度 | 高彩度家族只有 2.6–2.8:1,改成按家族解算后全部过线 |
| 9 | 色相环没做间距约束 | language 与 markup 只差 10° 且明度彩度全同 —— 一个颜色两个名字,比只有一个家族更糟,因为图例说它们是两个 |
| 10 | 适配算出了比例却没用上 | glyph() 算对了粗细,却交给按标称粗细构建的表,于是每次都发出标称值 |
| 11 | 四个家族一个图标都没有 | 声明十个家族,实际只有六个发运;.html/.css/.sql/.sh 全是同一个橙色。没有任何检查失败 —— 见上文「十个家族曾经是谎话」 |
| 12 | 打包器把自己的上一次输出打进了包里 | 包写在 dist/,而 dist/ 在包含列表里。每重建一次就把上一个 .vsix 装进新的:2.52 → 5.09 → 7.45 MiB。安装完全正常,什么也不报 —— 一个每次构建都变大的包,只会以一个没人在看的数字报告自己 |
第 7 条值得单独说:它当时的报错信息是「the tile chroma is the thing to move」,而这句话是错的 —— 板子没问题,是求解器发了一个自己没要求的颜色。
第 10 条也值得记:两趟适配在数学上是对的,但它把结果喂回了一张用旧粗细构建的表。任何测量工具都只会告诉你「粗细不一致」,不会告诉你哪一行错了 —— 是把发出的 stroke-width 打出来才看见的。
第 11 与第 12 条是同一类:一个通过了的检查,和一个没被问的问题。第 11 条里,「每个引用都能解析」全程为真,而「每个声明的家族都被用到」从来没人问。第 12 条里,--check 也是绿的 —— 它拿构建出来的包和盘上的文件比,而两次运行都装着上一个包,所以两次一致。要断言的是入口列表的性质:包由源码树决定,不由 dist/ 里恰好躺着什么决定。
目录
vscode/extension/
├─ package.json 扩展清单:8 主题 + 4 图标主题 + 6 命令 + 6 设置项
├─ catalog.json 生成:情绪目录(主题 id、名称、路径、种子)
├─ motion.json 生成:动效预设(4 情绪 × 4 档)
├─ themes/<mood>/<mode>.json 生成:8 个配色主题,自包含
├─ icons/<mood>/ 生成:4 套图标主题 + 230 个 SVG
├─ dist/audit.json 生成:744 组对比度审计结果
├─ src/
│ ├─ extension.mjs 运行时:选情绪、成套应用、动效、审计面板、还原
│ └─ motion.mjs 读取生成的动效预设
└─ tools/
├─ build-themes.mjs 配色主题生成器
├─ build-icons.mjs 图标生成器
├─ build-catalog.mjs catalog + motion + 审计报告
├─ validate.mjs 结构、一致性、覆盖完整性
├─ find-duplicates.mjs 重复键扫描
├─ audit-glyphs.mjs 量 189 个字形的框、居中、笔画粗细
├─ audit-palette.mjs 量 7 个墨色 × 4 情绪 × 2 模式的色距与对比度
├─ audit-surfaces.mjs 量 chrome 阶梯:每一级是否是可分辨的一步
├─ audit-glyph-detail.mjs 量每个字形的结构量,按「用它的 kind 数」加权排序
├─ check-glyph-paints.mjs 每个 paint 值必须是一个颜色
├─ audit-families.mjs 每个声明的家族是否真的可达
├─ render-icons.mjs 把真正发运的 SVG 渲染成图(判断用)
├─ render-zoom.mjs 少量图标渲染到 200px(看清细节用)
├─ data/workbench-keys.json 从 VS Code 1.140 提取的 997 键注册表
├─ lib/ 色值推导、VS Code 映射、语法表、图标字形与色调
│ └─ svg-raster.mjs 真正的 SVG 路径光栅化器(见下)
└─ test/artifact.test.mjs
src/ 里没有构建步骤,是纯 ESM JavaScript(engines.vscode >= 1.85 起支持)。tools/ 只在构建期运行,运行时不需要。
构建与验证
cd vscode/extension
node tools/build-themes.mjs # 配色主题
node tools/build-icons.mjs # 图标
node tools/build-catalog.mjs # 目录、动效、审计
node tools/build-extension-icon.mjs
node tools/validate.mjs # 全部检查
node tools/audit-glyphs.mjs # 189 个字形的几何
node tools/audit-palette.mjs # 7 个墨色的色距与对比度
node tools/audit-surfaces.mjs # chrome 阶梯的每一级
node tools/audit-glyph-detail.mjs # 哪些字形画得还不够
node tools/check-glyph-paints.mjs # 每个 paint 值都是颜色
node tools/audit-families.mjs # 十个家族是否真的都有文件落到
node --test "tools/test/**/*.test.mjs"
生成器都支持 --check:不写盘,若产物与设计系统不一致则以非零码退出。CI 用这个形式。
关于「判断图标好不好看」这件事
这个仓库的构建回路里没有眼睛 —— 模型不接受图像输入。所以一个设计师用眼睛做的判断,这里是量出来的:
audit-glyphs.mjs量字形是否越界、是否居中、笔画粗细是否统一;audit-palette.mjs量任意两个家族的颜色是否近到「一个颜色两个名字」。
这两个工具各自抓到过真缺陷(README 审计表第 9、10 条),但它们不声称能判断美丑。0.020 的色距底线是下界不是认证:十个家族在色环上平均相隔 45°,在这个彩度下能拿到的最大值就是 0.025 —— 底线设在它上面就是一个永远不可能通过的检查,而这一版的第一稿正是这么写的,结果它在调色板完全没问题的情况下让全部 8 个主题一起失败。
同时渲染这条路是通的,只是不能由我来看:render-icons.mjs 用 svg-raster.mjs 把真正发运的 SVG 光栅化(不是替身形状 —— 早先那版用替身,所以它画出糊块时,无法判断是 SVG 糊还是替身糊)。跑 node tools/render-icons.mjs 就会得到一张给眼睛看的图。
svg-raster.mjs 是刻意做小的:只支持字形库用到的那部分 SVG(M/m L/l H/h V/v C/c S/s Q/q T/t A/a Z/z 加 fill/fill-rule/stroke/rect rx/circle)。遇到子集之外的元素它会报出来而不是跳过 —— 静默跳过会画出一张和文件不一致、但看着没问题的图。
真的跑一个 VS Code 来验
node tools/pack-vsix.mjs # 先打包,harness 装的是包而不是源码树
node tools/vscode-session.mjs # 一个主题,跑完自动关窗
node tools/vscode-session.mjs --all # 8 个主题逐个验
node tools/vscode-session.mjs --keep # 留着窗口自己看
node tools/inspect-session.mjs <dir> # 查看某次 --keep 留下的档案
脚本会拉起一个一次性的 VS Code:独立的 --user-data-dir 与 --extensions-dir,不动你自己的设置与已装扩展;把打包好的 .vsix 用 code --install-extension 装进去;工作区里预先写好 workbench.colorTheme / workbench.iconTheme,窗口一起来就是这套主题。
然后它验五件事:
| 检查 | 证据来源 |
|---|---|
| 扩展被激活 | 扩展写进自己日志目录的 activation.json |
| 6 个命令都注册了 | 同一个报告里的 smelt.* 列表 |
| 目录与动效预设读到了 | 4 情绪 / 8 主题 / 31 项动效设置 |
| VS Code 真的解析了配色主题 | state.vscdb 的 colorThemeData 带上本扩展的 settingsId 与 label,且有 themeTokenColors |
| VS Code 真的解析了图标主题 | 同一个库的 iconThemeData 带上 smelt-icons-* 的 settingsId,并已生成图标样式表 |
这两条是关键,也是任何读文件的测试都够不到的:主题值如果 VS Code 认不出来,它会静默保留上一个主题,不报错、不提示。只有从状态库读回已解析的记录,才能说这个主题真的生效了。
这一步抓到的三个真实缺陷
三轮里所有文件层面的检查(结构、清单一致性、744 组对比度、--check 幂等)一直是全绿的。这三个只有真跑起来才看得见:
| # | 缺陷 | 为什么文件检查看不见 |
|---|---|---|
| 1 | workbench.colorTheme 写的是显示名,不是标识符 |
VS Code 拿这个字符串去比对已注册的 id。写 label 时 8 个主题全部「贡献成功、选择器里能看见、永远不会生效」——它存储这个没人匹配的字符串并保留原主题,不报任何错。实测:写 label → 解析成 smelt-forge-dark(第一个贡献);写 id → 解析成 smelt-dawn-light |
| 2 | --extensionDevelopmentPath 与安装态行为不同 |
开发模式下 只解析第一个贡献,其余全部回退到它。所以用开发路径跑的 harness 永远不可能通过 —— 更糟的是它也不可能抓到缺陷 1,因为「回退到第一个主题」和「主题生效了」长得一模一样。改成装 .vsix 之后才两条都成立 |
| 3 | 图标主题的持久化键是 iconThemeData,不是 fileIconThemeData |
是我读错了键名。它表现成一个稳定、可信、每次都失败的检查:主题 id 在文件里找得到,于是「图标被提到过」,但那个键永远不存在,报告把责任推给 VS Code |
还有一个同类问题:harness 一开始用固定 sleep 等状态库写入,轻载时能过,--all 连跑 8 个时 8 个全挂。改成轮询后,慢机器只是慢,不再是假失败。
--all 会逐个验全部 8 个主题 —— 单跑一个的话,themes/hush/light.json 里写错一个名字或 dawn 少一份图标清单,照样会发出去。
眼睛能看的那一部分
node tools/render-icons.mjs # 真正发运的 SVG,88px,按家族分行
node tools/render-zoom.mjs # 少量图标 200px,看曲线与笔画
node tools/contact-sheet.mjs # 原始 SVG 接触印相表 (dist/icon-contact-sheet.html)
node tools/capture-window.mjs out.png --pid <pid> # 抓一个真实窗口
「16px 下还认得出来吗」是渲染后由眼睛回答的问题 —— 文件大小、路径正确性、对比度全部能在糊成一团的图标上通过。
前两个走 svg-raster.mjs,渲染的是发运的那份 SVG。早先这里有个 raster-proof.mjs,用形状谓词近似字形轮廓 —— 它的头部也老老实实写了这一点,但「老实标注」并不等于「够用」:它画出糊块的时候,没法判断是 SVG 糊还是替身糊。那个文件已经删掉了,连带只服务它的 build-proof-data.mjs 与 tools/data/icon-proof.json。
contact-sheet.mjs 渲染的也是发运的 SVG 本身,只是交给浏览器,所以完全不经过这套光栅化器。
与设计系统的关系
配色不在这里定义。tools/lib/smelt-imports.mjs 是唯一知道 server/ 在哪的文件,推导、对比度、种子全部从上游设计系统 import —— 上游 714 项测试所锁定的那套常量,就是这里发运的值。
本扩展只加了两层:
derived-roles.mjs—— 把宿主认识的 50 个 token 展开到 VS Code 需要的约 200 个角色(悬停叠层、装订线、概览标尺、16 色 ANSI、六级括号…)。全部是算术,没有一个手写字面量。凡是 token 解的底不是该角色实际落笔的底,就重新解算,并在构建日志里说明移动了多少。workbench-map.mjs—— 一张 981 行的表,不是程序。值要么是角色名,要么是字面量加一句为什么不能是角色。
已知边界
- 图标主题不做明暗自动切换。 VS Code 的图标解析拿不到工作台明暗,所以一个图标主题只能是一份清单。扩展的成套应用会挑对应的那份;手动只换配色主题时,图标不跟着变(这是 VS Code 的行为,不是遗漏)。
- 浅色图标集与深色图标集是两套文件,后缀
-light。板子明度不同,不是同一张图换个色。 - 未注册的键一个都不设置(见上)。因此极少数第三方扩展自己的颜色键(例如 Error Lens)不由本主题提供。
- 动效只写用户级(除非显式打开工作区级)。
还原清掉的是本扩展写过的键,不动你原本的值。 - 代理 / 聊天 /
modern*是 1.140 新增家族,键名最不稳定。它们全部走角色引用,所以改名会在构建期失败;但更旧的 VS Code 上这些表面不会被着色(旧版会静默忽略未知键)。
商标边界
Rust 基金会 logo 与 Rust 语言齿轮标识是注册商标,本主题不使用、不复刻、不近似。Ferris 螃蟹为 CC-BY。齿轮元素若出现,只作为 Ferris 背壳的一部分。配色出处见仓库 references/rust-brand.md 与 references/yaru-26.04.md。
许可
MIT(代码)。颜色与图标的设计出处见上。