- Input 新增 suffix 插槽,眼睛不再并排在外 - 隐藏 ::-ms-reveal / ::-ms-clear,避免 Edge 聚焦时出现两个眼睛 - 展示页补 suffix 示例;COMPONENTS.md / README 同步;版本 0.3.1
256 lines
14 KiB
Markdown
256 lines
14 KiB
Markdown
# 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` |
|
||
| `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` `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()` `<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](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()`(吸顶页头)、
|
||
`<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`、Popover `min(320px, 100vw - 32px)`、Drawer `max-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](docs/ADDING-A-COMPONENT.md) 的 12 步走,
|
||
并用 [DESIGN.md §6](DESIGN.md) 的清单验收(组件文件 → 导出 → 展示页 → 导航 → README 清单 → 契约 → typecheck/lint/build → 截图)。
|
||
|
||
## 与 stash 应用的关系
|
||
|
||
`stash-web` 目前仍是自己的全局 CSS(`App.css`),本次接入是分阶段的:
|
||
|
||
1. **已具备**(本仓库):独立组件库 + 展示站,可单独构建与预览;
|
||
2. **下一步**:在 `stash-web` 里以 `file:` 依赖接入,先把侧栏 / 顶栏 / 空态 / 按钮换成库组件;
|
||
3. **再下一步**:把 `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` 已经是起点)。
|