Files
CNWei 707cf98fe0 feat(ui): 新增站点外壳组件 Brand / SearchField / Ticker
把应用里各自手写的三块收进库,风格沿用前台、行为参数化:

- Brand:logo 图片或首字圆形标记 + 名称,href 决定是链接还是纯展示,size/tone 可选
- SearchField:语义版搜索框(onChange 直接给字符串),variant 切 plain / card,
  支持 shortcut 提示与 hotkey 聚焦
- Ticker:无限滚动信息条,内部三份内容按实测宽度位移,循环不跳帧;
  speed / paused / label 可配
- 令牌补展示级字号 --su-font-size-2xl / --su-line-height-2xl(32 / 40)
- 展示页新增 #site-chrome(Brand / Ticker / SearchField),COMPONENTS.md 补契约与 changelog
2026-11-01 15:28:17 +08:00

417 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Stash UI 设计系统 · 规格总纲
> 版本 0.1 · 适用 `@stash/ui`
> 本文件定义**所有组件共同遵守的规则**。新增组件、改动组件、评审组件都以下面这些条目为准。
四份文档的分工:
| 文档 | 回答什么问题 | 什么时候看 |
| --- | --- | --- |
| **DESIGN.md**(本文) | 整体结构与硬性规则:分层、令牌、视觉、代码约定、验收标准 | 动手前看,评审时逐条对照 |
| [docs/COMPONENTS.md](docs/COMPONENTS.md) | 有哪些组件、各自的契约、缺什么组件、缺的时候该做什么 | 想知道「用哪个 / 有没有」时看 |
| [docs/ADDING-A-COMPONENT.md](docs/ADDING-A-COMPONENT.md) | 从零加一个组件的完整流程、模板、示例 | 确认要新增时照着做 |
| [docs/ALRIGHT-DECOMPOSITION.md](docs/ALRIGHT-DECOMPOSITION.md) | 参考站拆解:实测令牌、组件尺寸、交互节奏、暗色映射、保留差异 | 怀疑「这个值对不对」时查依据 |
展示站(`site/`)是这三份文档的运行实例:**文档里写的规则,展示页上必须能直接看到**。
---
## 1. 三条硬约束
违反任意一条,组件不合并。
1. **只认令牌**
组件 CSS 里的颜色、字号、行高、圆角、阴影、动效、间距,一律 `var(--su-*)`。
允许的字面量只有四类:`0`、`1px` 边框、`100%`、以及组件内声明的局部尺寸变量 `--xxx-size`(见 §5.3)。
2. **组件不认识业务**
不请求接口、不读路由、不 import 业务类型(`Category` / `entry` 之类)。
组件只接收 props,数据由页面喂给它。
3. **每个组件都有展示页**
展示页只能用真实组件渲染,不能写「演示专用版本」;新增组件必须同时加一节。
---
## 2. 分层结构
| 层 | 职责 | 现有组件 | 硬性规则 |
| --- | --- | --- | --- |
| **L1 原子** | 不可再分的小元素 | `Icon` `Badge` `Tag` `Divider` | 不负责布局,**不带外边距**,尺寸由内容或父级决定 |
| **L2 控件** | 接收输入 / 触发操作 | `Button` `IconButton` `Input` | 高度只用 `--su-control-height-*` 或 `--su-icon-button-*` |
| **L3 容器与布局** | 承载内容、排布 | `Card` `Grid` `Space` | 不假设内容形态;不写死宽度;不感知业务 |
| **L4 反馈** | 表达状态 | `Notice` `Empty` `ScrollTop` | 不阻塞、不弹层 |
| **页面级(不进库)** | 顶栏、侧栏、详情页、业务卡片 | — | 放在应用里,用 L1–L4 拼装 |
依赖方向:
```text
L4 ──→ L3 ──→ L1
│ │
└──────┴──→ L1 L2 ──→ L1
```
- 只允许「上层依赖下层」,**禁止下层 import 上层**(例如 `Card` 里不 import `Empty`)。
- 组件之间不共享 CSS。除 `Card` 这种明确的容器外,组件不假设自己处在什么容器里。
- 业务组件(分区卡片、应用条目、海报卡……)属于页面层:**用库里组件拼,不进库**。
---
## 3. 令牌(唯一的视觉来源)
完整定义见 [`src/theme/tokens.css`](src/theme/tokens.css)。前缀 `--su-` 是为了避免和宿主应用变量冲突。
所有取值都来自参考站实测(见 [docs/ALRIGHT-DECOMPOSITION.md](docs/ALRIGHT-DECOMPOSITION.md)),不是拍脑袋定的。
### 3.1 颜色
| 令牌组 | 用途 | 不要用于 |
| --- | --- | --- |
| `--su-gray-1..8` | 灰阶底座(与参考站一致) | 直接当品牌色用 |
| `--su-bg-page` / `--su-bg-card` / `--su-bg-hover` / `--su-bg-subtle` / `--su-bg-light` | 页面底、卡片底、悬浮底、浅色块(标签)、浅色块(按钮) | 做强调色 |
| `--su-bg-notice` / `--su-bg-danger-soft` | 提示条底色 | 卡片底色 |
| `--su-text-main` / `-secondary` / `-sub` / `-muted` / `-placeholder` | 文字层级(主文 → 次级 → 说明 → 占位) | 互相替代 |
| `--su-border` / `--su-border-strong` / `--su-card-border` | 默认细边 / 需要看清的分隔 / 卡片描边(几乎不可见) | 画装饰线 |
| `--su-color-primary` (+`-hover` `-soft`) | 主操作、链接、选中 | 大块背景 |
| `--su-color-danger` (+`-hover` `-soft` `-border`) | 危险、错误 | 普通强调 |
| `--su-color-warning` (+`-soft` `-text`) / `--su-color-orange-dark` `-light` | 提醒、付费角标 | 错误态 |
| `--su-color-success` (+`-soft`) | 在线、成功 | 主操作 |
| `--su-gradient-verified` / `-hot` / `-paid` / `-red` | 认证角标 / 置顶 / 付费 / 热门 | 大面积背景 |
### 3.2 间距
`--su-space-1..10` = 4 / 8 / 12 / 16 / 20 / 24 / 32 / 40。
只用这些值;需要 2px、6px 这类视觉微调时才允许字面量(且仅限内边距/圆点尺寸)。
### 3.3 字号与字重
| 令牌 | 值 | 用途 |
| --- | --- | --- |
| `--su-font-size-xs` / `--su-line-height-xs` | 12 / 16 | 角标(`Badge` / `ItemBadge`) |
| `--su-font-size-sm` / `--su-line-height-sm` | 13 / 20 | 标签、辅助文字 |
| `--su-font-size-md` / `--su-line-height-md` | 14 / 22 | 控件文字(按钮、输入、表格) |
| `--su-font-size-base` / `--su-line-height-base` | 16 / 24 | 内容正文、卡片标题(`Paragraph` / `Title level=3`) |
| `--su-font-size-lg` / `--su-line-height-lg` | 18 / 26 | 区块标题、大图标 |
| `--su-font-size-xl` / `--su-line-height-xl` | 24 / 32 | 页面标题(`Title level=1`) |
| `--su-font-size-2xl` / `--su-line-height-2xl` | 32 / 40 | 展示级装饰大字(封面占位字、引号),不参与正文排版 |
> 参考站的实际排版是 `13/20`、`14/22`、`16/24`、`18/26`、body `16/24`。
> 组件库把「控件文字」定在 14、把「内容正文」定在 16:控件更紧凑,文章更好读。
字重只有三档:`normal 400`、`bold 500`(默认「加粗」)、`strong 700`
(**只给两类**:字形符号如认证角标的 ✓,以及 12px 的 `ItemBadge` 状态角标)。
组件内**禁止**写 `font-weight: 600`。
### 3.4 圆角、阴影、控件尺寸、动效
| 令牌 | 值 | 用途 |
| --- | --- | --- |
| `--su-radius-xs` / `sm` / `md` / `lg` / `pill` | 4 / 6 / 12 / 24 / 999 | 标签 / 控件 / 卡片 / 大容器 / 胶囊 |
| `--su-shadow-card` | `0 6px 16px 0 rgb(27 26 49 / 1%)` | 卡片静止(参考站实测,极轻) |
| `--su-shadow-hover` | 同位置参数,3% | 卡片悬停 |
| `--su-shadow-pop` | `0 8px 24px rgb(27 26 49 / 12%)` | 浮层 |
| `--su-control-height-sm` / `md` | 32 / 36 | 按钮、输入框、未来的 Select |
| `--su-icon-button-xs` / `sm` / `md` | 28 / 36 / 44 | 圆形图标按钮(参考站 `.btn-icon`) |
| `--su-duration-fast` / `base` | 0.2s / 0.3s | 颜色变化 / 展开淡入 |
| `--su-ease-in-out` / `-out` / `-in` | ease-in-out / ease-out / ease-in | 默认用 `-in-out`(参考站同款) |
| `--su-skeleton-shine` | 浅色 50% 白 / 深色 8% 白 | 骨架屏扫光 |
| `--su-focus-ring` / `-offset` | `2px solid primary` / 2px | 键盘焦点环 |
### 3.5 布局与阅读
| 令牌 | 值 | 用途 |
| --- | --- | --- |
| `--su-layout-width` | 1380px | 页面容器最大宽度(Layout `contained` 与页面共用) |
| `--su-header-height` | 60px | 应用型页头高度 |
| `--su-radius-cover` | 8px | 封面卡圆角(比卡片小一档) |
| `--su-reading-line-height` | 1.9 | 长文正文行高(`Paragraph leading="relaxed"`) |
| `--su-cover-scrim` | 黑 85% → 40% → 透明 | 封面标题的压暗层 |
| `--su-shadow-card-strong` | 2% 卡片阴影 | 需要更明显层次时(如首页分区卡) |
| `--su-z-header` / `--su-z-float` | 100 / 90 | 顶栏 / 浮动元素 |
---
## 4. 视觉规则(拿不准就按这里来)
### 4.1 尺寸与间距
- 控件高度只有两档;**并排出现时必须同档**(`Input` 与 `Button size="md"` 都是 36px)。
- 组件**不带 margin**;元素间距交给 `Space` 或父容器。例外:`ScrollTop` 是浮动元素、`Divider` 自身带间距。
- 卡片内边距:`sm 12` / `md 20` / `lg 24`,不要出现 18、22 这类值。
### 4.2 颜色的用法
- 文字三档:主文 `text-main`、次级 `text-sub`、说明 `text-muted`;不要自造灰。
- **行式条目(列表行、作者行)不加底色、不加边框、不加阴影**——这是主题的核心观感,`Card` 不要套在行上。
- 卡片用「1px 细边 + 极轻阴影」,不要用重阴影或彩色描边制造层级。
### 4.3 状态矩阵
每个交互组件都要覆盖下表;展示页也要逐个状态摆出来。
| 状态 | 实现 | 备注 |
| --- | --- | --- |
| 默认 | 令牌默认值 | — |
| hover | 主色/危险按钮 `opacity: .8`;次要按钮换 `gray-3` 底;图标按钮换 `gray-2` 底 + 文字加深 | 参考站同款,不做位移、不缩放 |
| active | 与 hover 同色,不再额外变形 | 主题本身没有按压态 |
| disabled | 换浅底 + 弱文字 + `cursor: not-allowed`(不是整体降透明) | 不改变布局 |
| loading | 同色 spinner 占用图标位 + 自动 disabled + `aria-busy` | 仅 `Button` / `IconButton` |
| focus-visible | 组件自己写 `outline: var(--su-focus-ring)` + `offset` | 参考站把焦点样式删了,我们**必须**补(WCAG) |
| 空 | `Empty` | 不复用 `Notice` |
| 错误 | `Notice tone="error"` | 带 `role="alert"` |
### 4.4 动效
- 过渡时长统一用 `--su-duration-base`(0.3s)+ `--su-ease-in-out`,与参考站一致;
只有 hover 的快速反馈可以用 `--su-duration-fast`(0.2s)。
- 只做颜色 / 背景 / 边框 / 阴影 / 透明度,不做位移、缩放、旋转(加载 spinner 除外)。
- 需要 `transition` 的:`Button`、`IconButton`、`Card hoverable`、`Input`、`ScrollTop`、`Tag`(如加交互)。
- **每个带动效的组件都要有 `@media (prefers-reduced-motion: reduce)` 分支**(过渡置 none,spinner 放慢)。
### 4.5 响应式
断点只有三个档,**组件内部**处理,页面不要各写一套:
| 断点 | 谁在用 | 行为 |
| --- | --- | --- |
| `1200px` | `Layout` | 右栏(aside)从并排变成堆到内容下方 |
| `1100px` | `Layout`、`Grid` | 侧栏堆到内容上方;`Grid` 降为 2 列 |
| `820px` | 页面(如示例页) | 顶部导航收进抽屉、左侧资料栏隐藏 |
| `720px` | `Grid` | 降为 1 列 |
- 组件宽度默认 `100%`,由容器决定;不写死像素宽度(演示代码除外)。
- **不靠隐藏做适配**:`Layout` 在窄屏下把侧栏/右栏堆叠,内容一律保留;页面如果确实要隐藏(如移动端的资料栏),由页面自己决定。
- 高度用 `100dvh` 优先(`100vh` 兜底),地址栏收起时不会跳动。
- 弹层宽度必须夹住:Modal `calc(100vw - 32px)`、Popover `min(320px, calc(100vw - 32px))`、Drawer `max-width: 100vw`。
- 表格/标签栏/分段控件在窄屏横向滚动,不做压缩截断。
### 4.6 触屏
- 交互只用 `click`,不依赖 hover;`Tooltip` 的 hover 样式包在 `@media (hover: hover)` 里,触屏走 `:focus-within`。
- 可点区域:`xs` 图标按钮 28×28 是**桌面密集场景**下限;移动端布局请用 `sm(36)` / `md(44)`。
- 按钮类加 `touch-action: manipulation`,去掉双击缩放带来的点击延迟。
- 原生控件(checkbox/radio/switch)都包在 `<label>` 里,点文字也能操作。
> 现状:组件是响应式的、也能在手机上用(示例页与文档站都在 390 / 820 宽实测过),
> 但**还没有在真机上跑过**(iOS Safari 的 `dvh`、软键盘、`backdrop-filter` 需要真机复核)。
> 结论性的兼容承诺见 §10「已知坑」与 README 的「浏览器支持」。
---
## 5. 代码约定
### 5.1 文件结构
```text
src/components/
├── Xxx.tsx # 组件 + props 类型(同文件,不拆 types 目录)
└── Xxx.module.css # 只属于这个组件的样式
src/index.ts # 唯一出口:组件 + 类型(export type)
```
### 5.2 props 命名
| 类型 | 约定 | 例子 |
| --- | --- | --- |
| 语义变体 | `variant` | `primary / secondary / ghost / danger` |
| 尺寸 | `size` | `sm / md / lg` |
| 颜色语义 | `tone` | `primary / success / warning / danger / neutral` |
| 形状 | `shape` | `rounded / pill`、`circle / square` |
| 布尔 | 直接用形容词,**不加 is/has 前缀** | `block` `loading` `clearable` `wrap` |
| 透传 | 展开 `...rest` 到最外层原生元素 | 原生属性一律可用 |
| 受控 | 值由使用方持有;组件不偷偷维护业务状态 | `value` + `onChange` |
| 内部 UI 状态 | 只在确实属于组件自身时保留 | `Avatar` 的图片加载失败 |
- 事件保持原生语义:`onChange` 传事件,不要偷偷换成值;需要值由调用方 `event.target.value`。
- `className` / `style` 一律作用在**最外层**元素;要作用到内部就用具名 props(如 `inputRef`)。
- 原生 `button` 必须显式 `type`,默认 `"button"`,避免表单里误提交。
### 5.3 样式写法
- CSS Modules,类名短:`.root`、`.sm`、`.primary`、`.hoverable`;修饰类平铺,不嵌套。
- 选择器最多两层;禁止 `!important`;禁止标签选择器(伪类、`::before` 除外)。
- 组件专属尺寸(头像 24/36/48、角标 18、回到顶部 42)写成**组件内局部变量**,集中在 `.root` 或尺寸类里:
```css
.sm {
--avatar-size: 24px;
width: var(--avatar-size);
height: var(--avatar-size);
}
```
- 颜色 / 字号 / 间距永远从全局令牌取,**不要**复制一份到局部变量。
### 5.4 无障碍基线
- 装饰性图标 `aria-hidden="true"`;**只有图标**的控件必须传 `label`(进 `aria-label` 与 `title`)。
- 错误提示 `role="alert"`;说明类提示不加 live region,避免每次渲染都被播报。
- 不得移除 `focus-visible`;隐藏但仍可聚焦的元素要 `tabIndex={-1}`。
- 文案用中文标点;数字与单位之间不加空格(`24px`、`0.3s`)。
---
## 6. 验收清单(合并前逐条打勾)
```text
[ ] 1. 组件 CSS 里没有 hex / rgba / 字号 / 间距字面量(只用 --su-*)
[ ] 2. 覆盖默认 / hover / disabled / focus-visible(交互组件再加 loading)
[ ] 3. 透传 ...rest,button 的 type 默认 "button"
[ ] 4. className / style 作用在最外层
[ ] 5. 无外边距(margin),间距交给 Space / 父容器
[ ] 6. 展示页新增一节:变体 / 状态 / props 表 / 代码片段
[ ] 7. 导航锚点、README 清单、COMPONENTS.md 契约三处同步
[ ] 8. npm run typecheck && npm run lint && npm run build 通过
[ ] 9. 截图核对(scripts/screenshot.mjs),无视觉回归
[ ] 10. 没有新增运行时依赖
[ ] 11. 深色主题下不破:不写死白底/黑字假设,颜色全部走令牌
[ ] 12. props 名符合 §5.2;没有出现「业务字段名」(如 categoryId)
[ ] 13. 键盘走一遍:Tab 能看到焦点环,Enter/Space 能触发,Esc 能退出浮层
[ ] 14. 打开「减少动态效果」后没有持续动画(spinner 允许放慢)
```
---
## 7. 深色主题
已实现,组件零改动:所有颜色走语义令牌,深色只覆盖令牌块([tokens.css](src/theme/tokens.css) 末尾)。
用法:在 `<html>` 上设置 `data-scheme`:
| 值 | 效果 |
| --- | --- |
| 不设置 / `light` | 浅色 |
| `dark` | 深色 |
| `auto` | 跟随系统(`prefers-color-scheme`) |
```html
<html lang="zh-CN" data-scheme="dark">
```
取值来自参考站 `body[data-scheme=dark]` 的实测映射(见 [拆解文档 §8](docs/ALRIGHT-DECOMPOSITION.md)):
- 灰阶整体反相:`gray-5..8` 变深,文字改用浅色;
- `--su-bg-card` → `#202022`、`--su-border` → `#303033`、`--su-bg-page` → `#0f0f11`;
- 浅色底(`primary-soft` / `danger-soft` / `success-soft` / `warning-soft`)透明度提高,否则标签看不见;
- 阴影换成更深的黑(深色下浅色阴影没有意义)。
**新增组件的深色要求**:
- 颜色只用语义令牌(`bg-card` / `bg-subtle` / `bg-light` / `bg-hover` / `text-*` / `border-*`),
不要直接写 `--su-gray-*`——灰阶在深色下是反相的,直接用会串色;
- 展示页用 `demo.html?scheme=dark` 或右上角开关看一眼深色;
- 验收清单第 11 条会在深色下逐项检查。
---
## 8. 决策记录(为什么是这样)
| 决策 | 结论 | 理由 |
| --- | --- | --- |
| 视觉基准 | 以参考站实测值为准 | 之前按文档里的旧值做(阴影 2%、字阶 10/12/14/16),实测是 1% 和 13/14/16/18,见拆解文档 |
| 自研 vs 直接用 antd | 自研 | 视觉目标是 Alright 主题,用 antd 要重写大半 token;单二进制发布也不想多 300KB+ |
| 样式方案 | CSS Modules | Vite 内置、零依赖、类名可读;Tailwind 会把样式散进 className,emotion 要运行时 |
| 令牌前缀 | `--su-` | 宿主应用(stash-web)自己有一批 `--primary-color` 之类变量,必须能共存 |
| 组件边界 | 不收业务组件 | 业务卡片变化快,进库会把组件库拖成第二个页面层 |
| 展示站 | 自建 Vite 站 | 见 README「为什么不直接用 Storybook」 |
| 图标 | iconfont 字形子集 | 主题自带线性图标;按需加字形(流程见 COMPONENTS.md §5) |
| 焦点样式 | 与参考站相反:保留并加粗 | 参考站把 focus 样式删了(`box-shadow:none`),标了 tab 看不见焦点;组件库必须可访问 |
---
## 9. 已知坑(都踩过)
1. **展示站要显式 import 主题 CSS**:只靠 `src/index.ts` 的副作用引入时,生产构建会把 `iconfont.css` 摇掉(dev 正常、build 后图标全空)。
2. **库产物的 CSS 内联了图标字体**:lib 模式下 Vite 把 woff2 转成 data URI,`dist/stash-ui.css` 有 80KB+(gzip 56KB),换来「只引一个 CSS」。
3. **比例封面不要放进 `flex-direction: column` 的父容器**:纵向 flex 会把子项按内容高度压缩,`aspect-ratio` 封面被裁掉一截;用 grid 或 block 容器包一层。
4. **并排控件必须同高**:`Input` 与 `Button size="md"` 都是 `--su-control-height-md`。
5. **展示站里不要用 `.demo-*` 之外的自造类**:文档站样式只写在 `site/app.css`,组件样式不外溢。
6. **别照着旧文档抄数值**:参考站的 token(`--font-size-lg: 16px`)和它实际渲染的 `.text-lg: 18px` 不是一回事,
以 [拆解文档](docs/ALRIGHT-DECOMPOSITION.md) 的实测表为准。
---
## 10. 版本与变更
- 当前 `0.x`:API 仍可能调整,破坏性改动在 [COMPONENTS.md](docs/COMPONENTS.md) 末尾的「变更记录」记一行。
- 组件新增 / 删除 / 重命名,必须同步三处:`src/index.ts`、展示页、COMPONENTS.md。
## 11. 外部风格接入(2026-10-28 记录,讨论中,尚未执行)
> **场景**(之前记错了,这里更正):
> 开新项目时,使用方会给一张**别的网站的截图**,风格与 stash / Alright **不一致**,
> 要求「照这个做布局 / 这个组件」。这时必然出现三种情况:
> 库里没有、参数不够、组件不灵活。
>
> 这不是「迁移时补 CSS」的问题,而是**库要不要吸收外来设计语言**的问题。
> 如果不设闸门,结果只有两个:要么每个项目各写一套页面 CSS(库永远长不大),
> 要么把品牌专属样式塞进通用组件(库变成第二个页面层)。
### 11.1 为什么会撞墙(现有结构的三层限制)
| 层 | 现状 | 外来截图会撞到的地方 |
| --- | --- | --- |
| **颜色 / 字体** | 语义令牌 + CSS 变量,可整体覆盖 | 基本够用:换 `--su-color-*` / `--su-font-*` 即可,但**前提是页面只用语义令牌** |
| **形态**(圆角 / 阴影 / 高度 / 密度) | 一部分是令牌(radius、shadow、control-height),一部分写死在组件里(卡片圆角、按钮高度档位、Tab 高度) | 外来设计常用「32 / 40 / 48 三档高度」「24px 大圆角」「无阴影」——现在只能新增组件参数或覆盖内部类名 |
| **结构 / 交互** | 组件是按 stash 的形态做的(分区卡、封面卡、侧栏菜单) | 外来截图可能是「瀑布流 + 悬浮操作条」「顶部 Tab + 全宽表格」——这是**新组件**,不是参数问题 |
结论先说:**令牌对「配色/字体」够用,对「形态」还不够;组件对「结构」永远需要增量。**
所以闸门不是「能不能加」,而是「加在哪一层、以什么粒度加」。
### 11.2 闸门流程(收到截图后,先停再动手)
```text
截图
→ ① 拆解:把截图拆成「布局骨架 / 组件清单 / 形态值 / 交互」四张表
→ ② 映射:每一项对到库里的组件 + 令牌,标出「完全匹配 / 参数可调 / 缺失」
→ ③ 决策:按 11.3 的矩阵定归属(主题覆盖 / 加参数 / 加新组件 / 页面私有)
→ ④ 评审:把「缺失项 + 最小 API 方案」给使用方确认(此时不动代码)
→ ⑤ 实施:库先行(组件 + 契约 + 展示页 + 令牌),页面后接
→ ⑥ 验收:深浅色、焦点、reduced-motion、移动端、无业务字段
```
第 ② 步的产物就是一张 **「截图 → 组件/令牌」映射表**,模板见 [docs/STYLE-INTAKE.md](docs/STYLE-INTAKE.md)。
### 11.3 决策矩阵(外来样式该落到哪)
| 情况 | 归属 | 例子 |
| --- | --- | --- |
| 只是配色 / 字体不同 | **主题覆盖**:新增 `themes/<name>.css`,只改变量 | 紫色主色、衬线标题 |
| 同结构、形态不同(圆角 / 高度 / 密度) | **加参数或加尺寸档**到现有组件 | 按钮 40 / 48 档、卡片 16 圆角 |
| 同结构、交互不同(悬浮操作、展开方式) | **加 variant**(受控、可访问) | Tab 改成胶囊 + 下划线、卡片 hover 出操作 |
| 结构不同(新骨架 / 新组合模式) | **新组件**,前提是「≥2 处会用」或「属于通用模式」 | 瀑布流、顶部筛选条、全宽表格 |
| 只此一页的装饰(背景图、悬浮搜索、文章排版) | **页面私有 CSS**,必须在注释里写清「为什么不是组件」 | stash 的背景图层 |
三条硬边界:
1. **不许 fork 库**(复制组件到项目里改),要么参数化,要么进库;
2. **不许在页面覆盖组件内部类名**(`.su-xxx` / hashed class),只能传 props 或改令牌;
3. **品牌专属值不许进组件默认值**:外来项目的紫色不能改 `--su-color-primary` 的默认值,只能在主题文件里覆盖。
### 11.4 吸收的判定标准(避免库被拖成页面层)
一个能力同时满足下面三条才进库:
- **可复用**:至少两个使用方 / 两个页面会用到,或属于公认的通用模式;
- **无业务**:不携带字段、不请求数据、不 import 业务类型;
- **可验收**:能用 props 表达,且不破坏现有契约(破坏性改动走 COMPONENTS 变更记录)。
### 11.5 改共享组件必须报兼容性影响
组件不只一个地方用,改之前(以及交付说明里)必须显式列出:
1. **调用方清单**:哪些页面 / 组件用了它(至少列出仓库内已知调用点);
2. **变更性质**:新增可选 prop(兼容)/ 改默认值(视觉回归)/ 改或删 prop(破坏性)/ 只改样式(视觉回归);
3. **回归范围**:哪些展示页 demo、哪些应用页面需要重新截图;
4. **迁移方式**:破坏性改动要给出替代写法(旧 → 新 的对照),必要时保留一个版本周期。
> 经验:这次 `Layout` 把「页头」从限宽容器里移出来(页头通栏),属于**视觉回归**,
> 虽然 API 没变,但所有用 `contained` 的页面都会变——这种改动必须在说明里点出来,而不是静默提交。
### 11.5 待办(只记录,不执行)
- **令牌分层**:现在 `--su-*` 把「原语(灰阶、色板)」和「语义(bg-card、text-sub)」混在一起,
外来主题想换「形态」时没有落点。候选方案:
`--su-primitive-*`(色板/尺度)→ `--su-*`(语义)→ 组件只吃语义;另外把「形态值」抽成
`--su-shape-*`(radius / shadow / control-height / density),让主题能整体换形态。
- **尺寸档位**:Button / Input / Avatar 的高度档是否需要补齐 40 / 48,以及是否引入 `density="compact|comfortable"`。
- **主题文件规范**:`themes/<name>.css` 只允许改变量,禁止写选择器;需要一个 lint 或 review 检查项。
- **外部截图进库的示例**:拿一个真实的外来设计走一遍 11.2,验证流程是否可落地。