# 令牌管理

[← 返回 README](../README.md) · [文档索引](./index.md)

> **状态：阶段 1 与 2 已实现（0.6.0）。** 阶段 1 = 只读清单（发现 / 来源 / 用量 / 搜索，0.5.0）；
> 阶段 2 = **接管**：每条令牌一个「接管」按钮，写进插件自己的 `tokens.css`，改值立刻生效，撤销即删。
> **材质令牌的默认值也已上线**（0.6.1）：`--dsh-material-{acrylic,frost,mica}-*` 随插件注入，位置：设置 → 通用里那一行的「更多操作 → 令牌」。
> 名字以 [`docs/naming.md` A4 节](./naming.md) 为准。
>
> 真机实测（开发实例）：这一页列出了 **523 条令牌**；点「接管」后 `tokens.css` 里出现
> `body, body[data-ds-dark-theme] { --card-border-stops: … }`，改动立刻作用到页面；点「撤销」文件被删掉。
> 跨域表读不到的会单独计数。

---

## 它是什么

一张**令牌清单 + 接管开关**。它做三件事：

1. **发现**：把当前页面上的 `--*` 令牌全部列出来 —— DSH 自己的、别的扩展定义的、本插件定义的；
   每条标明**它来自哪张样式表**、**当前解析成什么值**、**被多少条规则用到**。
2. **接管**：给任意一条写一个覆盖值，插件把它写进自己的 `tokens.css` 并以**确定的顺序**注入，
   于是「改令牌」变成一次点击，而不是去翻别人的样式表、抄选择器、赌特异性和顺序。
3. **材质参数**：云母 / 亚克力 / 毛玻璃三套材质的参数也在这里（下面的 `--dsh-material-*`），
   于是**预设**只写 `var(--dsh-material-mica-bg)` 这类引用，参数集中在一处调。

## 为什么值得做

DSH 的背景几乎全是令牌驱动的（[`examples/acrylic.css`](../examples/acrylic.css) 里量过：45 条设背景
的规则里 44 条用 `var(--dsw-…)`，`--dsw-alias-bg-base` 一个令牌被 31 条规则使用）。所以：

- **改一个令牌 ≈ 改整片界面**，比逐元素覆盖省事得多，也不会出现「同一按钮四层元素各刷一层」那种糊。
- 但**前提是知道改的是哪个令牌**。现在这条路要打开控制台、翻几百个令牌、自己试。
  这个页面就是那条路的界面化。

## 「接管」到底是什么意思

**写一份我们这个插件自己的 `tokens.css`，挂在用户自己的样式表之前、在 DSH 与其它扩展之后。**

今天的注入顺序：

```
shared-base.css → Base.module.css → CustomCssRow.module.css → host-fix.css → Wallpaper.module.css
                                                                              ↓
                                                                    【tokens.css】← 新的一格
                                                                              ↓
                                                                      用户 custom.css
```

这个位置是有意的：

| 相对谁 | 结果 |
| --- | --- |
| DSH 自己的令牌（`body` 上的 `--dsw-*`） | 我们**后**注入 → 覆盖生效 |
| 别的扩展的令牌 | 同上（它们比用户表早注入，我们仍在它们之后） |
| 用户自己的 `custom.css` | 用户表在**最后** → 用户仍然赢。接管不夺走你的最终否决权 |

三条硬约束（都是踩过的，见 [已知坑](./known-issues.md)）：

1. **必须写在 `body, body[data-ds-dark-theme]` 上，不能写 `:root`。** DSH 把令牌定义在 `body` 上；
   深色令牌定义在 `body[data-ds-dark-theme]`（特异性 `(0,1,1)`）。只写 `body` 在浅色下赢、深色下输。
2. **引用令牌的值也必须落在 `body`** —— 在 `:root` 里写 `var(--dsw-…)` 会解析成空值，然后整条声明
   被静默丢弃。
3. **只写你改过的那几条。** 把 418 个令牌全抄一份写进去，等于把 DSH 的令牌定义冻结在今天 ——
   下次换代时它们不会跟着变。这是这个功能最危险的误用方式，界面上要挡住它。

## 发现：能看见什么、看不见什么

| 来源 | 看得见吗 | 怎么读 |
| --- | --- | --- |
| DSH 自己的 `--dsw-*` | ✅ | `getComputedStyle(document.body)`（`tokens.all()` 已经在做） |
| 本插件注入的几张表 | ✅ | `<style data-plugin-css="…">` 的 `sheet.cssRules` |
| 别的扩展注入的 `<style>` / `<link>`（同源） | ✅ | 同上，逐条规则读 `rule.style` 里的自定义属性 |
| 跨域 `<link>`（例如 CDN 主题） | ❌ | `cssRules` 抛 `SecurityError` —— 跳过并计数，界面上写「N 张表读不到」 |
| `@media` / `@supports` 里的定义 | ⚠️ 部分 | 只在条件成立时才是计算值；列表里标「条件内」 |
| 挂在某个元素 `style=""` 上的令牌 | ❌ | 它不是全局令牌，接管它没有意义 |
| 关闭的 shadow root 里的令牌 | ❌ | 读不到 |

**「被多少条规则用到」** 是回答「我改这个会不会炸」的关键数字。实现：把所有可读样式表的 `cssText`
拼成一个大字符串，**一次**扫出所有 `var(--name` 的计数（不是每 token 扫一遍，那是 418 × 全表）。

## 界面

「更多操作 → 令牌」打开一页（与历史版本 / 选择器体检同一套分页版式），内容：

```
令牌
├── 头：返回 | 搜索框 | 筛选（全部 / 我改过的 / 材质 / 被 10 条以上规则用到） | 「N 张表读不到」
├── 材质区（置顶，一直显示）  --dsh-material-{acrylic,frost,mica}-*
│   └── 每行：名字 · 当前值（就地编辑）· 来源（插件默认）· 用到它的规则数
├── 列表（虚拟滚动或分页，按名字排序）
│   └── 每行：令牌名 · 当前解析值 · 来源表 · 规则数 · 「接管」开关 · ↺ 重置
└── 脚：写盘状态（与编辑器同一个状态行）+「把接管项导出成 CSS 片段」
```

每行要能看清三件事：**叫什么、现在是什么、从哪来**。少一件都会让人改错东西。

- **就地编辑**：点当前值 → 输入框（单行，接受任意 CSS 值，包括 `light-dark(a, b)`、`var()`、
  `color-mix()`）。回车提交 → 写进 `tokens.css` → 立刻生效。
- **浅色 / 深色**：**这一版是一个输入框**，值是 `light-dark(浅, 深)` 时改整串（输入框的 tooltip
  里写了这句）。拆成两个输入框（`浅色` / `深色`）排在下一步：它需要把已有值解析回两半，而这一步
  一旦解析错就会把用户的深浅两套值写坏 —— 先让接管能跑通，再动这个。
- **接管开关**：关掉 = 从 `tokens.css` 里删掉这一条（回到来源的值），不是写回原值。
- **↺ 重置**：删掉覆盖并显示来源值。
- **危险提示**：规则数 ≥ 10 的行，值旁边挂一个 `⚠ 31 条规则`（点开列出前 5 个选择器）。
  这不是拦住你，是让你知道后果。

## 材质令牌（云母 / 亚克力 / 毛玻璃）—— 已实现（0.6.1）

**默认值随插件发布**（注入在用户表之前，来源显示 `dsh-custom-css/material-tokens.css`），令牌页把它们
置顶成「材质参数（插件默认）」一组，可以直接接管覆盖。**预设（0.7.0 起）只引用它们**，不再写死数字 ——
所以调一次材质，所有用了它的规则一起变。

三套材质共用一种形状：**底色 + 模糊 + 饱和 + 描边**（云母多一个颗粒不透明度）。
插件**自带默认值**（写在插件自己的表里，来源显示为「插件默认」），预设只引用它们：

```css
:root { /* 只是示意；真实定义落在 body, body[data-ds-dark-theme] 上，见上文约束 1 */ }

--dsh-material-acrylic-bg:       light-dark(color-mix(in srgb, #ffffff 72%, transparent), color-mix(in srgb, #1c1c1e 72%, transparent));
--dsh-material-acrylic-blur:     24px;
--dsh-material-acrylic-saturate: 150%;
--dsh-material-acrylic-border:   var(--dsw-alias-border-l2);

--dsh-material-frost-bg:         light-dark(…58%…, …58%…);
--dsh-material-frost-blur:       36px;
--dsh-material-frost-saturate:   120%;

--dsh-material-mica-bg:          light-dark(…86%…, …86%…);
--dsh-material-mica-blur:        18px;
--dsh-material-mica-saturate:    115%;
--dsh-material-mica-grain:       0.035;   /* 0 = 关掉颗粒 */
```

于是**预设**写的是：

```css
background-color: var(--dsh-material-mica-bg);
backdrop-filter: blur(var(--dsh-material-mica-blur)) saturate(var(--dsh-material-mica-saturate));
border-color: var(--dsw-alias-border-l1);
```

好处：**调一次材质 → 所有用了它的规则一起变**；而且参数集中在一个页面里，不用去每条规则里找数字。
坏处要写清楚：`var()` 没有默认值时，令牌缺失会让**整条声明被丢弃**（不报错）。所以插件必须
**自带默认值**，不能等用户先填。

## 边界情况

| 情况 | 怎么办 |
| --- | --- |
| 令牌值是半输入状态 | 不写盘；行标红并说明「这个值 CSS 解析不了」（用 `CSS.supports` 判关键属性，与格式校验同一口径） |
| 改的是一个被 400 条规则用到的令牌 | 允许，但先提示；写完之后页面可能整个变样 —— 那是它本来的作用 |
| 用户自己也写了同一条令牌 | 用户表在最后 → 用户赢。界面上在那一行标「被你自己的表覆盖了」 |
| 扩展被卸载了，覆盖还留着 | 覆盖留着不起作用（令牌已无来源），但界面仍显示它 —— 不要静默删，用户可能正在迁移 |
| 深色 / 浅色只有一个值 | 允许（就当两种主题同值），并在那一行写「两种主题同值」 |

## 分阶段

| 阶段 | 内容 |
| --- | --- |
| **1** ✅ | 只读清单：发现 + 来源 + 规则数 + 搜索筛选（没有写盘，先把「看得到」做出来）—— **0.5.0 已实现** |
| **2** ✅ | 接管：写 `tokens.css`、接管/撤销、就地改值（**0.6.0 已实现**）；浅色/深色两栏与危险提示是 2b |
| **3** ◐ | 材质令牌默认值 + 令牌页置顶 —— **0.6.1 已实现**；预设（那几个按钮）是下一版 |
| **4** | 导出成 CSS 片段（「把接管项变成我自己的表」——退出接管时的出口） |

## 相关

- [预设](./presets.md) —— 材质效果的写入方，引用这里的 `--dsh-material-*`
- [预览块](./preview.md) —— 改完在哪儿看
- [设计一致性](./design-tokens.md) —— 令牌阶梯
- [已知坑](./known-issues.md) —— 「`body` 不是 `:root`」等两条踩过的坑
- [`examples/acrylic.css`](../examples/acrylic.css) —— `--dsh-acrylic-*` 的既有做法
