DESIGN.md@skills/lunheng-article-pipeline/references · git:20261020.034c1e2 · 2026-10-20 · sha256 1c1650669e5931e5

DESIGN.md@skills/lunheng-article-pipeline/references git:20261020.034c1e2A

Immutable. This exact content never changes and is served at /api/v1/blob/1c1650669e5931e5.

# DESIGN.md — 论衡插件界面设计基准

> 版本:v19.27.0|**读者**:改 `lib/client.js`(浏览器半边)的维护者/子代理。**本文件不进任何运行期读清单**(流水线角色不读)。
> **为什么需要它**:客户端的样式约定此前**只活在代码注释里**(散在 `lib/client.js` 的 `T()` / `S` 与若干「v18.9x 修复」注解中)。注释会被重写、会被漏读;而 `lib/client.js` 是**单文件 + 零构建链**的形态,没有组件库或 lint 兜底——约定一旦失守就是**静默的视觉退化**(暗色下看不见、图表全黑、文字与底色同色)。本文件把这些约定收成**唯一真源**,并在 §9 列出机械消费者(写错会红)。
> **边界(如实声明)**:本文件**只是约定**,不是自动执行体。真正拦住越界的是 §9 的机检;本文件的作用是让「为什么这样写」在改代码之前就能查到。

## 一、适用范围

`lib/client.js` 的全部 UI(运行面板 + 开工台),以及任何后续新增的客户端半边。**不适用**于产物文档(`run/<项目>/` 下的 Markdown/SVG——那里归 `references/_shared/format-export.md` 与图件模板管)。

## 二、色彩:只用 DSH 语义 token

1. **唯一取色方式**:`T(name, fallback)` → `var(--dsw-alias-x, <浅色兜底>)`。**每个 token 必须带兜底值**(单参调用一律拒),宿主未定义时退化成旧观感,而不是变成不可读。
2. **禁颜色字面量**:除 `T()` 的兜底参数外,源码里不得出现 `#rgb/#rrggbb/#rrggbbaa`。
3. **禁主题选择器**:不得写 `@media (prefers-color-scheme: …)`、不得在样式里编码 `dark:` 分支。明暗归 `ui-theme`(由 `ui-layout` 把解析后的快照应用到文档),组件**随主题自动切换、零 JS 分支**。
4. **实测在用的 token(11 个)**:`bg-layer-1` / `bg-layer-2` / `border-l1` / `border-l2` / `brand-primary` / `label-primary` / `label-secondary` / `state-error-primary` / `state-idle-primary` / `state-success-primary` / `state-warn-primary`。token 名白名单**真源 = 本机客户端目录**(`cordis_inspect_query client Theme.listTokens`,14 个 alias + sidebar fill)——**拼错一个字母不会报错**,只会静默退化到兜底浅色,暗色主题下发白。
5. **两条踩过的坑**:
   - 未显式设色的文本**必须继承** `--dsw-alias-label-primary`(v18.93.1:暗色下题名/状态行/文件名黑到看不见)。
   - **不得用 `brand-primary` 当图表主色**(v18.91.2:本机主题里它是墨色 ⇒ 整片图变黑)。图表主系列用 `state-success-primary`,次系列用 `state-warn-primary`;浅色变体由 `color-mix(in srgb, <token> N%, transparent)` 派生(`lib/client.js` 的 9 项色板即此口径,**首尾不得同色**)。

## 三、字体与字号

| 用途 | 规格 |
|---|---|
| 正文基准 | `12.5px/1.6 "Segoe UI","Microsoft YaHei",system-ui,sans-serif` |
| 等宽(路径 / 命令 / 开场白预览) | `ui-monospace,Consolas,monospace`,11~11.5px |
| 允许的字号级 | 10.5 / 11 / 11.5 / 12 / 12.5 / 15 / 16 px(**不要新增中间值**) |
| 数字 | 一律 `fontVariantNumeric: 'tabular-nums'`(KPI / 进度值 / 表格 / 阶段带) |
| 字重 | 400(正文)/ 650(强调、标题、当前段)/ 700(KPI 数值) |

## 四、间距与圆角

- **gap 序列**:2 / 3 / 4 / 5 / 6 / 8 / 10 / 14 px。
- **内边距**:紧凑控件 `3px 7px`|中档 `6px 10px` / `8px 10px`|卡片 `8px 10px`。
- **圆角四档**:`6px`(按钮 / 输入框 / 小 chip)|`8px`(内嵌信息块 / 子卡)|`10px`(主卡片 / KPI 容器)|`999px`(胶囊:徽章 / 进度条)。
- **描边**:`0.5px solid` + `border-l1`(容器)/ `border-l2`(控件)。

## 五、组件约定

| 组件 | 口径 |
|---|---|
| 卡片 | 背景 `bg-layer-1`、描边 `border-l1`、圆角 10、`padding: 8px 10px`、`flex: 1 1 460px` |
| 小节标题 | `cardTitle`:10.5px、字距 0.06em、`label-secondary`、uppercase |
| KPI 格 | 容器 `bg-layer-2` 圆角 10;格内 `bg-layer-1`;标签 11px `label-secondary`;数值 16px/700 `label-primary` |
| 进度条 / 计量条 | 轨道 `bg-layer-2`、填充 `state-success-primary`、高 9px、圆角 999、`overflow: hidden` |
| 徽章 | `padding: 1px 7px`、圆角 999、`border-l2`、11px `label-secondary` |
| 主按钮 | 底色 `state-success-primary`、文字 `bg-layer-1`、圆角 6(**不用 brand-primary 作底**) |
| 次按钮 | `bg-layer-2` 底 + `border-l2` 描边 + `label-primary` 文字 |
| 表格 | 100% 宽、`border-collapse: collapse`、11.5px、行分隔 `0.5px border-l1` |
| 空态 | **必须给出可执行提示**(命令或下一步),不得只写「暂无数据」——「不谎称有数据」是本包纪律(v18.90.1 教训) |
| 状态行 | 三个事实各说各话、**不合并成一句好听的**(座位行 `seatLine` 是范式) |
| **图标(v19.0.4;v19.0.5 补「字形必须是路径」)** | **纯内联 SVG**(零依赖、零外链——不引第三方图标库、不联网取图):`fill/stroke: currentColor` 随主题、显示尺寸由 `size` 属性决定(缺省 16)、**装饰性图标带 `aria-hidden`**(相邻文字已表达含义时,读屏不该重复念)。两族并存,**按用途选**:① **线性稿**(侧栏 `PanelIcon`:24 网格、`fill:none`+`stroke:currentColor`、`strokeWidth` 1.7/2.1 随 active);② **圆章稿**(开工台 `WbIcon`:24 网格、圆描边 + 「论」字**轮廓路径**)。⚠️ **字形只许用路径,不得用 `<text>`/内嵌 SVG 字体**(实测:内嵌 SVG 字体现代浏览器不支持 ⇒ 退化成「宿主得装那个字体」;写死颜色在暗色主题下消失)。文字标签前的图标与文字用**容器的 `gap`** 分隔,不给图标加外边距。**字形 bbox 若来自字体制(y 轴向上),变换必须带负 y 缩放翻正并居中** |

## 六、动效

- **全库唯一动效**:`.lh-wrap > *` 的 `box-shadow` / `border-color` 过渡 `.16s ease`,hover 时描边升到 `border-l2` 并加 `0 2px 12px color-mix(…)` 阴影。
- **载体**:经 `ctx.get('styles').insert(css)` 注入**类样式**——hover 是伪类,内联样式写不了;卡片边框是内联的,故 hover 需 `!important` 覆盖。**缺席 `styles` 服务即降级**(无 hover 抬升,不影响可用性)。
- ⚠️ **注入的 CSS 必须是真调用**:v18.92.0–18.99.0 期间这条规则**从未生效**——实参被写成了字符串字面量,注入的是 `T(<token>, <兜底>)` 这段**源码文本**,浏览器整条声明丢弃(静默失效)。v19.0.0 修复并加钉。
- **禁**:自造 `@keyframes`、自写阴影常量、用动画表达状态(状态一律文字 + 颜色 token)。

## 七、响应式

- **不用 media query**(理由同色彩:断点归宿主布局)。一律 `flexWrap: 'wrap'` + `minWidth: 0` + 基准宽度(如 `flex: '1 1 460px'`),容器 `maxWidth: '100%'`、`boxSizing: 'border-box'`。
- 宽屏自适应靠 flex 基准值分裂列,不靠断点。

### 7.0 方向决定语义:**列容器里的 `flex-basis` 是高度**(v19.0.2 立;主人实测到的大段空白)

**教训(主人实测)**:「全部同意和展开高级设置之间有大段空白」——根因是**样式复用错位**:
行方向样式 `S.wbNote`(`flex: '1 1 220px'`,本意 220px 宽)被用进了**列容器** `S.wbField`
(`flexDirection: 'column'`)⇒ 同一个 `flex-basis` 在列方向被解释成**高度 220px**。

**规则**:
1. **字段列内(`wbField` / `wbFieldWide`)只许用方向无关的样式**(如 `wbFieldHint`、`wbLabel`、`wbInput`)——**一律不带 `flex`**。
2. 带 `flex: 1 1 <N>px` 的样式是**行方向专用**(`wbGrid` / `wbBar` / `wbActions` 这类 `flexWrap` 容器里用)。
3. 需要「占满一行」时,用**专用样式**(如 `wbFieldWide: flex: '1 1 100%'`)而不是临时拼。

**机械约束**:`tests/panel.test.mjs` 有**通类方向钉**——扫整棵渲染树(开工台 + 运行面板),任何列容器的子元素带 `px` 的 `flex-basis` 即红。**换一处复用同类样式照样红**,不靠人工记得。

### 7.1 槽位几何:`conversation.input.dock` **有两族几何,先选族再抄**(v19.0.1 立,v19.0.3 更正)

**教训(主人两次实测,两版各一次)**:v19.0.0「横向撑满窗口」→ v19.0.1 抄了 QueueDock 的几何 → v19.0.3「又窄了点,两端没对齐」。**根因不是数值错,是选错了几何族。**

| 族 | 谁在用 | 几何 | 视觉 |
|---|---|---|---|
| **叠层卡片** | `QueueDock` / `TodoPanel` | `width: calc(100% - 2×--dsh-composer-side-clearance - 2×--dsh-composer-dock-inset)`、`max-width: calc(--dsh-composer-card-max-width - 2×dock-inset)`、`padding: 0 dock-inset`、**负下边距**、**圆角只留上边** | **刻意**比输入框窄一圈、贴在输入框上沿(附件感) |
| **独立卡片 / 提示条** | shell 自己在输入栏里的 `uV2eYG_notice` | `width: 100%`、`max-width: var(--dsh-composer-card-max-width)` | **与输入框两端严格对齐** |

**本包的开工台属「独立卡片」族**(四边独立圆角、自带边框)⇒ 抄 `notice` 的写法。

**两条硬纪律**:
1. **外层容器已经做了侧向留白**(输入栏 root 有 `padding: 0 var(--dsh-composer-side-clearance)`)——所以 **`width: 100%` 就够了;再减一次 `side-clearance` 就是双重内缩**(v19.0.3 实测:右端短了约 32px)。
2. `max-width` 用 `var(--dsh-composer-card-max-width, 100%)`:宽窗口时靠 `margin: 0 auto` 居中、与输入框同宽;**变量缺失时 fallback 到 100%**(退化成满宽,而不是把 `calc()` 算坏)。变量的提供方是 shell(输入栏 root 定义 `--dsh-composer-card-max-width: calc(--dsh-chat-content-width + 32px)`、`-side-clearance: 16px`、`-dock-inset: 8px`)。

**判据一句话**:**要跟输入框两端对齐,就抄 shell 自己的 `notice`;要做「贴在输入框上的附件」,才抄 QueueDock。**

## 八、结构与跨版本生存

1. **`exports.inject = []`**:客户端半边**不声明**任何 inject 依赖;服务一律 `ctx.get(name)` 运行时探测 + 缺席降级。理由(实测):照抄别的插件 `inject` 清单会在缺服务的宿主上**永不执行回调**(如本机无 `sidebarRightTabs`),等于静默消失。例外:`slots` 是注册席位的最小必需面,走 `ctx.inject(['slots'], …)` 并整块 try/catch。
2. **零构建链**:手写 lazy-CJS(`window.__ModuleLoader__.load({ id, factory })`),只 `require('react')`,UI 全走 `React.createElement`。**不得新增第三方依赖**。
3. **座位如实汇报**:注册结果写进 `SEATS`,页面页脚如实列出(已注册/未注册/注册异常),**不假装有**。
4. **文案 vs 数据(两块分治,v19.0.1 明确)**:
   - **文案** → `// ══ L10N:BEGIN ══ … L10N:END ══`,经 `t('key')` 取;`zh`/`en` 键集合必须逐字相同;宿主 `locale` 缺席时回落内置字典。
   - **派生数据 / 枚举值** → `// ══ DATA:BEGIN ══ … DATA:END ══`(文类档案表的派生副本、期刊库、各字段的选项值)。它们是要**与流水线字面对齐**的机器可读值,不是文案 ⇒ **不进文案棘轮**,但**必须加逐行对账用例**(`文类档案.md` / `期刊数据库.md` 各一条)。
   - **两块之外**的**代码行**中文字面量受**棘轮**约束(只许降;v19.0.2 基线 267,口径 = 只数会被渲染的代码行——**整行注释不计**:注释渲染不出来,且注释里「两个单引号片段夹中文」会被正则误判为字面量,属假阳性,别让假阳性逼人抬基线)。
   - 判据:**这个词是给人读的,还是给程序匹配的?** 给人读的进 L10N(要能译);给程序匹配的进 DATA(要能对账)。
5. **写面纪律**:客户端**只写** `localStorage` 与「自己的输入框草稿」(`setDraft`)。**绝不自动 `submit()`**、不新增宿主 HTTP 路由、不读凭据。**写入只走席位标准通道**(`props.inputActions.setDraft`)——席位契约本身就提供它;**不得**去摸服务内部(如 `conversation.input.for(actx)`)当兜底:那不是公开接缝,会**静默腐坏**(v19.0.5 收口:主人真机回执确认标准通道可用后,删掉了旧版按猜补的第二通道;真出现「席位不给 props」时,`wb.channel.none` 会当场如实报缺,按它补即可)。
6. **一事实一处 + 观察/操作分离(v19.1.3–v19.1.6,主人裁定)**:① 进度 chips **只挂开工台标题栏**(收起即可见、**不进表单**);**不得另设独立进度条**(两处并存被主人一眼看穿);② 进度**只算既有快照字段**,缺的判据如实写「未纳入」,**不为它扩读面**;③ 不做逐 Phase 的表单、派发按钮与「续接」出口;字段标「必填/可选」**以 `wbValidate` 为准**,不靠印象;④ **「当前项目」三级判据**:**本会话绑定**(席位 `inject(sessionId)` → `localStorage`)→ 兜底按**项目内最新写入时间**(≤12 候选)→ 多项目才给极简切换器。**同一会话并行两项目无唯一当前项目**,由主人手选。

## 九、给 AI / 维护者的禁令清单(违反即 P0,机检会红)

| 禁令 | 谁在拦 |
|---|---|
| 写颜色字面量(T() 兜底除外)、写 `prefers-color-scheme` / `dark:` 分支 | `tests/panel.test.mjs`(token 白名单 + 字面量扫描) |
| 单参 `T('--token')`(无兜底) | 同上 |
| 图表主色用 `brand-primary` | 同上(`state-*` 白名单) |
| 把 `T(...)` 当字符串拼进 CSS | `tests/panel.test.mjs`(hover 真调用钉) |
| 新增中文文案写在 L10N 块外 | 同上(棘轮) |
| dock 条目自定宽度 / 撑满 100%(不照抄 composer 变量) | `tests/panel.test.mjs`(控宽契约钉) |
| zh/en 键集合不一致、空译文 | 同上(双语平衡) |
| 扩张 `exports.inject` | 同上(`deepEqual(mod.inject, [])`) |
| 新增宿主 HTTP 端点而不更新 `SECURITY.md` | `tests/panel.test.mjs`(面板路由数)+ 人工 review |
| 新增 token 名不先核对 `Theme.listTokens` | 同上(白名单集合) |
| 改本文件 `lib/client.js` 而不走授权五步 | `AGENTS.md` §文件修改操作约束 + `lib/guard.js`(工具级否决) |

## 十、变更流程

1. 改 `lib/client.js` 属**机制文件**改动 ⇒ 走「主人显式授权五步」(判据见 `references/maintainers.md` §14.1)。
2. 文案改动**同批**改 L10N 两套字典;数据表类副本(如开工台的文类表)**必须**与真源同步(钉在 `tests/panel.test.mjs`)。
3. 新增组件前先问:**这个席位存在吗**?席位目录真源 = `cordis_inspect_query(client, Slots, listSubTree)`(本机实测可用:`conversation.input.dock` / `shell.overlay` / `sidebar.footer.action` / `main` / `sidebar.panellist`)。**不要凭记忆写席位名**。
4. 样式改动后跑 `node --test tests/panel.test.mjs`(35 条,纯静态+桩渲染,秒级)。