14 KiB
Stash UI
Stash 的设计系统与组件库:设计令牌 + 基础组件 + 组件展示站。
独立于 stash 应用仓库,后续页面(控制台、详情页、后台)都从这里取组件拼装。
- 视觉标准来自 Alright 主题(与
stash/ALRIGHT-DESIGN.md一致); - 零运行时依赖(只 peer 依赖 react / react-dom);
- 样式用 CSS Modules + 设计令牌,换主题只改变量;
- 自带一个可点开的展示站,每个组件都有变体、状态、props 与代码片段。
快速开始
npm install
npm run dev # 文档站:http://127.0.0.1:5199
# 整页示例:http://127.0.0.1:5199/demo.html(?scheme=dark 直接进深色)
npm run typecheck # tsc --noEmit
npm run build # 构建库:dist/stash-ui.js + dist/stash-ui.css + dist/types
npm run build:site # 构建静态站点:dist-site/(index.html + demo.html)
npm run lint # oxlint
# 截图核对(需要本机 Chrome / Edge;playwright-core 不自带浏览器)
node scripts/screenshot.mjs # 首屏
node scripts/screenshot.mjs http://127.0.0.1:5199/#buttons shots/buttons.png
node scripts/screenshot.mjs http://127.0.0.1:5199/ shots/cards.png '#cards' # 只截某一节
环境要求:Node 24(package.json 里用 volta 固定了版本)。
文档(先读这个)
四份文档构成组件库的「契约」,新增 / 修改组件全部以它们为准:
| 文档 | 内容 | 什么时候看 |
|---|---|---|
| DESIGN.md | 规格总纲:分层结构、设计令牌、视觉规则、代码约定、验收清单、决策与已知坑 | 动手前、评审时 |
| docs/COMPONENTS.md | 组件目录与契约:现有 40 个组件的 API 与边界、组合模式、候选组件、图标新增流程 | 想知道「用哪个 / 有没有 / 缺什么」 |
| docs/ADDING-A-COMPONENT.md | 新增组件手册:12 步流程、代码模板、Spinner 完整示例、常见错误 |
要补一个组件时照着做 |
| docs/ALRIGHT-DECOMPOSITION.md | 参考站拆解:实测令牌、组件尺寸、交互节奏、暗色映射、保留差异 | 想核对「这个值对不对」 |
| docs/STYLE-INTAKE.md | 外部风格接入:截图 → 组件/令牌映射表、决策矩阵、验收清单 | 新项目给了外部设计稿、和现有风格不一致时 |
缺组件时的标准动作:先在 COMPONENTS.md §5 找到它(或登记一行)→ 按 ADDING-A-COMPONENT.md 走流程 → 用 DESIGN.md §6 的清单验收。
给 AI 用
仓库自带一个 skill,AI 不需要每次把五份文档读一遍:
skills/stash-ui/SKILL.md—— 入口:任务分流(用 / 改 / 加)、硬约束、验证命令、已知坑AGENTS.md/CLAUDE.md—— 仓库根指路,Codex / Claude Code / Cursor 会自动读到- docs/AI-USAGE.md —— 三种接入方式(直接读文件 / DSH 技能目录 / DSH 插件包)与维护规则
原则:skill 只做路由和护栏,规则只写在文档里,避免两处维护。
主题(外部风格)
themes/example.css—— 只改 CSS 变量的主题示例(换品牌色 / 圆角 / 阴影 / 控件高度);- 用法:
style.css之后引入主题文件,禁止写选择器; - 完整流程(截图 → 映射 → 决策 → 吸收)见 DESIGN.md §11 与 docs/STYLE-INTAKE.md。
目录结构
stash-ui/
├── DESIGN.md # 规格总纲(分层/令牌/视觉/验收)
├── AGENTS.md / CLAUDE.md # AI 入口(指路到 skill 与文档)
├── skills/stash-ui/SKILL.md # AI skill:任务分流 + 约束 + 验证
├── docs/
│ ├── COMPONENTS.md # 组件目录与契约
│ ├── ADDING-A-COMPONENT.md # 新增组件手册
│ ├── ALRIGHT-DECOMPOSITION.md# 参考站拆解(数值依据)
│ └── AI-USAGE.md # AI 接入方式与 skill 维护
├── src/ # 组件库(发布物只有这一层)
│ ├── index.ts # 统一出口:组件 + 类型 + 样式
│ ├── theme/
│ │ ├── tokens.css # 设计令牌(颜色/间距/字号/圆角/阴影/动效)
│ │ ├── icons.ts # 446 个图标名(自动生成)
│ │ ├── iconfont.css # 图标字体与字形(自动生成)
│ │ └── assets/iconfont.woff2
│ └── components/ # 37 个组件,每个 = Xxx.tsx + Xxx.module.css(见下方清单)
├── site/ # 展示站(不参与库构建)
│ ├── index.html / demo.html # 文档站 / 整页示例
│ ├── App.tsx # 左侧目录 + 右侧示例
│ ├── app.css # 文档站自己的排版样式
│ ├── components/ # Section / Demo / PropsTable
│ └── sections/ # 每个组件一节
├── scripts/screenshot.mjs # 截图核对用
├── vite.config.ts # 展示站构建
└── vite.lib.config.ts # 库构建
组件清单
| 组件 | 用途 | 关键 props |
|---|---|---|
Button |
操作按钮(圆角 / 胶囊,支持描边) | variant size shape block loading icon |
IconButton |
圆形图标按钮(28/36/44) | label(必填) icon variant size spinning |
Card |
白色容器(极浅描边) | as padding hoverable |
Cover |
比例封面卡(2:3 / 4:3 / 16:9) | ratio src caption lift onClick |
Image |
带比例 / 圆角 / 失败降级的图片 | ratio fit radius fallback |
FormField |
表单字段排版(标签 + 提示 / 错误) | label hint error required |
Typography |
标题 / 文字 / 段落 | level tone size lines |
Skeleton |
骨架屏占位 | variant lines width height |
Spinner |
加载指示 | size label |
Switch |
即时开关 | checked onChange size label |
Checkbox |
复选框 | checked onChange label |
RadioGroup |
互斥单选组 | options value onChange direction |
Select |
受控下拉选择 | options value onChange placeholder |
Segmented |
视图形态切换 | options value onChange size block |
Tabs |
页内分区切换 | items activeKey onChange variant |
Menu |
侧栏两级菜单 | items activeKey onSelect |
Breadcrumb |
路径导航 | items separator |
Table |
只读数据表格 | columns rows rowKey emptyText |
Tag |
分类 / 标签 / 状态(浅底 / 实底) | tone variant shape icon dot |
ItemBadge |
状态角标:置顶 / 付费 / HOT | tone |
Badge |
数字角标 | count tone max |
Avatar |
头像 / 封面占位 | src fallback size shape verified |
Grid |
卡片列表网格 | columns tabletColumns mobileColumns gap as |
Layout |
页面骨架:页头 / 侧栏 / 内容 / 右栏 / 页脚 | header sider aside footer maxWidth gap |
Space |
元素间距 | direction gap wrap align justify |
Divider |
分隔线 | orientation variant spacing |
Input |
输入框 | icon clearable onClear shortcut inputRef |
Notice |
提示条 | tone title action |
Empty |
空态 | text action |
Result |
整块结果反馈 | status title description actions |
Toast |
命令式轻提示 | toast() toast.success() <Toaster /> |
Progress |
进度条 | percent size tone showLabel |
Steps |
纵向步骤条 | items current |
Modal |
居中弹窗(原生 dialog) | open onClose title footer size |
Drawer |
侧滑抽屉(原生 dialog) | open onClose title placement width |
Tooltip |
hover / 聚焦文字提示 | content placement |
Popover |
点击展开的小面板 | content placement |
Pagination |
分页 | page total pageSize onChange |
ScrollTop |
回到顶部 | visible targetId |
Icon |
图标(446 个字形) | name |
整页示例与深色主题
site/demo.html 是用这套组件拼出来的完整页面(三栏布局:资料卡 / 内容流 / 侧栏聚合),
只用了库里的组件 + 页面自己的布局样式:
http://127.0.0.1:5199/demo.html—— 浅色http://127.0.0.1:5199/demo.html?scheme=dark—— 深色- 右上角按钮可以实时切换
深色主题由 <html data-scheme> 驱动(dark / auto / 不设置),组件零改动:
所有颜色走语义令牌,深色只覆盖 tokens.css 末尾的变量块。
新增组件时不要直接写 --su-gray-*,否则深色下会串色——规则见 DESIGN.md §7。
在应用里使用
库构建产物是标准 ESM 包,本地项目可以直接按路径引用:
# 在应用目录里
npm install ../stash-ui # 或 "dependencies": { "@stash/ui": "file:../stash-ui" }
// main.tsx:样式只引一次(令牌 + 图标字体 + 组件样式都在这份文件里)
import '@stash/ui/style.css'
import { Button, Card, Grid } from '@stash/ui'
接入约定:
- 只引一次
style.css,不要在页面里重复引令牌; - 应用自己的页面级样式(布局、页面专属类)写在应用里,不要加进组件库;
- 需要改主题就覆盖
--su-*变量,不要覆盖组件的内部类名。
浏览器与响应式
目标:桌面 Chrome / Edge / Safari / Firefox 最新两个版本;iOS Safari 16+、Android Chrome 110+。
库只用这些基线能力:CSS 变量、CSS Modules、:focus-visible、color-mix()(吸顶页头)、
<dialog>(Modal / Drawer)、dvh、-webkit-line-clamp(段落截断)。
响应式在组件内部处理,页面不用各写一套:
| 断点 | 组件 | 行为 |
|---|---|---|
| ≤1200px | Layout |
右栏堆到内容下方 |
| ≤1100px | Layout / Grid |
侧栏堆到内容上方;网格降为 2 列 |
| ≤720px | Grid |
网格降为 1 列 |
触屏相关的约定:
- 交互只用 click,不依赖 hover;
Tooltip的 hover 样式包在@media (hover: hover)里; - 弹层宽度都有夹取(Modal
100vw - 32px、Popovermin(320px, 100vw - 32px)、Drawermax-width: 100vw); - 表格、标签栏、分段控件在窄屏横向滚动,不做压缩;
- 移动端建议用
sm(36)/md(44)的图标按钮,xs(28)留给桌面密集场景。
已实测:文档站与示例页在 390×844(手机)、820×1180(平板)、1440×900(桌面) 下无横向溢出。
尚未在真机(iOS Safari 软键盘、dvh、backdrop-filter)上复核。
约定(摘要,完整规则见 DESIGN.md)
- 令牌先行:组件里不允许出现硬编码色值 / 字号 / 间距,一律走
--su-*; - CSS Modules:每个组件一个
Component.module.css,类名短(.root/.sm)且不外泄; - 组件不带数据:库里的组件只接收 props,不请求接口、不读全局状态;
- 新增组件:按 ADDING-A-COMPONENT.md 的 12 步走, 并用 DESIGN.md §6 的清单验收(组件文件 → 导出 → 展示页 → 导航 → README 清单 → 契约 → typecheck/lint/build → 截图)。
与 stash 应用的关系
stash-web 目前仍是自己的全局 CSS(App.css),本次接入是分阶段的:
- 已具备(本仓库):独立组件库 + 展示站,可单独构建与预览;
- 下一步:在
stash-web里以file:依赖接入,先把侧栏 / 顶栏 / 空态 / 按钮换成库组件; - 再下一步:把
App.css按组件拆分,页面只保留布局样式;同步删掉库中重复的样式定义。
对照表(迁移时查):
| stash 现有类 | 组件库对应 |
|---|---|
.card-box |
Card |
.section-empty |
Empty |
.notice / .notice-error |
Notice(tone="info" / "error") |
.header-info-tag / .item-tag / .service-pill |
Tag |
.count-badge |
Badge |
.search-bar |
Input(icon="search" + clearable) |
.app-grid / .poster-grid / .article-grid / .ces-grid |
Grid |
.back-to-top |
ScrollTop |
.profile-avatar / .app-card-avatar |
Avatar |
构建注意(踩过的坑)
- 展示站要显式 import 主题 CSS:
site/main.tsx里直接引src/theme/tokens.css与src/theme/iconfont.css。只靠src/index.ts的副作用引入时,生产构建会把iconfont.css摇掉(dev 正常、build 后图标全空)。 - 库产物的 CSS 内联了图标字体:lib 模式下 Vite 会把 woff2 转成 data URI,
所以
dist/stash-ui.css有 80KB+(gzip 56KB),换来「只引一个 CSS 文件」。 展示站走普通构建,字体仍是独立资源。 - 比例封面不要放进
flex-direction: column的父容器:纵向 flex 会把子项按内容高度压缩, 带aspect-ratio的封面会被裁掉一截(site/app.css里纵向示例因此改用 grid)。 - 组件尺寸对齐:
Input与Button size="md"都是 36px,并排成一行才是齐的。
为什么不直接用 Storybook
Storybook 能做的事我们这里只需要一小部分,而它要额外的依赖、端口和一套独立构建; 本项目是「本地小工具 + 单二进制发布」的配套,直接做成一个 Vite 站点更轻:
site/就是文档站,dev 下改组件即时生效,build 后是纯静态页;- 展示用例直接用真实组件,不存在「story 与页面不一致」;
- 截图脚本(
scripts/screenshot.mjs)已经把视觉回归的基础打好了, 将来要接 Playwright 截图对比也不用换工具。
后续可做
- 暗色主题:令牌已经收敛,补一份
[data-theme='dark']覆盖即可; - Typography / Menu / Tabs / Modal 这类更上层的组件;
- 展示站加「复制代码」与 props 表自动生成;
- 视觉回归:在展示站上加 Playwright 截图对比(
scripts/screenshot.mjs已经是起点)。