Files
stash-ui/DESIGN.md
T
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

24 KiB
Raw Blame History

Stash UI 设计系统 · 规格总纲

版本 0.1 · 适用 @stash/ui 本文件定义所有组件共同遵守的规则。新增组件、改动组件、评审组件都以下面这些条目为准。

四份文档的分工:

文档 回答什么问题 什么时候看
DESIGN.md(本文) 整体结构与硬性规则:分层、令牌、视觉、代码约定、验收标准 动手前看,评审时逐条对照
docs/COMPONENTS.md 有哪些组件、各自的契约、缺什么组件、缺的时候该做什么 想知道「用哪个 / 有没有」时看
docs/ADDING-A-COMPONENT.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 拼装

依赖方向:

L4 ──→ L3 ──→ L1
 │      │
 └──────┴──→ L1        L2 ──→ L1
  • 只允许「上层依赖下层」,禁止下层 import 上层(例如 Card 里不 import Empty)。
  • 组件之间不共享 CSS。除 Card 这种明确的容器外,组件不假设自己处在什么容器里。
  • 业务组件(分区卡片、应用条目、海报卡……)属于页面层:用库里组件拼,不进库。

3. 令牌(唯一的视觉来源)

完整定义见 src/theme/tokens.css。前缀 --su- 是为了避免和宿主应用变量冲突。 所有取值都来自参考站实测(见 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 文件结构

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 或尺寸类里:
.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. 验收清单(合并前逐条打勾)

[ ] 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 末尾)。

用法:在 <html> 上设置 data-scheme:

值 效果
不设置 / light 浅色
dark 深色
auto 跟随系统(prefers-color-scheme)
<html lang="zh-CN" data-scheme="dark">

取值来自参考站 body[data-scheme=dark] 的实测映射(见 拆解文档 §8):

  • 灰阶整体反相: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 不是一回事, 以 拆解文档 的实测表为准。

10. 版本与变更

  • 当前 0.x:API 仍可能调整,破坏性改动在 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 闸门流程(收到截图后,先停再动手)

截图
  → ① 拆解:把截图拆成「布局骨架 / 组件清单 / 形态值 / 交互」四张表
  → ② 映射:每一项对到库里的组件 + 令牌,标出「完全匹配 / 参数可调 / 缺失」
  → ③ 决策:按 11.3 的矩阵定归属(主题覆盖 / 加参数 / 加新组件 / 页面私有)
  → ④ 评审:把「缺失项 + 最小 API 方案」给使用方确认(此时不动代码)
  → ⑤ 实施:库先行(组件 + 契约 + 展示页 + 令牌),页面后接
  → ⑥ 验收:深浅色、焦点、reduced-motion、移动端、无业务字段

第 ② 步的产物就是一张 「截图 → 组件/令牌」映射表,模板见 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,验证流程是否可落地。