# 设计语言 本文是这套主题的设计说明:**颜色是什么、为什么是这个值、落在哪个界面角色上**。想拿它当设计语言学习入口的,读这一篇就够。 工程实现细节在 [engineering-notes.md](engineering-notes.md),测试在 [testing.md](testing.md)。 --- ## 一、核心概念:工业编辑风 终末地官网的视觉不是「深色科技感」,而是**印刷编辑风**:一张有质感的纸、压在上面的墨黑字、以及一个用来做信号的高饱和色。三条规则贯穿全篇: 1. **纸与墨是主体,强调色是信号。** 强调色只用于「需要被看见的那一处」——按钮、悬停、选中行、焦点环——而不是大面积铺色。 2. **全直角。** 圆角会把界面推向「柔和产品」的观感;直角把它推向「技术文档 / 工程图纸」。只有形状本身是圆的(状态点、头像、加载圈)才保留圆形。 3. **数字等宽。** 开启 `tnum`,让表格、耗时、百分比在跳动时不产生横向抖动。 --- ## 二、色板 ### 中性色(纸 / 墨 / 线) 两套模式共用同一套语义,只换取值。 | 角色 | 令牌 | 亮色 | 暗色 | | --- | --- | --- | --- | | 纸底(应用背景、侧栏) | `--dsw-alias-bg-base` | `#e8e8e2` | `#101110` | | 面板 / 气泡 | `--dsw-alias-bg-layer-1` | `#f2f2ec` | `#181a18` | | 沉底块 | `--dsw-alias-bg-layer-2` | `#dcddd6` | `#1e201d` | | 浮层(菜单、弹层) | `--dsw-alias-bg-overlay` | `#f2f2ec` | `#1c1e1c` | | 细线 | `--dsw-alias-border-l1` | `#d8d9d5` | `#343633` | | 重线 | `--dsw-alias-border-l2` | `#b6b8b3` | `#4a4d49` | | 正文 | `--dsw-alias-label-primary` | `#101110` | `#f5f5f0` | | 次要文字 | `--dsw-alias-label-secondary` | `#4a4c48` | `#898d89` | 亮色不用纯白 `#fff`、暗色不用纯黑 `#000`:`#e8e8e2` 带一点暖灰,是「纸」而不是「屏幕」;`#101110` 留一点绿倾向,避免深灰界面常见的死黑。 ### 强调色:两套配色 设置页「主题配色」在两者间切换,**默认谷地黄**。 | 配色 | 强调色 | 相对亮度 | 说明 | | --- | --- | --- | --- | | **谷地黄**(默认) | `#fff500` | 86.6% | 终末地官网的信号黄 | | **武陵青** | `#14d0d0` | 49.8% | 青碧色 | 存储键 `dsh-theme-endfield-palette`,取值 `valley` / `wuling`。只有精确值 `wuling` 选中武陵青,未设置或异常值都回落到默认。 ### 语义状态色 不参与配色切换——错误就该是红的。 | 角色 | 亮色 | 暗色 | | --- | --- | --- | | 错误 | `#ff3b30` | `#ff6b61` | | 成功 | `#2f9e44` | `#4fbf5c` | | 警告 | `#d9822b` | `#ffb700` | --- ## 三、强调色的派生角色 一个强调色不够用。同一个色在不同表面上需要不同的取值,这些派生量在样式表里都是 CSS 变量,**声明在 `body` 上**(原因见[工程笔记](engineering-notes.md#变量必须声明在-body-而不是-root))。 | 变量 | 谷地黄 | 武陵青 | 用途 | | --- | --- | --- | --- | | `--edge-accent` | `#fff500` | `#14d0d0` | 实心底、图标、焦点环、光标 | | `--edge-accent-rgb` | `255, 245, 0` | `20, 208, 208` | 约 30 处 `rgba(var(--edge-accent-rgb), α)` 半透明色块 | | `--edge-accent-deep` | `#e8e000` | `#10b8b8` | 悬停 / 按下加深 | | `--edge-accent-onpaper` | `#d9c700` | `#14d0d0` | 亮色模式下唯一需要「作填充压在纸上」的槽位 | | `--edge-status-light` | `#6b5d00` | `#006a6a` | 亮色模式回合状态文字 | | `--edge-status-light-mid` | `#3f3600` | `#003f3f` | 亮色模式流光亮带 | | `--edge-status-dark` | `#fff500` | `#14d0d0` | 暗色模式回合状态文字 | | `--edge-status-dark-mid` | `#a08a00` | `#7ee7e7` | 暗色模式流光亮带 | | `--edge-glow-light` | `0.08` | `0.08` | hero 背景光晕透明度 | | `--edge-glow-dark` | `0.05` | `0.04` | 同上,暗色 | ### 为什么亮色模式的强调色要「下沉」 这是最容易踩的一条。**在奶油纸底上,`#fff500` 不是一种配色选择,而是一次抹除**——黄色文字压在纸底上只有 1.07:1(面板 `#f2f2ec` 上更低,1.02:1),完全不可见。同理青色也过不了:`#14d0d0` 在纸底上仅 1.56:1。 所以亮色模式里凡是「强调色作为文字」的地方,都换成同一色相的**深色档**: - 谷地黄 → `#6b5d00`(5.35:1,亮色下第一个达标的金) - 武陵青 → `#006a6a`(5.22:1) 在纸上,`#007070` 以上的青都过不了双底 AA——这是**纸的属性**,不是强调色亮度的问题,所以武陵青提亮后这两个值完全没变。 而暗色模式方向相反:底是近黑,强调色本身就够亮,直接用原色。 ### 为什么暗色的「亮带」方向不一致 回合状态标签是渐变流光文字,中间有一条亮带扫过字形。 - 谷地黄的亮带是**压暗**的 `#a08a00`——黄色向上已经到顶,再亮就成白色。 - 武陵青的亮带是**提亮**的 `#7ee7e7`——青色在这个亮度向下的余量比黄色小,更深的青(`#0a7d7d`)只有 3.54:1 不达标。 同一个规则(AA 4.5:1),两个相反的方向。这类「不能照抄」的地方是本主题最主要的复杂度来源。 ### 武陵青为什么是 `#14d0d0` 初版用了 `#0daaaa`,相对亮度只有 31.7%——不到谷地黄(86.6%)的四成,在近黑页面上存在感明显弱一截。`#14d0d0` 保持同一条色轴(R 低、G = B,仍是青碧而非灰青),亮度提高到 49.8%。 上界同样有约束:到 `#16dcdc` 时,亮色模式下色块与纸底只剩 1.39:1(现行 1.56:1),开始「融进纸里」,而且那么浅的青读起来偏白。**`#14d0d0` 是两种模式下都还站得住的最亮一档。** 这两条边界写成了断言(亮度下限 45%、上限 56%、必须留在青碧色轴上)。 --- ## 四、对比度规则 **每个角色都要对它实际落在的表面验算,不达标就构建失败。** `test/palette-contrast.test.js` 覆盖全部 27 项。 | 角色 | 谷地黄 | 武陵青 | 门槛 | | --- | --- | --- | --- | | 实心强调底 + 墨色字 | 16.50:1 | 9.88:1 | AA 4.5 | | 悬停加深底 + 墨色字 | 13.60:1 | 7.72:1 | AA 4.5 | | 暗色图标 / 链接强调色 | 15.26:1 | 9.14:1 | ≥3 | | 回合状态·亮色主色 | 5.35:1 | 5.22:1 | AA 4.5 | | 回合状态·亮色亮带 | 9.82:1 | 9.58:1 | AA 4.5 | | 回合状态·暗色主色 | 15.26:1 | 9.14:1 | AA 4.5 | | 回合状态·暗色亮带 | 5.11:1 | 12.06:1 | AA 4.5 | 三条附加规则: - **渐变文字要对两种底色都达标**(`bg-base` 与 `bg-layer-1`),因为亮带会扫过字形;而在 `prefers-reduced-motion` 下上游把 `background-size` 钉成 100%,那条亮带会**永久留在**字里。 - **两配色的等高线合成对比度相差 ≤20%**:相同 alpha ≠ 相同存在感,所以 alpha 按「合成后对比度对齐」分别调参,而不是照抄。 - **hero 光晕只许更轻,不许更响**:判据是单侧的——比它替换的 `#6187D8` @8% 更安静永远可接受。 > **装饰性表面的对比度描述的是「有多抢眼」,不是可读性。** 水印永远在正文之后,等高线在所有内容之下,它们的对比度数值用来控制干扰强度。经验下限约 `1.06:1`,低于此形同不存在。 --- ## 五、令牌映射:中和残留品牌蓝 DSH 自带 DeepSeek 品牌蓝。主题把这些令牌整组重映射,否则会在纸墨界面里留下零散的蓝点。 | 令牌组 | 处理 | | --- | --- | | `--dsw-static-deepseek-50…900` | 整组换成中性灰阶(亮色为纸阶,暗色为墨阶) | | `--dsw-static-deepseek-450` | 亮色 → `--edge-accent-onpaper`,暗色 → `--edge-accent` | | `--dsw-static-deepseek-500/600` | → 墨 / 纸色(**不可动**,见下) | | `--dsw-alias-button-info-fill` | 亮色 → 墨黑,暗色 → 强调色 | | `--dsw-alias-state-business-primary` | 同上 | | `--dsw-specific-sidebar-nav-item-active-accent` | 同上 | | `--dsw-alias-label-primary-bluish` | → 正文色 | | `--dsw-specific-bubble` / `-highlight` | → 面板 / 沉底块 | `--dsw-static-deepseek-500/200` 是**共享令牌**:同时支撑 `button-info-fill`、`state-business-primary` 与 `bubble-highlight`。要改回合状态标签的颜色时不能动它们,只能覆盖那条规则自己的 `background-image`。 ### 强调色交互一览 | 表面 | 声明 | | --- | --- | | 文本选中 | `::selection { color:#000; background: var(--edge-accent) }` | | 输入光标 | `caret-color: var(--edge-accent)` | | 焦点环 | `outline: 2px solid var(--edge-accent)`,`offset: 1px` | | 滚动条 | 轨道透明,滑块 `--edge-line`,悬停 `--edge-accent` | | 链接悬停 | 下划线着色;暗色模式下文字也变强调色 | | 表格行悬停 | 实心强调底 + 墨色字 | | 半透明色块 | `rgba(var(--edge-accent-rgb), α)`,亮色 α 略低于暗色 | 亮色与暗色的 α 不同:**在近黑底上叠加亮度,比在纸底上减去亮度显得响得多**。同一个 α 在暗色下更吵,所以暗色的半透明块普遍取更高的 α(因为强调色本身在暗色下要更突出),而装饰性图层(水印)在暗色下反而要**降低** α。 --- ## 六、直角化怎么实现 直角不是逐个组件改,而是**先全部清零,再恢复该圆的**: ```css body:not(.theme-endfield-round) [class] { border-radius: 0 !important; } body:not(.theme-endfield-round) [class*='avatar'], body:not(.theme-endfield-round) [class*='spinner'], body:not(.theme-endfield-round) [class*='dot'], body:not(.theme-endfield-round) [class$='_iconButton'] { border-radius: 50% !important; } ``` 用 `[class]` 而非 `*`,避免命中无类名的结构性节点;用 `body:not(.theme-endfield-round)` 作前缀,让「主题圆角」开关只是 `` 上加减一个 class,不需要重新注册样式表。 --- ## 七、配色切换是一个 class 整份样式表里所有强调色都读**调色板变量**,所以切换配色只是 `` 上加减一个 class:不重新注册令牌层、不重算样式、无 JS 重绘。 这一点能成立依赖一个事实:**应用是把主题令牌写成 `` 的行内样式的**(`dsh-client-ui-layout` 对每个令牌调 `body.style.setProperty`)。因此主题把 `--dsw-alias-brand-primary` 的暗色值声明为 `var(--edge-accent)`,该令牌就随 class 自动重解析。 唯一需要 JS 介入的表面是**等高线画布**——canvas 描边不能是 CSS 变量,所以它由一个监听 `` class 的 `MutationObserver` 触发重绘(顺带让「别的标签页改了配色」也能跟着变)。 配色切换后的实测像素占比(`test/verify-shots.js` 解码真实截图统计): | 截图 | 黄色像素 | 青色像素 | | --- | --- | --- | | 谷地黄 · 亮色 | **5.47%** | 0.44% | | 武陵青 · 亮色 | 0.39% | **6.51%** | | 谷地黄 · 暗色 | **2.51%** | 0.42% | | 武陵青 · 暗色 | 0.34% | **5.40%** | 约 93% 的中性像素(纸底、墨字、边框)在两版之间基本不变——切换只动强调色。 --- ## 八、可复用的几条经验 如果只带走几句话: 1. **颜色是量出来的,不是挑出来的。** 每个色值都对它实际落在的表面算过对比度;不达标的写法(哪怕看起来「就该是这个颜色」)一律改掉。 2. **同一个色在亮/暗模式下往往需要相反的处理。** 亮色下沉、暗色提亮,或者反过来,取决于该色相在那个亮度还剩多少余量。 3. **「装饰」和「前景」用两套标准。** 前景守 AA 4.5:1;装饰守「不抢眼但可见」的区间(约 1.06 ~ 1.6:1)。 4. **主题令牌自身可以是 `var()` 引用。** 这是把「换配色」从一次重绘降级成一次 class 切换的关键。 5. **底色归主题、文字归应用 = 裂缝。** 只接管背景令牌而不接管前景,会造出「白字压在黄底上」这类 1.05:1 的不可见组合。改一个背景令牌时,要一起检查谁在上面写字。