# Stash UI
Stash 的设计系统与组件库:**设计令牌 + 基础组件 + 组件展示站**。
独立于 `stash` 应用仓库,后续页面(控制台、详情页、后台)都从这里取组件拼装。
- 视觉标准来自 Alright 主题(与 `stash/ALRIGHT-DESIGN.md` 一致);
- 零运行时依赖(只 peer 依赖 react / react-dom);
- 样式用 CSS Modules + 设计令牌,换主题只改变量;
- 自带一个可点开的展示站,每个组件都有变体、状态、props 与代码片段。
## 快速开始
```bash
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](DESIGN.md) | **规格总纲**:分层结构、设计令牌、视觉规则、代码约定、验收清单、决策与已知坑 | 动手前、评审时 |
| [docs/COMPONENTS.md](docs/COMPONENTS.md) | **组件目录与契约**:现有 42 个组件的 API 与边界、组合模式、候选组件、图标新增流程 | 想知道「用哪个 / 有没有 / 缺什么」 |
| [docs/ADDING-A-COMPONENT.md](docs/ADDING-A-COMPONENT.md) | **新增组件手册**:12 步流程、代码模板、`Spinner` 完整示例、常见错误 | 要补一个组件时照着做 |
| [docs/ALRIGHT-DECOMPOSITION.md](docs/ALRIGHT-DECOMPOSITION.md) | **参考站拆解**:实测令牌、组件尺寸、交互节奏、暗色映射、保留差异 | 想核对「这个值对不对」 |
| [docs/STYLE-INTAKE.md](docs/STYLE-INTAKE.md) | **外部风格接入**:截图 → 组件/令牌映射表、决策矩阵、验收清单 | 新项目给了外部设计稿、和现有风格不一致时 |
缺组件时的标准动作:先在 COMPONENTS.md §5 找到它(或登记一行)→ 按 ADDING-A-COMPONENT.md 走流程 →
用 DESIGN.md §6 的清单验收。
## 给 AI 用
仓库自带一个 skill,AI 不需要每次把五份文档读一遍:
- [`skills/stash-ui/SKILL.md`](skills/stash-ui/SKILL.md) —— **入口**:任务分流(用 / 改 / 加)、硬约束、验证命令、已知坑
- [`AGENTS.md`](AGENTS.md) / [`CLAUDE.md`](CLAUDE.md) —— 仓库根指路,Codex / Claude Code / Cursor 会自动读到
- [docs/AI-USAGE.md](docs/AI-USAGE.md) —— 三种接入方式(直接读文件 / DSH 技能目录 / DSH 插件包)与维护规则
原则:**skill 只做路由和护栏,规则只写在文档里**,避免两处维护。
## 主题(外部风格)
- [`themes/example.css`](themes/example.css) —— 只改 CSS 变量的主题示例(换品牌色 / 圆角 / 阴影 / 控件高度);
- 用法:`style.css` 之后引入主题文件,**禁止写选择器**;
- 完整流程(截图 → 映射 → 决策 → 吸收)见 [DESIGN.md §11](DESIGN.md) 与 [docs/STYLE-INTAKE.md](docs/STYLE-INTAKE.md)。
## 目录结构
```text
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` |
| `TextButton` | 文字按钮(行内操作,无外框) | `icon` `tone` `disabled` `title` `label` |
| `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` |
| `ToggleRow` | 开关行(左标签 + 右开关) | `label` `checked` `onChange` `hint` `switchLabel` |
| `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` |
| `Caret` | 折叠箭头(6px 折角) | `open` `size` |
| `Breadcrumb` | 路径导航 | `items` `separator` |
| `Table` | 只读数据表格 | `columns` `rows` `rowKey` `size` `emptyText` |
| `SortableList` | 可拖拽排序的条目列表 | `items` `activeKey` `onSelect` `onReorder` `disabled` |
| `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` `suffix` `clearable` `onClear` `shortcut` `inputRef` |
| `Textarea` | 多行文本(Markdown / 简介) | `rows` `resize` `invalid` |
| `PasswordInput` | 密码输入(锁图标 + 显隐切换) | `icon` `size` `toggle` |
| `Notice` | 提示条 | `tone` `title` `action` |
| `Empty` | 空态 | `text` `action` |
| `Result` | 整块结果反馈 | `status` `title` `description` `actions` |
| `Toast` | 命令式轻提示 | `toast()` `toast.success()` `` |
| `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` —— 深色
- 右上角按钮可以实时切换
深色主题由 `` 驱动(`dark` / `auto` / 不设置),**组件零改动**:
所有颜色走语义令牌,深色只覆盖 [tokens.css](src/theme/tokens.css) 末尾的变量块。
新增组件时不要直接写 `--su-gray-*`,否则深色下会串色——规则见 [DESIGN.md §7](DESIGN.md)。
## 在应用里使用
库构建产物是标准 ESM 包,本地项目可以直接按路径引用:
```bash
# 在应用目录里
npm install ../stash-ui # 或 "dependencies": { "@stash/ui": "file:../stash-ui" }
```
```tsx
// 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()`(吸顶页头)、
`