feat: stash-ui 0.1.0(40 个组件 + 文档站 + 主题机制)
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
# 新增一个组件 · 操作手册
|
||||
|
||||
> 配套:[DESIGN.md](../DESIGN.md)(规则)、[COMPONENTS.md](COMPONENTS.md)(目录与契约)
|
||||
> 目标:任何人(或 AI)只读这三份文档,就能补出一个风格、API、文档都与现有组件一致的组件。
|
||||
|
||||
---
|
||||
|
||||
## 0. 先判断:该不该进库
|
||||
|
||||
三个问题**全部为「是」**才进库:
|
||||
|
||||
1. **通用吗**:换一个页面、换一个应用还用得上?
|
||||
2. **无业务吗**:不需要请求接口、不需要认识 `Category` / `entry` 这类业务字段?
|
||||
3. **组合不出来吗**:现有组件拼不出来?只是「拼起来麻烦一点」不算。
|
||||
|
||||
对应处理:
|
||||
|
||||
| 判断结果 | 处理 |
|
||||
| --- | --- |
|
||||
| 全「是」 | 进库,走下面的流程 |
|
||||
| 业务相关 | 放应用里(页面层),用现有组件拼;必要时在 COMPONENTS.md §4 记一条组合模式 |
|
||||
| 组合能解决 | 不进库,把组合写进展示页 `#composition` |
|
||||
|
||||
---
|
||||
|
||||
## 1. 流程总览(12 步)
|
||||
|
||||
| # | 动作 | 产物 | 卡点 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 在 COMPONENTS.md 登记 | 组件地图一行 + §3 契约草案(状态 🚧) | 没登记不开工 |
|
||||
| 2 | 定层级与命名 | 决定 L1–L4 与组件名 | 参照 DESIGN §2 |
|
||||
| 3 | 设计 API | props 表(含类型、默认值) | 命名必须符合 DESIGN §5.2 |
|
||||
| 4 | 写组件 | `src/components/Xxx.tsx` | 透传 rest、`type="button"`、className 合并 |
|
||||
| 5 | 写样式 | `src/components/Xxx.module.css` | 只用令牌;覆盖状态矩阵 |
|
||||
| 6 | 导出 | `src/index.ts` | 组件 + `export type` |
|
||||
| 7 | 展示页 | `site/sections/Xxx.tsx` + App.tsx 挂载 | 变体 / 状态 / props / 代码 |
|
||||
| 8 | 导航与清单 | `site/App.tsx` 的 NAV、README 组件表 | 三处同步(还有契约) |
|
||||
| 9 | 契约定稿 | COMPONENTS.md 状态改 ✅ | 与代码逐字对一遍 |
|
||||
| 10 | 验收 | DESIGN §6 清单逐条打勾 | 有一条不过就不提交 |
|
||||
| 11 | 截图核对 | `shots/` 下的截图 | 看 hover / disabled / 边界 |
|
||||
| 12 | 提交 | `feat(ui): add Xxx` | 一个组件一个提交 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 模板
|
||||
|
||||
### 2.1 组件 TSX
|
||||
|
||||
```tsx
|
||||
import type { HTMLAttributes, ReactNode } from 'react'
|
||||
import styles from './Xxx.module.css'
|
||||
|
||||
/**
|
||||
* 一句话说清它是什么。
|
||||
*
|
||||
* 第二段写「边界」:不该用在哪、和相邻组件的区别。
|
||||
*/
|
||||
export type XxxVariant = 'primary' | 'secondary'
|
||||
|
||||
export type XxxProps = {
|
||||
/** 语义变体 */
|
||||
variant?: XxxVariant
|
||||
/** 尺寸档位 */
|
||||
size?: 'sm' | 'md'
|
||||
children?: ReactNode
|
||||
} & HTMLAttributes<HTMLDivElement> // 透传原生属性
|
||||
|
||||
export function Xxx({ variant = 'primary', size = 'md', children, className, ...rest }: XxxProps) {
|
||||
// 类名合并:固定顺序 root → 修饰 → 外部传入
|
||||
const classes = [styles.root, styles[variant], styles[size], className].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<div className={classes} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- `className` / `style` 落在**最外层**;
|
||||
- 变体用 `styles[variant]` 映射,不写 if 分支拼类名;
|
||||
- 按钮类组件:`type` 默认 `"button"`,`disabled` 与 `loading` 一起处理;
|
||||
- 受控组件不维护业务状态(`Avatar` 的图片加载失败属于 UI 状态,可以留)。
|
||||
|
||||
### 2.2 样式 module
|
||||
|
||||
```css
|
||||
/* 组件专属尺寸用局部变量;颜色 / 字号 / 间距一律全局令牌 */
|
||||
.root {
|
||||
--xxx-size: 36px;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--su-space-2);
|
||||
height: var(--xxx-size);
|
||||
padding: 0 var(--su-space-3);
|
||||
border-radius: var(--su-radius-sm);
|
||||
font-size: var(--su-font-size-md);
|
||||
color: var(--su-text-main);
|
||||
transition: background-color var(--su-duration-fast) var(--su-ease-out);
|
||||
}
|
||||
|
||||
/* 变体 */
|
||||
.primary {
|
||||
background-color: var(--su-color-primary);
|
||||
color: var(--su-white);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: var(--su-gray-2);
|
||||
color: var(--su-text-main);
|
||||
}
|
||||
|
||||
/* 尺寸 */
|
||||
.sm {
|
||||
--xxx-size: var(--su-control-height-sm);
|
||||
font-size: var(--su-font-size-sm);
|
||||
}
|
||||
|
||||
/* 状态:hover / disabled / focus-visible 见 DESIGN §4.3 */
|
||||
.root:hover:not(:disabled) {
|
||||
background-color: var(--su-gray-3);
|
||||
}
|
||||
|
||||
.root:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 展示页 section
|
||||
|
||||
```tsx
|
||||
import { Xxx } from '../../src'
|
||||
import { Demo } from '../components/Demo'
|
||||
import { PropsTable } from '../components/PropsTable'
|
||||
import { Section } from '../components/Section'
|
||||
|
||||
export function XxxSection() {
|
||||
return (
|
||||
<Section id="xxx" title="组件名" api="Xxx" desc="一句话说明它的职责和边界。">
|
||||
<Demo
|
||||
title="基础用法"
|
||||
desc="说明这个变体适合什么场景。"
|
||||
code={`<Xxx variant="primary">内容</Xxx>`}
|
||||
>
|
||||
<Xxx>内容</Xxx>
|
||||
</Demo>
|
||||
|
||||
<Demo title="状态" desc="把 hover / disabled / 空态都摆出来。" layout="column">
|
||||
<Xxx>默认</Xxx>
|
||||
<Xxx disabled>禁用</Xxx>
|
||||
</Demo>
|
||||
|
||||
<PropsTable
|
||||
rows={[
|
||||
{ name: 'variant', type: "'primary' | 'secondary'", defaultValue: "'primary'", desc: '语义变体' },
|
||||
{ name: '…rest', type: 'HTMLAttributes', desc: '透传原生属性' },
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 契约(贴进 COMPONENTS.md §3)
|
||||
|
||||
见 [COMPONENTS.md §8](COMPONENTS.md) 的模板。
|
||||
|
||||
---
|
||||
|
||||
## 3. 完整示例:`Spinner`
|
||||
|
||||
假设要补 COMPONENTS.md §5 里的 P2 组件 `Spinner`,从头到尾就是这样。
|
||||
|
||||
### 3.1 登记(第 1 步)
|
||||
|
||||
在 COMPONENTS.md §1 加一行:
|
||||
|
||||
| 组件 | 层 | 状态 | 一句话 | 关键 props |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `Spinner` | L4 | 🚧 | 独立加载指示 | `size` `label` |
|
||||
|
||||
### 3.2 组件(第 4 步)
|
||||
|
||||
`src/components/Spinner.tsx`:
|
||||
|
||||
```tsx
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import styles from './Spinner.module.css'
|
||||
|
||||
/**
|
||||
* 加载指示:一个旋转的圆环。
|
||||
*
|
||||
* 按钮内部的 loading 由 Button 自己处理;
|
||||
* 这个组件用于「整块区域正在加载」——列表首屏、面板刷新。
|
||||
* 一行文字的占位请用 Skeleton,不要用 Spinner。
|
||||
*/
|
||||
export type SpinnerProps = {
|
||||
size?: 'sm' | 'md'
|
||||
/** 无障碍名称,屏幕阅读器会读出来 */
|
||||
label?: string
|
||||
} & HTMLAttributes<HTMLSpanElement>
|
||||
|
||||
export function Spinner({ size = 'md', label = '加载中', className, ...rest }: SpinnerProps) {
|
||||
const classes = [styles.root, styles[size], className].filter(Boolean).join(' ')
|
||||
|
||||
return <span className={classes} role="status" aria-label={label} {...rest} />
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 样式(第 5 步)
|
||||
|
||||
`src/components/Spinner.module.css`:
|
||||
|
||||
```css
|
||||
.root {
|
||||
--spinner-size: 20px;
|
||||
--spinner-border: 2px;
|
||||
|
||||
display: inline-block;
|
||||
width: var(--spinner-size);
|
||||
height: var(--spinner-size);
|
||||
border: var(--spinner-border) solid currentColor;
|
||||
border-top-color: transparent;
|
||||
border-radius: var(--su-radius-pill);
|
||||
color: var(--su-color-primary);
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.sm {
|
||||
--spinner-size: 14px;
|
||||
--spinner-border: 2px;
|
||||
}
|
||||
|
||||
.md {
|
||||
--spinner-size: 20px;
|
||||
}
|
||||
|
||||
/* 尊重系统的「减少动态效果」:放慢而不是停住(停住会看起来像坏掉了) */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.root {
|
||||
animation-duration: 2.4s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 导出(第 6 步)
|
||||
|
||||
`src/index.ts`:
|
||||
|
||||
```ts
|
||||
export { Spinner } from './components/Spinner'
|
||||
export type { SpinnerProps } from './components/Spinner'
|
||||
```
|
||||
|
||||
### 3.5 展示页(第 7 步)
|
||||
|
||||
`site/sections/Spinners.tsx`:
|
||||
|
||||
```tsx
|
||||
import { Spinner } from '../../src'
|
||||
import { Demo } from '../components/Demo'
|
||||
import { PropsTable } from '../components/PropsTable'
|
||||
import { Section } from '../components/Section'
|
||||
|
||||
export function Spinners() {
|
||||
return (
|
||||
<Section
|
||||
id="spinner"
|
||||
title="加载中"
|
||||
api="Spinner"
|
||||
desc="整块区域的加载指示;文字占位用 Skeleton,按钮内的加载态交给 Button 的 loading。"
|
||||
>
|
||||
<Demo title="尺寸" code={`<Spinner size="sm" />\n<Spinner size="md" />`}>
|
||||
<Spinner size="sm" />
|
||||
<Spinner size="md" />
|
||||
</Demo>
|
||||
|
||||
<Demo title="带文字的加载区" layout="column" code={`<Spinner label="正在读取内容库" />`}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 'var(--su-space-2)' }}>
|
||||
<Spinner />
|
||||
<span style={{ color: 'var(--su-text-muted)' }}>正在读取内容库…</span>
|
||||
</span>
|
||||
</Demo>
|
||||
|
||||
<PropsTable
|
||||
rows={[
|
||||
{ name: 'size', type: "'sm' | 'md'", defaultValue: "'md'", desc: '14px / 20px' },
|
||||
{ name: 'label', type: 'string', defaultValue: "'加载中'", desc: '无障碍名称' },
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
`site/App.tsx`:在「交互」组里加 `{ id: 'spinner', label: '加载中 Spinner' }`,并在 `<Feedback />` 后挂 `<Spinners />`。
|
||||
|
||||
### 3.6 契约(第 9 步)
|
||||
|
||||
```markdown
|
||||
### Spinner ✅
|
||||
|
||||
- **用途**:整块区域正在加载(列表首屏、面板刷新)。
|
||||
- **不该用在哪**:按钮内部(用 `Button loading`)、文字占位(用 `Skeleton`)。
|
||||
- **解剖**:单个 span,`role="status"` + `aria-label`,纯 CSS 圆环。
|
||||
- **Props**:`size: 'sm' | 'md'`(14 / 20)、`label`(默认「加载中」)。
|
||||
- **规则**:颜色继承 `currentColor`;`prefers-reduced-motion` 下放慢到 2.4s,不停住。
|
||||
- **展示页**:`#spinner`
|
||||
```
|
||||
|
||||
### 3.7 验收与截图(第 10–11 步)
|
||||
|
||||
```bash
|
||||
npm run typecheck && npm run lint && npm run build && npm run build:site
|
||||
npm run dev # 另开一个终端
|
||||
node scripts/screenshot.mjs http://127.0.0.1:5199/ shots/spinner.png '#spinner'
|
||||
```
|
||||
|
||||
对着截图确认:尺寸档位一致、颜色是主色、没有布局抖动、深色底不破(把背景改成 `--su-gray-8` 试一下)。
|
||||
|
||||
### 3.8 提交(第 12 步)
|
||||
|
||||
```text
|
||||
feat(ui): add Spinner
|
||||
|
||||
- 新增加载指示组件(sm/md,role=status + aria-label)
|
||||
- 展示页补一节;COMPONENTS.md 契约与 README 清单同步
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 常见错误(都踩过或差点踩)
|
||||
|
||||
| 现象 | 原因 | 正确做法 |
|
||||
| --- | --- | --- |
|
||||
| 换主题后组件颜色不变 | CSS 里写死了 `#fff` / `rgba(...)` | 一律 `var(--su-*)`;颜色不够就去 tokens.css 补令牌 |
|
||||
| 组件在页面里位置飘 | 组件自带 margin | 组件不带外边距,间距交给 `Space` / 父容器 |
|
||||
| 并排的按钮/输入框高低不齐 | 高度写死且不一致 | 用 `--su-control-height-sm/md` |
|
||||
| 图标按钮读不出来 | 只有图标没有名称 | `IconButton` 的 `label` 必填 |
|
||||
| 表单里点按钮页面刷新 | 没写 `type` | 默认 `type="button"` |
|
||||
| 行式条目看起来「一块一块」 | 给列表行套了 `Card` | 列表行不加底色/边框/阴影 |
|
||||
| 比例封面被裁掉一截 | 外层是 `flex-direction: column` | 换 grid / block 容器(DESIGN §9) |
|
||||
| 生产构建图标全空 | 展示站没有显式引入 `iconfont.css` | `site/main.tsx` 显式 import 两个 theme CSS |
|
||||
| 展示页 demo 和真实页面不一致 | demo 里写了一份「演示版」 | demo 必须直接用组件,只喂数据 |
|
||||
| 组件里出现 `categoryId` | 把业务塞进了库 | 业务字段留在页面层 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 评审时的三句话
|
||||
|
||||
1. **够通用吗?**(不是业务组件、不是一次性布局)
|
||||
2. **只用了令牌吗?**(颜色/字号/间距没有字面量)
|
||||
3. **文档齐了吗?**(展示页一节 + 契约 + 清单 + 导航)
|
||||
|
||||
三条都过,就可以合并。
|
||||
@@ -0,0 +1,102 @@
|
||||
# 让 AI 正确使用 / 扩展这套组件库
|
||||
|
||||
结论先说:**需要 skill,但 skill 只做「入口 + 护栏 + 验证循环」,规则仍然只写在文档里。**
|
||||
否则规则会两处维护,迟早漂移。
|
||||
|
||||
## 1. 三件东西各管什么
|
||||
|
||||
| 文件 | 给谁 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| [`skills/stash-ui/SKILL.md`](../skills/stash-ui/SKILL.md) | 支持 skill 的 agent(DSH 等) | 任务分流(用 / 改 / 加)、硬约束、验证命令、已知坑 |
|
||||
| [`AGENTS.md`](../AGENTS.md) | 任意 AI 编码工具(Codex、Claude Code、Cursor…) | 进仓库先读到「去哪里看」 |
|
||||
| [`DESIGN.md`](../DESIGN.md) / [`docs/COMPONENTS.md`](COMPONENTS.md) / [`docs/ADDING-A-COMPONENT.md`](ADDING-A-COMPONENT.md) | 人与 AI | 唯一事实来源(规则、契约、流程) |
|
||||
|
||||
skill 的内容刻意很短(约 80 行),因为它解决的是**入口问题**:
|
||||
AI 不需要每次都把 5 份文档读一遍,而是先被路由到正确的文档和验收命令。
|
||||
|
||||
## 2. 三种接入方式
|
||||
|
||||
### 2.1 直接读文件(最通用,零配置)
|
||||
|
||||
任何能读仓库的 AI 都可以从 `skills/stash-ui/SKILL.md` 开始:
|
||||
|
||||
```text
|
||||
读 skills/stash-ui/SKILL.md,按里面的流程执行。
|
||||
```
|
||||
|
||||
Claude Code / Codex / Cursor 这类工具会自动读仓库根的 `AGENTS.md`(或 `CLAUDE.md`),
|
||||
所以把仓库交给它们时,入口是自动的。
|
||||
|
||||
### 2.2 DeepSeek Harness(技能目录)
|
||||
|
||||
如果你的 harness 支持扫描技能目录,把 `skills/stash-ui/` 整个复制到技能根目录即可
|
||||
(目录名即 skill 名,`SKILL.md` 的 frontmatter 里也有 `name: stash-ui`)。
|
||||
|
||||
### 2.3 DeepSeek Harness(插件包,官方同款做法)
|
||||
|
||||
DSH 的 skill 由 `@deepseek-ai/dsh-skill-filesystem` 提供,插件通过 `cordis.patch.yml`
|
||||
注册一个 `bundledSkillDir`。最小骨架如下(放在本仓库之外,例如 `stash-ui-dsh-plugin/`):
|
||||
|
||||
```text
|
||||
stash-ui-dsh-plugin/
|
||||
├── package.json # name: @yourscope/stash-ui-dsh,dsh.bundle.patch: ./cordis.patch.yml
|
||||
├── cordis.patch.yml
|
||||
├── lib/index.js # 导出 resolveStashSkillRoot(可选,便于测试)
|
||||
└── skills/stash-ui/SKILL.md
|
||||
```
|
||||
|
||||
`cordis.patch.yml`(把 `<你的包名>` 换成实际 npm 名):
|
||||
|
||||
```yaml
|
||||
- insert:
|
||||
- id: stash-ui-skill-filesystem
|
||||
name: '@deepseek-ai/dsh-skill-filesystem'
|
||||
config:
|
||||
providerName: stash-ui
|
||||
includeDefaultRoots: false
|
||||
bundledSkillDir: !!js process.getBuiltinModule('node:path').join(
|
||||
process.getBuiltinModule('node:path').dirname(
|
||||
process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('<你的包名>/package.json')
|
||||
),
|
||||
'skills'
|
||||
)
|
||||
```
|
||||
|
||||
`package.json` 关键字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "<你的包名>",
|
||||
"type": "module",
|
||||
"files": ["lib", "cordis.patch.yml", "skills"],
|
||||
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
|
||||
}
|
||||
```
|
||||
|
||||
装到 DSH profile 后,`skill` 工具的技能目录里就会出现 `stash-ui`;
|
||||
如果没出现,先确认 patch 里的包名能被 profile 的 `createRequire(baseUrl)` 解析到。
|
||||
|
||||
## 3. AI 用这套库时的三条铁律
|
||||
|
||||
1. **先查再用**:`COMPONENTS.md §2` 有「用哪个」决策表;能组合就不新增组件。
|
||||
2. **令牌唯一**:颜色 / 间距 / 字号 / 圆角只走 `--su-*`;直接写 `--su-gray-*` 会在深色下串色。
|
||||
3. **改 props = 改契约**:组件、展示页、`COMPONENTS.md` + README 三处必须同时更新。
|
||||
|
||||
## 4. 验证循环(AI 必须自己跑)
|
||||
|
||||
```bash
|
||||
npm run typecheck && npm run lint && npm run build && npm run build:site
|
||||
node scripts/screenshot.mjs http://127.0.0.1:5199/#<section> shots/<section>.png
|
||||
node scripts/screenshot.mjs "http://127.0.0.1:5199/demo.html?scheme=dark" shots/demo-dark.png full
|
||||
```
|
||||
|
||||
- `lint` 要求 **0 warnings**(不是「能过就行」)。
|
||||
- 视觉改动要贴图核对;深色必须单独看一遍。
|
||||
- 交付时说明改了哪些组件、契约是否同步、截图路径。
|
||||
|
||||
## 5. 维护 skill 的规则
|
||||
|
||||
- skill 里**不复制**具体数值、props 表、12 步流程;只写入口、约束、命令、坑。
|
||||
- 文档改了影响流程的部分(验收清单、命令、目录结构)时,同步改 skill 那几行。
|
||||
- 新增「已知坑」时,先写到 `DESIGN.md §9`,再决定要不要在 skill 里留一句指针。
|
||||
- 每次组件数量变化,只改 README / COMPONENTS,skill 里不写死数量(本文档除外)。
|
||||
@@ -0,0 +1,201 @@
|
||||
# Alright 参考站拆解
|
||||
|
||||
> 对标站点:<https://alright.demo.nicetheme.xyz/>
|
||||
> 拆解日期:2026-10-26 · 对照版本:`@stash/ui` 0.1.0
|
||||
> 产物:[DESIGN.md](../DESIGN.md)(规则)、[COMPONENTS.md](COMPONENTS.md)(契约)都按这份拆解更新。
|
||||
|
||||
## 1. 怎么拆的
|
||||
|
||||
不看截图猜,全部取实测值:
|
||||
|
||||
| 手段 | 用途 | 落盘位置 |
|
||||
| --- | --- | --- |
|
||||
| 抓取主题 CSS(620KB,Autoptimize 合并包) | 读原始规则:令牌、`:hover`、`@media`、暗色覆盖 | `.ref/theme.css` |
|
||||
| Playwright 跑计算样式 | 拿到真实渲染值(字号、行高、圆角、阴影、尺寸、过渡) | `.ref/probe.json` |
|
||||
| Playwright 截图 | 桌面 1440 / 移动 390、首页 / 详情页 | `.ref/shots/*.png` |
|
||||
|
||||
复现命令(都要先 `npm run dev`):
|
||||
|
||||
```bash
|
||||
node .ref/probe.mjs # 重新采集计算样式 + 截图
|
||||
node .ref/layout.mjs # 只量布局(列宽、头部高度)
|
||||
```
|
||||
|
||||
`.ref/` 只是拆解产物,不进构建、不进发布。
|
||||
|
||||
## 2. 站点底座(决定了哪些东西不该抄)
|
||||
|
||||
- **WordPress + Bootstrap 5**:按钮、表单、模态、分页的基础行为来自 Bootstrap;
|
||||
- **Alpine.js**(`x-data` / `@click`):交互状态,例如点赞、主题切换;
|
||||
- **Swiper / Fancybox / Plyr / CKEditor**:轮播、灯箱、播放器、编辑器,各有独立样式;
|
||||
- **自建 iconfont**(Project 4809463):线性图标,字号 18~24;
|
||||
- 主题自己的工具类:`.text-lg`、`.flex-avatar`、`.item-badge`、`.box-block`、`.tags-list`……
|
||||
|
||||
结论:**只对齐主题层**(令牌、尺寸、间距、圆角、阴影、交互节奏、组件外观),
|
||||
不复刻 Bootstrap 的组件实现(下拉、模态那种带 JS 状态机的东西我们自己做,见 COMPONENTS.md §5)。
|
||||
|
||||
## 3. 设计令牌对照
|
||||
|
||||
| 参考站变量 | 值 | 我们的令牌 | 对齐情况 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--gray-1 … --gray-8` | `#f7f8fa` `#f6f6f6` `#eee` `#c9cdd4` `#86909c` `#4e5969` `#2c343c` `#1d2129` | `--su-gray-1 … -8` | ✅ 完全一致 |
|
||||
| `--blue` | `#3858f6` | `--su-color-primary` | ✅ |
|
||||
| `--red` | `#ff3e3e` | `--su-color-danger` | ✅ |
|
||||
| `--orange` / `--orange-dark` / `--orange-light` | `#ffba3a` / `#ff8338` / `#fffbe8` | `--su-color-warning` / `--su-color-orange-dark` / `--su-color-orange-light` | ✅ 本轮补齐后两项 |
|
||||
| `--green` | `#4abd86` | `--su-color-success` | ✅ |
|
||||
| `--gradient-red` | `linear-gradient(to right,#ff6034,#ff3e3e)` | `--su-gradient-red` | ✅ 本轮新增 |
|
||||
| `.item-badge` 置顶 / 付费 | `135deg #ff2f43→#ff6327` / `#ff952f→#ff662b` | `--su-gradient-hot` / `--su-gradient-paid` | ✅ 本轮新增 |
|
||||
| 认证角标 SVG | `#FFB74A → #FD900F` | `--su-gradient-verified` | ✅ |
|
||||
| `--border-radius-xs/sm/md/lg/max` | 4 / 6 / 12 / 24 / 999 | `--su-radius-*` | ✅ |
|
||||
| `--shadow` | `0 6px 16px 0 rgb(27 26 49 / 1%)` | `--su-shadow-card` | ⚠️ 原为 2%,**本轮改成 1%** |
|
||||
| `--shadow-2` | `rgb(27 26 49 / 3%)` | `--su-shadow-hover` | ⚠️ 原为 6%,**本轮改成 3%** |
|
||||
| `--border-color` / `--border-light` | `gray-2` / `gray-3` | `--su-border` / `--su-border-strong` | ✅ |
|
||||
| `--card-border-color` | `gray-1` | `--su-card-border` | ✅ 本轮新增(卡片描边几乎不可见) |
|
||||
| `--color-text` / `--color-secondary` | `gray-7` | `--su-text-secondary` | ✅ 本轮新增 |
|
||||
| `--color-muted` | `gray-6` | `--su-text-sub` | ✅(我们的 `text-muted` 保留 `gray-5` 给更弱的占位) |
|
||||
| `--bg-body` / `--card-bg` | `gray-1` / `#fff` | `--su-bg-page` / `--su-bg-card` | ✅ |
|
||||
| `--bg-light` | `gray-2` | `--su-bg-subtle`(本轮改为 gray-1) | ⚠️ 参考站标签用的是 `gray-1`,见 §6 |
|
||||
| `--animation-duration-fast/base` | `.2s` / `.3s` | `--su-duration-fast/base` | ✅ |
|
||||
| `--animation-timing-function-enter/leave` | `ease-out` / `ease-in` | `--su-ease-out` / `--su-ease-in` | ✅ |
|
||||
| 组件里的过渡 | 绝大多数是 `all .3s ease-in-out` | `--su-ease-in-out` | ✅ 本轮新增,组件过渡改用它 |
|
||||
|
||||
## 4. 字号与行高(最容易抄错的一处)
|
||||
|
||||
参考站 `:root` 里的 `--font-size-*`(10/12/14/16/24)**并不是页面实际字号**,
|
||||
真正生效的是 `.text-*` 工具类,且在小屏还有一组覆盖:
|
||||
|
||||
| 类 | 桌面 | 小屏(≤600px) | 典型用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `.text-xs` | **13 / 20** | 12 / 16 | 标签、元信息 |
|
||||
| `.text-sm` | **14 / 22** | — | 正文(侧栏简介) |
|
||||
| `.text-md` | **16 / 24** | 15 / 24 | 正文、条目摘要 |
|
||||
| `.text-lg` | **18 / 26** | — | 条目标题、卡片标题 |
|
||||
| `body` | **16 / 24** | — | 全局 |
|
||||
|
||||
`.item-badge` 是 12 / 14.4(700 字重),标签 `.tags-list a` 是 13 / 20。
|
||||
|
||||
我们的处理:把 `--su-font-size-*` 改成 12 / 13 / 14 / 18 / 24,行高 16 / 20 / 22 / 26 / 32;
|
||||
组件内文字只用到 `sm 13`(标签)、`md 14`(控件)、`lg 18`(图标),
|
||||
`xs 12` 给角标。**基础字号仍是 14**(见 §10 的保留差异)。
|
||||
|
||||
## 5. 布局
|
||||
|
||||
| 项 | 参考站实测 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 容器 | 1320 宽,居中,左右 60 | 对应 `--layout-max-width` 一类变量 |
|
||||
| 三栏 | 左 330 / 中 660 / 右 330(gutter 24) | `col-xl-3 / col-xl-6 / col-xl-3` |
|
||||
| 顶栏 | 80 高,透明叠在渐变背景上 | 导航项 48 高、字号 16 |
|
||||
| 内容起始 | y=112(顶栏 80 + 32) | 卡片与顶栏留 32 的呼吸 |
|
||||
| 卡片内边距 | `40px 24px`(侧栏 widget) / `12px`(缩略图卡片) | 竖向留白明显大于横向 |
|
||||
| 缩略图 | 3 列,间距 8,圆角 8 | 图集区块 |
|
||||
|
||||
这些是**页面级**参数,不进组件库;卡片的内边距档位(12 / 20 / 24)继续由 `Card padding` 管。
|
||||
|
||||
## 6. 组件实测(对齐依据)
|
||||
|
||||
| 组件 | 参考站选择器 | 实测关键值 | 我们的对应 |
|
||||
| --- | --- | --- | --- |
|
||||
| 卡片 | `.box-block` | `#fff`、radius 12、`1px solid gray-1`、shadow `0 6px 16px 1%`、padding `40/24`、过渡 opacity/transform .3s | `Card` |
|
||||
| 缩略图卡 | `.item` | `#fff`、radius 12、同款描边与阴影、padding 0(内容 12) | `Card padding="none"` |
|
||||
| 标签 | `.tags-list li a` | 高 24、`13/20`、radius 30、`gray-1` 底、`gray-6` 字、左侧 20×15 图标、hover 字色变 `gray-8`;hot 变体是 `rgba(255,94,68,.1)` 底 + 红字 | `Tag`(soft / solid / icon / dot) |
|
||||
| 状态角标 | `.item-badge` | `12/14.4`、700、padding `2px 4px`、radius 4、白字、红/橙渐变底 | `ItemBadge`(本轮新增) |
|
||||
| 数字角标 | `.btn-icon small` | 12px、padding `2px 5px`、radius 10、红底白字 | `Badge`(18 高) |
|
||||
| 头像 | `.flex-avatar` + `.avatar-badge` | 96 / 40 / 20 三档;角标是 SVG 渐变对勾,尺寸 12 / 16 / 20 | `Avatar`(24/36/48 + 16/20 角标) |
|
||||
| 图标按钮 | `.btn-icon` | 圆形,默认 42(变体 28 / 36 / 44),图标字号 16 / 20 / 24,无阴影 | `IconButton`(本轮改为圆形 28/36/44) |
|
||||
| 按钮 | `.btn` / `.btn-rounded` | radius 6 / 32、padding `8px 20px`、字号 14、`transition all .3s`、`primary:hover` 是 **opacity .8** | `Button`(新增 `shape="pill"`,hover 用 opacity) |
|
||||
| 输入框 | `.form-control` | 高 36、radius 6、padding `8px 14px`、`1px solid gray-3`、focus 换主色边框、无阴影 | `Input`(描边改 `gray-3`,focus 主色) |
|
||||
| 图片悬浮 | `.custom-hover` / `.hover-content` | 图片 `scale(1.1) rotate(2deg)`;遮罩 `rgba(0,0,0,.65)` 淡入、图标上移 10px | 组合模式(见 COMPONENTS.md §4) |
|
||||
| 分页 | `.page-numbers` | 32×32、radius 4、`gray-2` 底、当前项主色底白字 | 规划中(P3) |
|
||||
| 弹层 | `.nice-tips` | 遮罩 `rgba(0,0,0,.6)`、内容 radius 12、padding 28、`translateY(-40px)→0 .3s` | 规划中(P3 Modal) |
|
||||
| 滚动工具条 | `.scroll-toolbar` | `position:fixed; right:30; bottom:50%`,内部是 42px 圆形按钮 | `ScrollTop` |
|
||||
|
||||
## 7. 交互语言
|
||||
|
||||
| 行为 | 参考站做法 | 我们的做法 |
|
||||
| --- | --- | --- |
|
||||
| 颜色 / 背景过渡 | `all .3s ease-in-out`(部分图标 `color .3s`) | 具体属性 + `--su-duration-base` + `--su-ease-in-out` |
|
||||
| 主按钮 hover | `opacity: .8`(不改色) | ✅ 一致(primary / danger) |
|
||||
| 次要按钮 hover | 换 `gray-3` 底 | ✅ 一致 |
|
||||
| 图标按钮 hover | 轻微底色 + 文字加深,无阴影 | ✅ 一致 |
|
||||
| 卡片 hover | 部分列表 `translateY(-5px) scale(1.01)`;`hoverable` 只加深阴影 | 组件层只做阴影加深,位移留给页面(避免列表抖动) |
|
||||
| 图片 hover | 图片放大 1.1 + 旋转 2°,遮罩淡入 | 组合模式,不做进组件 |
|
||||
| 焦点 | 把 Bootstrap 的 `box-shadow` 去掉了(`box-shadow:none`) | **反着做**:补 2px 主色 focus ring(a11y 要求) |
|
||||
| 减少动效 | 用 `@media (not (prefers-reduced-motion))` 包裹动画 | ✅ 每个带动效的组件都有 `prefers-reduced-motion` 分支 |
|
||||
| 禁用 | 换浅底 + 弱文字(不是整体透明) | ✅ 一致(`bg-subtle` + `text-muted`) |
|
||||
| 加载 | `.btn.loading` 浅底 + `cursor: wait` + `pointer-events:none` | `loading` 自动 disabled + spinner + `aria-busy` |
|
||||
|
||||
## 8. 暗色方案(参考站已支持,我们留接口)
|
||||
|
||||
参考站用 `body[data-scheme="dark|auto"]` 覆盖整套变量,灰阶整体反相:
|
||||
|
||||
```css
|
||||
body[data-scheme=dark] {
|
||||
--gray-1:#f7f8fa; --gray-2:#f2f3f5; --gray-3:#e5e6eb; --gray-4:#c9cdd4;
|
||||
--gray-5:#494949; --gray-6:#303030; --gray-7:#202022; --gray-8:#0f0f11;
|
||||
--color-text:var(--gray-1); --color-secondary:var(--gray-2);
|
||||
--color-muted:var(--gray-4); --color-light:var(--gray-3);
|
||||
--bg-body:var(--black); --bg-dark:var(--gray-8); --bg-secondary:var(--gray-7);
|
||||
}
|
||||
```
|
||||
|
||||
要点:**语义变量跟着翻转,灰阶保持不变**——所以组件只要引用语义令牌就自动适配。
|
||||
我们的 `--su-*` 结构相同,后续加一份 `[data-scheme='dark']` 覆盖即可;
|
||||
仍有少量组件直接用了 `--su-gray-*`(Button secondary、Tag neutral、IconButton hover),
|
||||
深色主题落地时要把这些改成 `--su-bg-subtle` / `--su-bg-hover` 之类的语义令牌。
|
||||
|
||||
## 9. 本轮对齐清单
|
||||
|
||||
**令牌**([tokens.css](../src/theme/tokens.css))
|
||||
|
||||
- 阴影 2%/6% → **1%/3%**,补 `0 … 0` 的位置参数;
|
||||
- 字阶 10/12/14/16 → **12/13/14/18**,行高 16/20/22/26;
|
||||
- 新增:`--su-color-orange-dark/light`、`--su-gradient-hot/paid/red`、`--su-text-secondary`、
|
||||
`--su-bg-subtle`、`--su-card-border`、`--su-ease-in-out`、`--su-focus-ring(-offset)`;
|
||||
- 控件高度:`sm 28→32`;图标按钮 `24/32 → 28/36/44`。
|
||||
|
||||
**组件**
|
||||
|
||||
| 组件 | 变更 |
|
||||
| --- | --- |
|
||||
| `Button` | 新增 `shape="pill"`;hover 改 opacity .8;禁用改浅底弱字;补 focus ring;补 reduced-motion |
|
||||
| `IconButton` | 改圆形,尺寸 `xs/sm/md = 28/36/44`,图标 16/20/24;补 focus ring |
|
||||
| `Card` | 描边改 `--su-card-border`(gray-1),阴影用新令牌 |
|
||||
| `Tag` | 重写:默认 `neutral` 浅底胶囊 24 高 13px;新增 `variant="solid"`、`icon`、`dot`;保留 `shape="rounded"` 做元信息前缀 |
|
||||
| `Badge` | 高度 16→18,字号 10→12 |
|
||||
| `Input` | 描边 `gray-3`;focus 主色边框 + 键盘 focus ring(`:has(:focus-visible)`) |
|
||||
| `Avatar` | `lg` 认证角标 18→20,对齐参考站三档 |
|
||||
| `ScrollTop` | 过渡改 `ease-in-out`,补 focus ring |
|
||||
| `ItemBadge` | **新增**:置顶 / 付费 / HOT / 草稿 状态角标 |
|
||||
|
||||
**展示页**:Buttons 补「形状」与图标按钮三档;Marks 重构为「浅底 / 实底 / 状态角标 / 数字角标 / 组合」;
|
||||
组合示例的卡片标题加入 `ItemBadge`。
|
||||
|
||||
## 10. 保留的差异(有意为之)
|
||||
|
||||
| 差异 | 参考站 | 我们 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| 基础字号 | body 16/24 | 14/22 | 这是本地工具的控制台,不是阅读型博客;14 更紧凑。内容页(文章正文)建议用 16 |
|
||||
| 卡片内边距 | `40px 24px` | 12 / 20 / 24 档位 | 参考站的竖向留白是为 widget 设计的,工具里太松 |
|
||||
| 标签图标 | 每个标签强制带 20×15 图标 | 可选 `icon` | 图标是主题的分类素材,通用组件不该强绑 |
|
||||
| Bootstrap 组件 | 下拉、模态、分页都用 BS | 自己实现(规划中) | 不引 CSS 框架,避免 200KB+ 和样式覆盖战 |
|
||||
| hover 位移 | 部分列表卡片抬升 5px | 组件不做位移 | 列表里位移会造成抖动;需要时页面自己加 |
|
||||
| 动画时长 | 全站 `all .3s` | 按属性、默认 .3s | `all` 会把布局属性也带上,性能差 |
|
||||
|
||||
## 11. 生产标准补充(参考站没有的)
|
||||
|
||||
参考站是展示型主题,下面这些是我们作为组件库必须补的:
|
||||
|
||||
- **focus-visible**:所有可交互组件补 2px 主色焦点环(参考站把焦点样式删了);
|
||||
- **reduced-motion**:`Button` / `IconButton` / `Card` / `Input` / `ScrollTop` 都有降级分支;
|
||||
- **disabled 语义**:`Button` 用真实 `disabled`,`loading` 同时 `aria-busy`;
|
||||
- **图标按钮 label 必填**:`IconButton.label` 是必填 prop,防呆;
|
||||
- **错误提示 `role="alert"`**、`Empty` 与 `Notice` 分工(§COMPONENTS);
|
||||
- **键盘可达**:`ScrollTop` 隐藏时 `tabIndex=-1`;Input 清除按钮是真实 `button`;
|
||||
- **令牌约束**:组件 CSS 里没有裸色值(`DESIGN.md` §1 硬约束)。
|
||||
|
||||
## 12. 后续对齐项
|
||||
|
||||
1. ~~**暗色主题**~~:已完成(`data-scheme="dark|auto"`,见 DESIGN §7);
|
||||
2. **ScrollTop → 滚动工具条**:参考站是「点赞 / 收藏 / 回到顶部」三键竖排,等有收藏功能再做;
|
||||
3. **图片悬浮遮罩**:抽成组合模式(`Card hoverable` + 遮罩层),配合未来的 `Image` 组件;
|
||||
4. **分页 / 弹层**:按 COMPONENTS.md §5 的 P3 优先级补。
|
||||
@@ -0,0 +1,668 @@
|
||||
# 组件目录与契约
|
||||
|
||||
> 配套:[DESIGN.md](../DESIGN.md)(规则)、[ADDING-A-COMPONENT.md](ADDING-A-COMPONENT.md)(新增流程)、
|
||||
> [ALRIGHT-DECOMPOSITION.md](ALRIGHT-DECOMPOSITION.md)(参考站实测依据)
|
||||
|
||||
状态图例:✅ 已实现 · 🚧 开发中 · 🔜 规划 · ⛔ 不进库(用组合替代)
|
||||
|
||||
---
|
||||
|
||||
## 1. 组件地图
|
||||
|
||||
| 组件 | 层 | 状态 | 一句话 | 关键 props |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `Icon` | L1 | ✅ | 图标字体封装(446 个字形) | `name` |
|
||||
| `Badge` | L1 | ✅ | 数字角标(18 高,12px) | `count` `tone` `max` |
|
||||
| `ItemBadge` | L1 | ✅ | 状态角标:置顶 / 付费 / HOT | `tone` |
|
||||
| `Tag` | L1 | ✅ | 分类与元信息标签(24 高,13px) | `tone` `variant` `shape` `icon` `dot` |
|
||||
| `Divider` | L1 | ✅ | 分隔线 | `orientation` `variant` `spacing` |
|
||||
| `Typography` | L1 | ✅ | 标题 / 文字 / 段落(Title / Text / Paragraph) | `level` `tone` `size` `lines` |
|
||||
| `Button` | L2 | ✅ | 操作按钮(圆角 / 胶囊) | `variant` `size` `shape` `block` `loading` `icon` |
|
||||
| `IconButton` | L2 | ✅ | 圆形图标按钮(28 / 36 / 44) | `label`(必填) `icon` `variant` `size` `spinning` |
|
||||
| `Input` | L2 | ✅ | 组合式输入框 | `icon` `clearable` `onClear` `shortcut` `inputRef` |
|
||||
| `Switch` | L2 | ✅ | 即时生效的开关 | `checked` `onChange` `size` `label` |
|
||||
| `Checkbox` | L2 | ✅ | 复选框(多选 / 确认) | `checked` `onChange` `label` |
|
||||
| `RadioGroup` | L2 | ✅ | 互斥单选组 | `options` `value` `onChange` `direction` |
|
||||
| `Select` | L2 | ✅ | 受控下拉选择 | `options` `value` `onChange` `placeholder` |
|
||||
| `Segmented` | L2 | ✅ | 视图形态切换 | `options` `value` `onChange` `size` `block` |
|
||||
| `Tabs` | L2 | ✅ | 页内分区切换 | `items` `activeKey` `onChange` `variant` |
|
||||
| `Menu` | L2 | ✅ | 侧栏两级菜单(可折叠动画) | `items` `activeKey` `onSelect` `itemHeight` |
|
||||
| `Breadcrumb` | L3 | ✅ | 路径导航 | `items` `separator` |
|
||||
| `Layout` | L3 | ✅ | 页面骨架:页头 / 侧栏 / 内容 / 页脚 | `header` `sider` `aside` `maxWidth` `gap` `height` |
|
||||
| `Card` | L3 | ✅ | 白色容器(1px 极浅描边) | `as` `padding` `hoverable` |
|
||||
| `Cover` | L3 | ✅ | 比例封面卡(2:3 / 4:3 / 16:9 + 标题遮罩) | `ratio` `src` `caption` `lift` `onClick` |
|
||||
| `Image` | L3 | ✅ | 带比例 / 圆角 / 失败降级的图片 | `ratio` `fit` `radius` `fallback` |
|
||||
| `FormField` | L3 | ✅ | 表单字段排版(标签 + 控件 + 提示/错误) | `label` `hint` `error` `required` |
|
||||
| `Grid` | L3 | ✅ | 卡片列表网格 | `columns` `tabletColumns` `mobileColumns` `gap` `as` |
|
||||
| `Space` | L3 | ✅ | 一维间距容器 | `direction` `gap` `wrap` `align` `justify` |
|
||||
| `Table` | L3 | ✅ | 只读数据表格 | `columns` `rows` `rowKey` `emptyText` |
|
||||
| `Notice` | L4 | ✅ | 说明 / 错误提示条 | `tone` `title` `action` |
|
||||
| `Empty` | L4 | ✅ | 空态 | `text` `action` |
|
||||
| `Result` | L4 | ✅ | 整块结果反馈 | `status` `title` `description` `actions` |
|
||||
| `Modal` | L4 | ✅ | 居中弹窗(原生 dialog) | `open` `onClose` `title` `footer` `size` |
|
||||
| `Drawer` | L4 | ✅ | 侧滑抽屉(原生 dialog) | `open` `onClose` `title` `placement` `width` |
|
||||
| `Tooltip` | L4 | ✅ | hover / 聚焦文字提示 | `content` `placement` |
|
||||
| `Popover` | L4 | ✅ | 点击展开的小面板 | `content` `placement` |
|
||||
| `Toast` | L4 | ✅ | 命令式轻提示 | `toast()` `toast.success()` `<Toaster />` |
|
||||
| `Progress` | L4 | ✅ | 进度条 | `percent` `size` `tone` `showLabel` |
|
||||
| `Steps` | L4 | ✅ | 纵向步骤条 | `items` `current` |
|
||||
| `ScrollTop` | L4 | ✅ | 回到顶部 | `visible` `targetId` `label` |
|
||||
| `Pagination` | L3 | ✅ | 分页 | `page` `total` `pageSize` `onChange` |
|
||||
| `Skeleton` | L4 | ✅ | 骨架屏占位 | `variant` `lines` `width` `height` |
|
||||
| `Spinner` | L4 | ✅ | 加载指示(行内 / 区块) | `size` `label` |
|
||||
| `Avatar` | L1 | ✅ | 头像 / 封面占位(24 / 36 / 48 / 64) | `src` `fallback` `size` `shape` `tone` `verified` |
|
||||
|
||||
规划中的组件见 §5;明确不做的见 §6。
|
||||
|
||||
---
|
||||
|
||||
## 2. 用哪个?(决策表)
|
||||
|
||||
| 场景 | 用它 | 不要 |
|
||||
| --- | --- | --- |
|
||||
| 显示数量 | `Badge count={n}` | 用 `Tag` 包数字 |
|
||||
| 显示「置顶 / 付费 / HOT」 | `ItemBadge tone="sticky \| paid \| hot"` | 用 `Tag` 拼渐变 |
|
||||
| 分类、标签、元信息 | `Tag`(默认 neutral 浅底) | 自己写带底色的 span |
|
||||
| 需要强调的分类标签 | `Tag variant="solid"`(实底白字) | 给浅底标签调色值 |
|
||||
| 连接状态 | `Tag tone="success" dot` | 自绘圆点 |
|
||||
| 列表为空、搜索无结果 | `Empty`(可带 `action`) | 复用 `Notice` |
|
||||
| 接口失败、服务不可用 | `Notice tone="error"` | `Empty` |
|
||||
| 一般说明、引导 | `Notice`(`tone="info"`) | 直接写 p |
|
||||
| 一块内容 / 一个面板 | `Card`(`padding` 决定内边距) | 自己写白色 div |
|
||||
| 列表行(应用行、作者行) | 直接 `Space` + 内容 | **套 `Card`**(行不需要底色/边框/阴影) |
|
||||
| 多列排布 | `Grid` | 手写 `display:grid; repeat(...)` |
|
||||
| 一维间距 | `Space` | 每个元素写 margin |
|
||||
| 图标 | `<Icon name="…" />` | 写 `iconfont icon-xxx` 类名 |
|
||||
| 主操作 | `Button`(一页一个 primary) | 多个 primary 并列 |
|
||||
| 登录 / 筛选这类主操作 | `Button shape="pill"` | 圆角按钮凑合 |
|
||||
| 只有图标的操作 | `IconButton label="…"` | `<button><Icon /></button>` |
|
||||
| 分隔内容块 | `Divider` | `border-top` 手写 |
|
||||
| 长内容回到顶部 | `ScrollTop` | 自己写 fixed 按钮 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 现有组件契约
|
||||
|
||||
每个组件按固定结构写:用途 / 不该用在哪 / 解剖 / Props / 规则 / 展示页。新增组件照抄这个结构(模板见 §8)。
|
||||
|
||||
### 3.1 Icon ✅
|
||||
|
||||
- **用途**:把图标字体的字形封装成组件,颜色与字号继承当前文字。
|
||||
- **不该用在哪**:需要多色图标、需要 SVG 描边可控的场景。
|
||||
- **解剖**:单个 `<i class="su-iconfont su-icon-{name}">`,`aria-hidden="true"`。
|
||||
- **Props**:`name: IconName`(13 个字形)+ 原生 `<i>` 属性。
|
||||
- **规则**:图标一律视为装饰;字号在父级用 `font-size` 控制;新增字形走 §7 流程。
|
||||
- **展示页**:`#icons`
|
||||
|
||||
### 3.2 Badge ✅
|
||||
|
||||
- **用途**:数量角标(1、18、99+)。
|
||||
- **不该用在哪**:状态文字(用 `ItemBadge` 或 `Tag`)。
|
||||
- **解剖**:`inline-flex` 胶囊,18px 高、12px 字,等宽数字。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `count` | `number` | 必填 | 数字 |
|
||||
| `tone` | `'danger' \| 'primary' \| 'neutral'` | `'danger'` | 配色 |
|
||||
| `max` | `number` | `99` | 超过显示 `N+`;`0` 表示不截断 |
|
||||
|
||||
- **规则**:不负责定位;压在标题右上角由使用方用 `className` 绝对定位。
|
||||
- **展示页**:`#marks`
|
||||
|
||||
### 3.3 ItemBadge ✅
|
||||
|
||||
- **用途**:标题前的状态角标——「置顶 / 付费 / HOT / 草稿」。
|
||||
- **不该用在哪**:数量(`Badge`)、分类(`Tag`)。
|
||||
- **解剖**:18px 高、radius 4、12px 加粗白字、红/橙渐变底(参考站 `.item-badge`)。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `tone` | `'hot' \| 'sticky' \| 'paid' \| 'neutral'` | `'hot'` | hot/sticky 红渐变,paid 橙渐变,neutral 灰底 |
|
||||
| `children` | `ReactNode` | 必填 | 角标文字(建议 2–4 字) |
|
||||
|
||||
- **规则**:只做状态提示,不可点击;和标题并排时用 `Space gap="sm"`,不要加 margin。
|
||||
- **展示页**:`#marks`
|
||||
|
||||
### 3.4 Tag ✅
|
||||
|
||||
- **用途**:分类、标签、元信息。
|
||||
- **不该用在哪**:数量(`Badge`)、状态角标(`ItemBadge`)、可点击筛选(暂无交互态)。
|
||||
- **解剖**:24px 高胶囊、13px 字,左侧可带图标或状态点;实底变体用于强调。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `tone` | `'neutral' \| 'primary' \| 'success' \| 'warning' \| 'danger'` | `'neutral'` | 语义配色 |
|
||||
| `variant` | `'soft' \| 'solid'` | `'soft'` | 浅底 / 实底白字 |
|
||||
| `shape` | `'pill' \| 'rounded'` | `'pill'` | 胶囊 / 小方块(元信息前缀用) |
|
||||
| `icon` | `ReactNode` | — | 前置图标(如分类图标) |
|
||||
| `dot` | `boolean` | `false` | 前置 6px 状态点 |
|
||||
|
||||
- **规则**:标签是内容宽度;放进纵向 flex 时用 `align-items: flex-start`,否则会被拉满。
|
||||
- **展示页**:`#marks`
|
||||
|
||||
### 3.5 Divider ✅
|
||||
|
||||
- **用途**:横向分隔内容块、纵向分隔并排元素。
|
||||
- **解剖**:`role="separator"` 的普通 div,靠 border 画线。
|
||||
- **Props**:`orientation: 'horizontal' | 'vertical'`、`variant: 'solid' | 'dashed'`、`spacing: 'sm' | 'md'`。
|
||||
- **规则**:横向自带上下外边距(唯一带 margin 的基础组件),纵向用于 `Space` 内部。
|
||||
- **展示页**:`#layouts`
|
||||
|
||||
### 3.6 Button ✅
|
||||
|
||||
- **用途**:页面操作。
|
||||
- **不该用在哪**:只有图标(`IconButton`)、纯跳转链接(`<a>`)。
|
||||
- **解剖**:`[icon] [spinner] [label]`,高度 32 / 36,左右内边距 20。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `variant` | `'primary' \| 'secondary' \| 'outline' \| 'ghost' \| 'danger'` | `'primary'` | 语义 |
|
||||
| `size` | `'sm' \| 'md'` | `'md'` | 32 / 36px |
|
||||
| `shape` | `'rounded' \| 'pill'` | `'rounded'` | 6px 圆角 / 胶囊 |
|
||||
| `block` | `boolean` | `false` | 占满一行 |
|
||||
| `loading` | `boolean` | `false` | 自动禁用 + `aria-busy` |
|
||||
| `spinning` | `boolean` | `false` | 图标原地旋转(不换转圈),用于刷新类动作 |
|
||||
| `icon` | `ReactNode` | — | 文字左侧图标 |
|
||||
| `…rest` | `ButtonHTMLAttributes` | — | `type` 默认 `"button"` |
|
||||
|
||||
- **规则**:一屏一个 `primary`;hover 是 `opacity .8`(参考站同款),不要改背景色;禁用是浅底弱字。
|
||||
- **展示页**:`#buttons`
|
||||
|
||||
### 3.7 IconButton ✅
|
||||
|
||||
- **用途**:圆形图标按钮(顶栏入口、更多、刷新、关闭)。
|
||||
- **解剖**:正圆 + 居中图标;`spinning` 时图标旋转。
|
||||
- **Props**:`label`(**必填**,进 `aria-label` 与 `title`)、`icon`(必填)、
|
||||
`variant: 'ghost' | 'soft'`、`size: 'xs' | 'sm' | 'md'`(28 / 36 / 44,图标 16 / 20 / 24)、`spinning`、`…rest`。
|
||||
- **规则**:没有 `label` 不允许合并;`spinning` 只用于「操作进行中」,不做装饰动画。
|
||||
- **展示页**:`#buttons`
|
||||
|
||||
### 3.8 Input ✅
|
||||
|
||||
- **用途**:搜索框、筛选输入、表单单行输入。
|
||||
- **不该用在哪**:多行文本(未来 `Textarea`)、下拉选择(未来 `Select`)。
|
||||
- **解剖**:`[icon] [input] [shortcut] [clear]`,外层容器负责边框与高度(36px、`1px solid gray-3`)。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `icon` | `IconName` | — | 左侧图标 |
|
||||
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 32 / 36 / 44px |
|
||||
| `clearable` | `boolean` | `false` | 有值且传了 `onClear` 才显示清除 |
|
||||
| `onClear` | `() => void` | — | 清除回调 |
|
||||
| `shortcut` | `string` | — | 右侧提示(如 `⌘K`),仅展示 |
|
||||
| `inputRef` | `Ref<HTMLInputElement>` | — | 拿真实 input 做聚焦 |
|
||||
| `className` / `style` | — | — | **作用在外层容器** |
|
||||
| `…rest` | `InputHTMLAttributes` | — | `onChange` 是原生事件 |
|
||||
|
||||
- **规则**:与 `Button size="md"` 同高 36;focus 只把边框换成主色(**不再叠 outline**:文本框点击时也会命中 `:focus-visible`,叠环会出现「双框」,2026-10-28 修)。
|
||||
- **展示页**:`#inputs`
|
||||
|
||||
### 3.9 Card ✅
|
||||
|
||||
- **用途**:白色容器(面板、卡片、详情块)。
|
||||
- **不该用在哪**:列表行、纯排布容器。
|
||||
- **解剖**:`1px solid gray-1`(几乎不可见)+ radius 12 + `shadow-card`(1%),内边距由 `padding` 决定。
|
||||
- **Props**:`as: 'div' | 'section' | 'article' | 'aside'`、`padding: 'none' | 'sm' | 'md' | 'lg'`(0 / 12 / 20 / 24)、`hoverable`、`className`、`style`。
|
||||
- **规则**:卡片不提供标题栏;「色条 + 标题 + 角标 + 更多」由页面组合;`hoverable` 只加深阴影,不做位移。
|
||||
- **展示页**:`#cards`
|
||||
|
||||
### 3.10 Grid ✅
|
||||
|
||||
- **用途**:卡片列表的多列排布。
|
||||
- **不该用在哪**:一维间距(`Space`)、页面整体骨架(CSS layout)。
|
||||
- **解剖**:`display:grid`,列数走 CSS 变量,断点内建:`>1100`、`≤1100`、`≤720`。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `columns` | `number` | `4` | 宽屏列数 |
|
||||
| `tabletColumns` | `number` | `columns / 2`(至少 2) | ≤1100px |
|
||||
| `mobileColumns` | `number` | `1` | ≤720px |
|
||||
| `gap` | `'sm' \| 'md' \| 'lg'` | `'md'` | 8 / 12 / 16 |
|
||||
| `as` | `'div' \| 'ul'` | `'div'` | 列表语义 |
|
||||
|
||||
- **规则**:不要在父级用 `flex-direction: column` 包住含比例图片的 Grid(会被压扁,见 DESIGN §9)。
|
||||
- **展示页**:`#layouts`
|
||||
|
||||
### 3.11 Space ✅
|
||||
|
||||
- **用途**:一组元素之间的一维间距。
|
||||
- **解剖**:`display:flex` + `gap`,方向/对齐/间距都用语义档位。
|
||||
- **Props**:`direction: 'row' | 'column'`、`gap: 'xs' | 'sm' | 'md' | 'lg'`(4 / 8 / 12 / 16)、`wrap`、`align: 'start' | 'center' | 'end' | 'stretch'`、`justify: 'start' | 'center' | 'end' | 'between'`。
|
||||
- **规则**:间距只用档位,不传像素;纵向排列 + 左对齐要给 `align="start"`。
|
||||
- **展示页**:`#layouts`
|
||||
|
||||
### 3.12 Notice ✅
|
||||
|
||||
- **用途**:页面级说明与错误提示。
|
||||
- **不该用在哪**:表单字段校验、浮层提示(未来 `Toast`)。
|
||||
- **解剖**:标题 + 正文 + 可选操作按钮;错误态 `role="alert"`。
|
||||
- **Props**:`tone: 'info' | 'error'`、`title`(必填)、`children`、`action: { label, onClick }`。
|
||||
- **规则**:错误态只用于「确实失败」;同一区域不要堆两条以上提示。
|
||||
- **展示页**:`#feedback`
|
||||
|
||||
### 3.13 Empty ✅
|
||||
|
||||
- **用途**:列表为空、搜索无结果、内容未就绪。
|
||||
- **解剖**:一句说明 + 可选操作区。
|
||||
- **Props**:`text`(默认「暂无内容」)、`action`。
|
||||
- **规则**:空态不要用红色/错误色;能给出下一步操作时一定给。
|
||||
- **展示页**:`#feedback`
|
||||
|
||||
### 3.14 ScrollTop ✅
|
||||
|
||||
- **用途**:长内容回到顶部。
|
||||
- **解剖**:固定在视口右侧中部(`top:50%`、`right:24`)的 42px 圆钮,`visible` 控制淡入淡出。
|
||||
- **Props**:`visible`(必填)、`targetId`(滚动容器 id;不传滚窗口)、`label`(默认「回到顶部」)、`offset`(距右侧 px,默认 24)。
|
||||
- **规则**:滚动容器在页面里滚动时必须传 `targetId`;隐藏时 `tabIndex=-1`。
|
||||
- **展示页**:`#scrolltop`
|
||||
|
||||
### 3.15 Avatar ✅
|
||||
|
||||
- **用途**:头像、应用图标、封面占位。
|
||||
- **解剖**:裁切层(图片 / 首字) + 外置认证角标,两者分层,角标不会被圆角切掉。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src` | `string` | — | 图片;加载失败自动降级 |
|
||||
| `alt` | `string` | `''` | 图片替代文本 |
|
||||
| `fallback` | `string` | `'•'` | 无图时的字符(通常取首字) |
|
||||
| `icon` | `ReactNode` | — | 无图时的图标(优先于 fallback),用于缺省头像 |
|
||||
| `size` | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | 24 / 36 / 48 / 64px |
|
||||
| `shape` | `'circle' \| 'square'` | `'circle'` | 人/应用用圆,图标封面用方 |
|
||||
| `tone` | `'neutral' \| 'primary'` | `'neutral'` | 无图底色:浅灰 / 品牌浅渐变(资料卡) |
|
||||
| `verified` | `boolean` | `false` | 右下角认证角标(12 / 16 / 20px) |
|
||||
|
||||
- **规则**:`fallback` 传一个字符;资料卡用 `xl` + `tone="primary"`,列表里用 `lg`,紧凑行用 `sm`;不要给头像加边框。
|
||||
- **展示页**:`#avatars`
|
||||
|
||||
### 3.16 Modal ✅
|
||||
|
||||
- **用途**:需要打断流程的确认 / 表单 / 说明。
|
||||
- **不该用在哪**:轻量反馈(等 `Toast`)、页面内的展开收起(用条件渲染 + 动画)。
|
||||
- **解剖**:原生 `<dialog>`(焦点陷阱、Esc、背景 inert 由浏览器提供)+ 遮罩 + 标题栏 + 正文 + 底部操作区。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `open` | `boolean` | 必填 | 受控开关 |
|
||||
| `onClose` | `() => void` | 必填 | Esc / 遮罩 / 关闭按钮都会调用 |
|
||||
| `title` | `ReactNode` | 必填 | 标题,同时作为 `aria-labelledby` |
|
||||
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 300 / 460 / 640 |
|
||||
| `closeOnOverlay` | `boolean` | `true` | 点遮罩是否关闭 |
|
||||
| `footer` | `ReactNode` | — | 底部按钮区 |
|
||||
|
||||
- **规则**:打开时锁背景滚动;危险操作必须二次确认;底部按钮顺序是「取消在左、主操作在右」。
|
||||
- **展示页**:`#modal`
|
||||
|
||||
### 3.17 Pagination ✅
|
||||
|
||||
- **用途**:列表/搜索结果的翻页。
|
||||
- **解剖**:上一页 + 页码(超长时省略号折叠)+ 下一页;32×32、radius 4、当前页主色实底。
|
||||
- **Props**:`page`(从 1 开始)、`total`、`pageSize`(默认 10)、`onChange(page)`、`siblings`(当前页左右各几个,默认 1)。
|
||||
- **规则**:首尾页固定;边界上「上一页/下一页」禁用;总数文案由页面自己写(用 `Space justify="between"`)。
|
||||
- **展示页**:`#pagination`
|
||||
|
||||
### 3.18 Typography(Title / Text / Paragraph)✅
|
||||
|
||||
- **用途**:页面与内容排版——标题层级、文字层级、正文段落。
|
||||
- **不该用在哪**:控件内部的文字(按钮/输入自己管),不要为了「加个字号」而套一层 `Text`。
|
||||
- **解剖**:三个组件共用一个样式模块;`Title` 按 `level` 输出真实 `h1–h4`,`Text` 是 `span`/`p`/`div`,`Paragraph` 用 `-webkit-line-clamp` 做行数截断。
|
||||
- **Props**:
|
||||
|
||||
| 组件 | prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `Title` | `level` | `1 \| 2 \| 3 \| 4` | `2` | 24/32、18/26、16/24、14/22 |
|
||||
| `Title` | `weight` | `'normal' \| 'medium' \| 'semibold' \| 'strong'` | `'medium'` | 400 / 500 / 600 / 700;参考站的 600 用 `semibold` |
|
||||
| `Title` | `as` | `'h1' \| 'h2' \| 'h3' \| 'h4' \| 'div'` | 按 level | 换语义标签 |
|
||||
| `Text` | `tone` | `'main' \| 'secondary' \| 'sub' \| 'muted'` | `'main'` | 文字层级 |
|
||||
| `Text` | `size` | `'xs' \| 'sm' \| 'md' \| 'base' \| 'lg'` | `'md'` | 12 / 13 / 14 / 16 / 18 |
|
||||
| `Text` | `strong` / `truncate` | `boolean` | `false` | 500 字重 / 单行省略 |
|
||||
| `Paragraph` | `tone` | `'main' \| 'secondary' \| 'sub'` | `'sub'` | 文字层级 |
|
||||
| `Paragraph` | `size` | `'sm' \| 'md' \| 'base'` | `'base'` | 13 / 14 / 16,长文正文用 md |
|
||||
| `Paragraph` | `leading` | `'base' \| 'relaxed'` | `'base'` | relaxed = 1.9 行高(阅读型) |
|
||||
| `Paragraph` | `lines` | `number` | — | 最多几行,超出省略 |
|
||||
|
||||
- **规则**:组件不带外边距,段落间距交给 `Space`;不要在页面里另写 `font-size`。
|
||||
- **展示页**:`#typography`
|
||||
|
||||
### 3.19 Skeleton ✅
|
||||
|
||||
- **用途**:内容加载前的占位(列表首屏、刷新、分页加载)。
|
||||
- **不该用在哪**:按钮内的 loading(`Button` 自带 spinner)、整页空态(`Empty`)。
|
||||
- **解剖**:`text`(多行文本条,最后一行收短)/ `rect`(封面)/ `circle`(头像),占位块上有一层扫光。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `variant` | `'text' \| 'rect' \| 'circle'` | `'text'` | 形状 |
|
||||
| `lines` | `number` | `1` | 仅 text:行数 |
|
||||
| `width` / `height` | `number \| string` | — | rect 默认 100% / 120px,circle 默认 40 |
|
||||
| `className` / `style` | — | — | 外边距等布局微调 |
|
||||
|
||||
- **规则**:形状必须和真实内容一致(否则加载完成后跳动);整块区域标 `aria-busy="true"`;骨架自身 `aria-hidden`;`prefers-reduced-motion` 下停止扫光。
|
||||
- **展示页**:`#skeleton`
|
||||
|
||||
### 3.20 Spinner ✅
|
||||
|
||||
- **用途**:行内或区块的加载指示(同步状态、面板刷新)。
|
||||
- **不该用在哪**:按钮内(`Button loading` 自带)、文字占位(`Skeleton`)。
|
||||
- **解剖**:2px 圆环,颜色取主色,`role="status"` + `aria-label`。
|
||||
- **Props**:`size: 'sm' | 'md'`(14 / 20)、`label`(默认「加载中」)、`className`。
|
||||
- **规则**:颜色继承 `currentColor`(默认主色,可被外层覆盖);`prefers-reduced-motion` 下降到 2.4s。
|
||||
- **展示页**:`#spinner`
|
||||
|
||||
### 3.21 Switch ✅
|
||||
|
||||
- **用途**:设置项的**即时**开关。
|
||||
- **不该用在哪**:需要点「保存」才生效的表单(用 `Checkbox` + 提交按钮)。
|
||||
- **解剖**:原生 `button` + `role="switch"`,轨道 + 白色圆点;空格/回车可切换。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `checked` | `boolean` | 必填 | 受控状态 |
|
||||
| `onChange` | `(checked: boolean) => void` | 必填 | 切换回调 |
|
||||
| `size` | `'sm' \| 'md'` | `'md'` | 32×18 / 40×22 |
|
||||
| `disabled` | `boolean` | `false` | 禁用 |
|
||||
| `label` | `string` | — | 无障碍名称:旁边没有文字时必填 |
|
||||
|
||||
- **规则**:旁边配文字时用 `Text` + `Space justify="between"`,不要自己做一套行布局。
|
||||
- **展示页**:`#switch`
|
||||
|
||||
### 3.22 Checkbox ✅
|
||||
|
||||
- **用途**:多选、需要确认的选项。
|
||||
- **解剖**:原生 `input[type=checkbox]`(视觉隐藏)+ 自绘 16px 方块 + label 文本,点击文字也能勾选。
|
||||
- **Props**:`checked`、`onChange(checked)`、`label`(必填,同时作为无障碍名称)、`disabled`。
|
||||
- **规则**:多选组用 `Space direction="column"` 排列;禁用态只降对比,不隐藏。
|
||||
- **展示页**:`#checkbox`
|
||||
|
||||
### 3.23 RadioGroup ✅
|
||||
|
||||
- **用途**:互斥选项组。
|
||||
- **解剖**:原生 `input[type=radio]`(隐藏)+ 自绘圆点;同组共享 `name`,方向键在组内移动。
|
||||
- **Props**:`options: { value, label, disabled? }[]`、`value`、`onChange(value)`、`direction: 'row' | 'column'`、`disabled`。
|
||||
- **规则**:2–3 个短选项用 row;设置类用 column。单个开/关用 `Switch`,多选用 `Checkbox`。
|
||||
- **展示页**:`#radio`
|
||||
|
||||
### 3.24 Segmented ✅
|
||||
|
||||
- **用途**:视图形态切换(网格/列表、全部/收藏)。
|
||||
- **Props**:`options: { value, label, icon? }[]`、`value`、`onChange(value)`、`size: 'sm' | 'md'`、`block`。
|
||||
- **规则**:只切展示、不切内容分区(后者用 `Tabs`);选中项白底 + 轻阴影。
|
||||
- **展示页**:`#segmented`
|
||||
|
||||
### 3.25 Select ✅
|
||||
|
||||
- **用途**:受控的单值下拉选择。
|
||||
- **解剖**:触发器是原生 button(`aria-haspopup="listbox"`),展开为 `role="listbox"`;点外部 / Esc 关闭,↑↓ 移动、Enter 选择。
|
||||
- **Props**:`options`、`value`、`onChange(value)`、`placeholder`、`disabled`。
|
||||
- **规则**:多选与搜索不在范围内(用 `Input` + 列表组合);高度与 `Input` 一致(36)。
|
||||
- **展示页**:`#select`
|
||||
|
||||
### 3.26 Breadcrumb ✅
|
||||
|
||||
- **用途**:详情页路径导航。
|
||||
- **Props**:`items: { label, href? }[]`、`separator`(默认 `/`)。
|
||||
- **规则**:最后一项忽略 `href`,并标 `aria-current="page"`。
|
||||
- **展示页**:`#breadcrumb`
|
||||
|
||||
### 3.27 Tabs ✅
|
||||
|
||||
- **用途**:页内内容分区切换。
|
||||
- **Props**:`items: { key, label, disabled? }[]`、`activeKey`、`onChange(key)`、`variant: 'line' | 'pill'`。
|
||||
- **规则**:只渲染标签栏,面板由页面用 `role="tabpanel"` 渲染;← → 移动、Home/End 跳首尾。
|
||||
- **展示页**:`#tabs`
|
||||
|
||||
### 3.28 Menu ✅
|
||||
|
||||
- **用途**:侧栏菜单(一层折叠 + 两级)。
|
||||
- **Props**:`items: { key, label, icon?, children? }[]`、`activeKey`、`onSelect(key)`、`defaultOpenKeys`、`itemHeight`(一级行高,默认 36)。
|
||||
- **规则**:数据无关,业务树由页面转换;含 `activeKey` 的父级默认展开,父子均用 `aria-expanded` / `aria-current` 标注;折叠箭头是 CSS chevron(视觉回归:从字形改为边框绘制)。
|
||||
- **展示页**:`#menu`
|
||||
|
||||
### 3.29 Toast(toast / Toaster)✅
|
||||
|
||||
- **用途**:操作反馈,几秒后自动消失。
|
||||
- **解剖**:模块级 store + `useSyncExternalStore`;`Toaster` 挂在应用根节点,只挂一次。
|
||||
- **API**:`toast(message, { tone?, duration? })`、`toast.success / .error`、`dismissToast(id)`。
|
||||
- **规则**:命令式调用;页面内常驻提示用 `Notice`,整块状态用 `Result`;文案一句话,不放按钮。
|
||||
- **展示页**:`#toast`
|
||||
|
||||
### 3.30 Table ✅
|
||||
|
||||
- **用途**:数据表格(只读)。
|
||||
- **Props**:`columns: { key, title, width?, align? }[]`、`rows: Record<string, ReactNode>[]`、`rowKey`(默认 `id`)、`emptyText`。
|
||||
- **规则**:排序 / 筛选 / 分页由页面组合,组件不内置交互;空数据渲染 `emptyText`。
|
||||
- **展示页**:`#table`
|
||||
|
||||
### 3.31 Result ✅
|
||||
|
||||
- **用途**:整块的完成 / 失败 / 警示 / 说明反馈。
|
||||
- **Props**:`status: 'success' | 'error' | 'warning' | 'info'`、`title`、`description`、`actions`。
|
||||
- **规则**:只用于整页或整块状态;行内提示用 `Notice`,轻量反馈用 `toast`。
|
||||
- **展示页**:`#result`
|
||||
|
||||
### 3.32 Progress ✅
|
||||
|
||||
- **用途**:任务 / 存储进度。
|
||||
- **Props**:`percent`(夹在 0–100)、`size: 'sm' | 'md'`(4 / 8px)、`tone`、`showLabel`。
|
||||
- **规则**:纯展示;带 `role="progressbar"` 与 `aria-valuenow`。
|
||||
- **展示页**:`#progress`
|
||||
|
||||
### 3.33 Steps ✅
|
||||
|
||||
- **用途**:有顺序的流程(发布、导入向导)。
|
||||
- **Props**:`items: { title, description? }[]`、`current`(从 0 开始)。
|
||||
- **规则**:`index < current` 视为已完成;当前项加粗 + 主色描边。
|
||||
- **展示页**:`#steps`
|
||||
|
||||
### 3.34 Drawer ✅
|
||||
|
||||
- **用途**:侧滑面板(筛选、设置、详情),不打断主流程。
|
||||
- **解剖**:与 `Modal` 同源(原生 `<dialog>`),区别是贴边、全高、滑入动画。
|
||||
- **Props**:`open`、`onClose`、`title`、`placement: 'right' | 'left'`、`width`(默认 360)、`footer`。
|
||||
- **规则**:需要打断流程的确认用 `Modal`;打开时锁背景滚动。
|
||||
- **展示页**:`#drawer`
|
||||
|
||||
### 3.35 Tooltip ✅
|
||||
|
||||
- **用途**:hover / 聚焦时的一行文字说明。
|
||||
- **Props**:`content`、`placement: 'top' | 'bottom'`、`children`(建议可聚焦元素)。
|
||||
- **规则**:纯 CSS 定位,不做自动翻转;交互内容用 `Popover`。
|
||||
- **展示页**:`#tooltip`
|
||||
|
||||
### 3.36 Popover ✅
|
||||
|
||||
- **用途**:点击展开的小面板(说明、快捷操作)。
|
||||
- **Props**:`content`、`children`、`placement: 'bottom-start' | 'bottom-end'`。
|
||||
- **规则**:点外部 / Esc 关闭;内容复杂或需要滚动时改用 `Drawer` / `Modal`。
|
||||
- **展示页**:`#tooltip`
|
||||
|
||||
### 3.37 Layout ✅
|
||||
|
||||
- **用途**:页面外壳——页头 / 侧栏 / 内容 / 右栏 / 页脚的区域划分与最大宽度。
|
||||
- **不该用在哪**:应用内的小区块(用 `Card` + `Grid`);业务导航内容(由页面作为 slot 传入)。
|
||||
- **解剖**:`flex` 纵向三行(header / body / footer),body 内是 `sider + main + aside`;main 里再套一层限宽容器(`maxWidth`)。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `header` | `ReactNode` | — | 页头,默认吸顶(半透明 + 模糊) |
|
||||
| `sider` | `ReactNode` | — | 左侧栏,固定宽度 |
|
||||
| `aside` | `ReactNode` | — | 右侧栏,固定宽度(目录 / 聚合 / 广告位) |
|
||||
| `footer` | `ReactNode` | — | 页脚 |
|
||||
| `children` | `ReactNode` | 必填 | 内容区 |
|
||||
| `maxWidth` | `number \| 'full'` | `1280` | 内容最大宽度 |
|
||||
| `siderWidth` | `number` | `260` | 左侧栏宽度 |
|
||||
| `asideWidth` | `number` | `300` | 右侧栏宽度 |
|
||||
| `stickyHeader` | `boolean` | `true` | 页头是否吸顶 |
|
||||
| `contained` | `boolean` | `false` | 页身套一层居中容器(**页头始终通栏**,页头内容自己限宽) |
|
||||
| `gap` | `number` | `0` | 侧栏 / 内容 / 右栏之间的间距(px) |
|
||||
| `contentPadding` | `'none' \| 'sm' \| 'md' \| 'lg'` | `'md'` | 内容区留白:0 / 12 / 20 / 24 |
|
||||
| `contentOffset` | `number` | `0` | 内容区顶部偏移(px),给页头下的悬浮搜索/工具条留位 |
|
||||
| `gutter` | `number` | `0` | contained 容器的左右留白(px) |
|
||||
| `headerHeight` | `number` | `60` | 页头最小高度(px) |
|
||||
| `headerVariant` | `'blur' \| 'solid'` | `'blur'` | 半透明模糊 / 实底 + 下边框 |
|
||||
| `siderBordered` | `boolean` | `true` | 侧栏右侧分隔线 |
|
||||
| `height` | `'auto' \| 'screen'` | `'auto'` | screen:锁视口,适合应用型界面 |
|
||||
| `scrollable` | `boolean` | `false` | 内容区内部滚动(配 `height="screen"`) |
|
||||
| `hideScrollbar` | `boolean` | `false` | 内部滚动时隐藏滚动条(仍可滚轮/键盘滚动) |
|
||||
| `contentId` | `string` | — | 内容区 id(供 ScrollTop 等按 id 滚动) |
|
||||
|
||||
- **规则**:Layout 不写业务内容,也不带背景装饰(需要渐变/光晕时由页面自己放绝对定位层);响应式在组件内处理——≤1200px 右栏堆到内容下方、≤1100px 侧栏堆到内容上方,**内容都不隐藏**。
|
||||
- **常用配方**:顶栏+内容(首页)、左栏+内容(管理页)、内容+右栏(文章)、三栏(工作台)、窄内容(设置/阅读),展示页用 `Segmented` 切换。
|
||||
- **展示页**:`#layout`
|
||||
|
||||
### 3.38 Cover ✅
|
||||
|
||||
- **用途**:比例封面卡——海报(2:3)、图文(4:3)、宽图(16:9)统一用「图片 + 底部压暗层 + 标题」。
|
||||
- **不该用在哪**:需要叠加角标 / 操作按钮的封面(用 `Card padding="none"` 里放 `Cover` + 角标);纯头像用 `Avatar`。
|
||||
- **解剖**:`aspect-ratio` 容器 + `object-fit: cover` 图片 + `--su-cover-scrim` 渐变遮罩;传 `onClick` 时整块变成 `<button>`,否则是 `<figure>`。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `src` | `string` | — | 图片地址;不传时显示占位字符 |
|
||||
| `alt` | `string` | 必填 | 图片说明,同时作为按钮 title |
|
||||
| `caption` | `ReactNode` | — | 底部标题(最多 2 行) |
|
||||
| `ratio` | `'poster' \| 'article' \| 'wide' \| number` | `'article'` | 2/3、4/3、16/9,或数字宽高比 |
|
||||
| `radius` | `'cover' \| 'none'` | `'cover'` | 8px / 贴边(配合 `Card padding="none"`) |
|
||||
| `lift` | `boolean` | `true` | 悬停整块上浮 2px(参考站行为) |
|
||||
| `hoverZoom` | `boolean` | `false` | 悬停放大图片 1.04(与 lift 二选一) |
|
||||
| `onClick` | `() => void` | — | 传了就把整块变成按钮 |
|
||||
|
||||
- **规则**:封面比例由 `ratio` 定,不要在页面里再写 `aspect-ratio`;标题必须压暗层,否则白字会丢。
|
||||
- **展示页**:`#cover`
|
||||
|
||||
### 3.39 Image ✅
|
||||
|
||||
- **用途**:正文配图、相册、缩略图——带比例、圆角、加载失败降级。
|
||||
- **不该用在哪**:带标题遮罩的封面卡(用 `Cover`);头像(用 `Avatar`)。
|
||||
- **Props**:`src`、`alt`、`ratio: 'square' | 'video' | 'wide' | number`(默认 `video`)、
|
||||
`fit: 'cover' | 'contain'`(默认 `cover`)、`radius: 'none' | 'sm' | 'md' | 'cover'`(默认 `sm`)、`fallback`。
|
||||
- **规则**:必须给 `alt`;失败自动退化为占位块,不出现破图;懒加载默认开启。
|
||||
- **展示页**:`#image`
|
||||
|
||||
### 3.40 FormField ✅
|
||||
|
||||
- **用途**:表单字段的排版——标签 + 控件 + 提示 / 错误。
|
||||
- **不该用在哪**:**不做校验**(校验留在页面);不是表单容器(多个字段用 `Space` 排列)。
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `label` | `ReactNode` | 必填 | 字段名(渲染为 `<label>`) |
|
||||
| `htmlFor` | `string` | — | 关联控件 id |
|
||||
| `hint` | `ReactNode` | — | 说明文字,有错误时隐藏 |
|
||||
| `error` | `ReactNode` | — | 错误文案,出现时标红并 `role="alert"` |
|
||||
| `required` | `boolean` | `false` | 显示红色 `*` |
|
||||
| `children` | `ReactNode` | 必填 | 控件(`Input` / `Select` / `Switch`…) |
|
||||
|
||||
- **规则**:错误用文案而不是只换颜色(颜色不足以表达);`defaultValue` / `value` 等仍由控件自己管。
|
||||
- **展示页**:`#inputs`
|
||||
|
||||
---
|
||||
|
||||
## 4. 组合模式(不进库,但必须照这个拼)
|
||||
|
||||
| 模式 | 由什么拼 | 展示页 |
|
||||
| --- | --- | --- |
|
||||
| 分区卡片(首页一块) | `Card` + 色条 span + `ItemBadge` + 标题 + `Badge` + `IconButton` + `Grid` | `#composition` |
|
||||
| 应用行 | `Avatar` + 两行文字(名称 / 说明),无容器 | `#avatars`、`#composition` |
|
||||
| 筛选工具条 | `Card padding="sm"` + `Space justify="between"` + `Input` + `Tag` + `Button shape="pill"` | `#composition` |
|
||||
| 海报卡 | `Card padding="none"` + 比例封面 + 标题 + `Tag` | `#composition` |
|
||||
| 顶栏信息条 | `Space` + `Tag shape="rounded"` + 文字 | `#marks` |
|
||||
| 标题行 | `ItemBadge` + 标题 + `Tag` | `#marks` |
|
||||
| 图片悬浮(参考站 `.custom-hover`) | 图片容器 `overflow:hidden` + hover 时 `scale(1.1)` + 65% 黑遮罩淡入 | 未实现(等 `Image` 组件) |
|
||||
|
||||
这些结构的代码在 [`site/sections/Composition.tsx`](../site/sections/Composition.tsx),做页面时直接复制再换数据。
|
||||
|
||||
---
|
||||
|
||||
## 5. 规划中的组件
|
||||
|
||||
原计划的 14 个(Radio → Drawer)已全部落地,当前**没有待实现的组件**。
|
||||
|
||||
下一批(还没进计划,等应用侧提出真实需求再定):
|
||||
|
||||
| 候选 | 触发条件 | 注意点 |
|
||||
| --- | --- | --- |
|
||||
| `Image`(比例封面 + 悬浮遮罩) | 应用里有 3 处以上重复的封面实现 | 自带 `aspect-ratio`、懒加载、失败占位 |
|
||||
| `Form` / `FormItem` | 出现第一个带校验的表单 | 只做布局与错误文案,校验逻辑交给页面 |
|
||||
| `DatePicker` | 需要按日期筛选 | 依赖 Popover 定位,建议直接用原生 `input[type=date]` |
|
||||
| `Upload` | 需要拖拽上传 | 依赖接口,先做成受控壳 |
|
||||
| `Layout` 多侧栏 / 断点可配 | 同一页面需要两个左栏,或要自定义折叠断点 | 现在固定「一个 sider + 一个 aside」;扩展时用 `columns` 数组而不是再加 slot |
|
||||
|
||||
---
|
||||
|
||||
## 6. 明确不进库
|
||||
|
||||
| 东西 | 原因 | 替代 |
|
||||
| --- | --- | --- |
|
||||
| 分区卡片、应用条目、海报卡、图文卡 | 业务组件,字段随需求变 | 用 `Card` + `Grid` + `Avatar` + `Tag` 组合(§4) |
|
||||
| Header / Sidebar / 页面骨架 | 页面级布局 | 放在应用里 |
|
||||
| 24 栅格 Layout(`Row`/`Col`) | 只用得到 `Grid` | `Grid` + `Space` + CSS Grid |
|
||||
| 全量图标库 | 字体 500+ 字形,体积无意义 | 按需加字形(§7) |
|
||||
| 富文本编辑器、图表、日期面板 | 不是设计系统职责 | 引专用库 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 新增图标字形的流程
|
||||
|
||||
1. 从参考站 iconfont 复制字形码(`\eXXX`);
|
||||
2. `src/theme/iconfont.css` 加一条 `.su-icon-<name>::before { content: '\eXXX'; }`;
|
||||
3. `src/components/Icon.tsx` 的 `IconName` 联合类型加 `'<name>'`;
|
||||
4. 展示页 `site/sections/Icons.tsx` 的 `ICON_NAMES` 加一项;
|
||||
5. 字形不在现有 woff2 里时,替换 `src/theme/assets/iconfont.woff2` 并重新构建;
|
||||
6. 跑一次 `npm run build` 确认 CSS 里字体仍是内联的。
|
||||
|
||||
---
|
||||
|
||||
## 8. 契约模板(新增组件时复制到 §3)
|
||||
|
||||
```markdown
|
||||
### Xxx 🚧
|
||||
|
||||
- **用途**:
|
||||
- **不该用在哪**:
|
||||
- **解剖**:(结构描述:几层、每个部分的职责)
|
||||
- **Props**:
|
||||
|
||||
| prop | 类型 | 默认 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| — | — | — | — |
|
||||
|
||||
- **变体与状态**:(variant / size / 状态矩阵中本组件需要覆盖哪些)
|
||||
- **规则**:(组合禁忌、尺寸对齐、a11y 要求)
|
||||
- **展示页**:`#anchor`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
| --- | --- |
|
||||
| 2026-10-26 | 初版:14 个基础组件(Icon / Badge / Tag / Divider / Button / IconButton / Input / Card / Grid / Space / Notice / Empty / ScrollTop / Avatar) |
|
||||
| 2026-10-26 | 补齐令牌:`danger-hover`、`warning-text`、`bg-notice`、`bg-danger-soft`、`gradient-verified`、`font-weight-strong`、`control-height-*`、`icon-button-*` |
|
||||
| 2026-10-26 | **对齐参考站**:阴影 1%/3%、字阶 12/13/14/18、`Tag` 重写(soft/solid/icon/dot)、新增 `ItemBadge`、`Button` 加 `shape`、`IconButton` 改圆形 28/36/44、`Card` 描边改 gray-1、`Input` 描边改 gray-3、补 focus ring 与 reduced-motion |
|
||||
| 2026-10-26 | 深色主题(`data-scheme`)+ `Button variant="outline"` + `bg-light` 语义令牌;新增整页拼装示例 `site/demo.html` |
|
||||
| 2026-10-27 | 图标改用参考站全量 iconfont(446 个字形,`IconName` 自动生成);新增 `Modal`、`Pagination`;示例页加入删除确认弹窗与分页 |
|
||||
| 2026-10-27 | 新增 `Typography`(Title / Text / Paragraph)与 `Skeleton`;补 `--su-font-size-base`(16/24)与 `--su-skeleton-shine`;示例页改用排版组件并在刷新时显示骨架屏 |
|
||||
| 2026-10-27 | 新增 `Spinner`、`Switch`、`Checkbox`(表单基础收口);示例页左栏加入「偏好」卡片(开关控制封面显示、复选框、同步中的 Spinner) |
|
||||
| 2026-10-27 | 一次补齐剩余 14 个组件:`RadioGroup`、`Segmented`、`Select`、`Breadcrumb`、`Tabs`、`Menu`、`Toast`、`Table`、`Result`、`Progress`、`Steps`、`Drawer`、`Tooltip`、`Popover`;示例页接入面包屑、下拉、抽屉与 toast |
|
||||
| 2026-10-27 | 新增 `Layout`(页面骨架:页头 / 侧栏 / 内容 / 页脚),组件总数 37 |
|
||||
| 2026-10-28 | `Layout` 增加 `aside` / `asideWidth` 右栏,形成三栏骨架;展示页改为 5 个配方切换;补移动端适配(`dvh`、触屏 hover 隔离、分段/弹层夹宽、演示页导航抽屉) |
|
||||
| 2026-10-28 | 为对齐 stash 原设计补充通用能力:`Layout` 增加 `gap` / `contentPadding` / `gutter` / `headerHeight` / `headerVariant` / `siderBordered` / `height="screen"` + `scrollable` + `contentId`;新增 `Cover`;`Avatar` 增加 `xl` 与 `tone`;`Paragraph` 增加 `size` / `leading`;新增布局类令牌(`--su-layout-width`、`--su-header-height`、`--su-reading-line-height`、`--su-radius-cover`、`--su-shadow-card-strong`、`--su-cover-scrim`) |
|
||||
| 2026-10-28 | **视觉回归**:`Layout` 的 `contained` 改为只包页身,页头通栏;`screen + scrollable` 修复内容区高度未约束导致无法滚动;新增 `contentOffset`。`Menu` 增加 `itemHeight`、字重 500、圆角 8、chevron 改为 CSS 绘制,列表间距 2→4。`Avatar` 增加 `icon`。影响面:所有用到 `Menu` / `Layout contained` 的页面都需要重截图 |
|
||||
| 2026-10-28 | **1:1 补全(视觉 + 交互)**:`Title` 增加 `weight`(600);`Input` 增加 `size`(44);`Button` 增加 `spinning`(图标原地旋转);`Cover` 改为 hover 上浮 2px(`lift`,默认开;`hoverZoom` 默认关);`ScrollTop` 增加 `offset`;`Menu` 的 hover 改主色、子菜单改 `grid-template-rows` 高度动画(0.28s);`Avatar xl` 徽标 20px、无图底色用新令牌 `--su-avatar-gradient`。**影响面**:`Cover`/`Menu`/`Avatar`/`Title` 的所有展示页 demo 与应用页面需重截图 |
|
||||
| 2026-10-28 | **修复**:`Input` 聚焦不再叠 outline(文本框点击会命中 `:focus-visible`,导致边框 + 外环「双框」;现在只用边框变主色)。`Layout` 增加 `hideScrollbar`(内部滚动条可隐藏)。影响面:所有 `Input` 页面视觉(去掉了多余外环)、`Layout screen` 页面可选用 |
|
||||
| 2026-10-28 | **补齐剩余能力**:新增 `Image`(比例 / 填充 / 失败降级)与 `FormField`(标签 + 提示 + 错误,不做校验);`Button` 增加 `size="lg"`(44);展示页补 `Title weight`、`Avatar xl/tone/icon`、`Cover lift`、`Menu itemHeight`、`Layout screen/hideScrollbar`、`Input size`、`FormField` 示例;新增 `themes/example.css` 主题示例(只改变量) |
|
||||
@@ -0,0 +1,88 @@
|
||||
# 外部风格接入(截图 → 组件/令牌)
|
||||
|
||||
> 用在「使用方给了一张别的网站截图,风格和 stash-ui 不一致」的场景。
|
||||
> 目的:**先对齐方案,再动代码**;把「能吸收的」吸进库,把「只能页面私有的」明确记成例外。
|
||||
> 规则来源:[DESIGN.md §11](../DESIGN.md)。
|
||||
|
||||
## 0. 一句话原则
|
||||
|
||||
截图只提供**意图**,不提供实现。先把它翻译成「骨架 / 组件 / 形态值 / 交互」,
|
||||
再决定每一项落在哪一层:主题变量、组件参数、组件变体、新组件、页面私有。
|
||||
|
||||
## 1. 接入单(收到截图后先填)
|
||||
|
||||
```md
|
||||
### 项目:<名称> 来源:<截图/URL> 日期:<日期>
|
||||
|
||||
1. 布局骨架:<几栏、页头/侧栏/底栏、最大宽度、断点>
|
||||
2. 组件清单:<截图里出现的每个控件,按出现顺序>
|
||||
3. 形态值:<主色、圆角、阴影、控件高度、字号阶梯、间距节奏>
|
||||
4. 交互:<hover / 展开 / 切换 / 空态 / 加载>
|
||||
5. 范围:<整站统一 / 只此一页 / 只此一个组件>
|
||||
```
|
||||
|
||||
## 2. 映射表(核心产物)
|
||||
|
||||
| # | 截图里的东西 | 库里的对应 | 结论 | 证据(值) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | 顶栏 64px 白底 | `Layout headerVariant="solid"` + `headerHeight` | ✅ 参数可调 | 截图 64,库默认 60 |
|
||||
| 2 | 主色 #6C5CE7 | `--su-color-primary` 主题覆盖 | ✅ 主题 | 只改变量 |
|
||||
| 3 | 按钮 48 高、10 圆角 | `Button` 只有 32/36/44 | ⚠️ 缺尺寸档 | 需新增 `size="lg"`(40/48) |
|
||||
| 4 | 卡片 16 圆角 | `--su-radius-md` 12 | ⚠️ 缺档 | 主题覆盖不了,需 `radius` 档或新令牌 |
|
||||
| 5 | 瀑布流列表 | 无 | ❌ 新组件 | 判定:是否 ≥2 处会用 → `Masonry` |
|
||||
| 6 | 封面左上角角标 | `Cover` + 绝对定位 | ⚠️ 组合即可 | 不需要新组件,写组合模式 |
|
||||
| 7 | 背景光晕 | 无 | 🟡 页面私有 | 一次性装饰,页面 CSS |
|
||||
|
||||
图例:✅ 现有能力(调参/主题)|⚠️ 需要参数化或加变体|❌ 需要新组件|🟡 页面私有
|
||||
|
||||
## 3. 决策:先问三个问题
|
||||
|
||||
1. **换主题能不能解决?** 只是颜色/字体 → `themes/<name>.css` 改变量,不动组件。
|
||||
2. **同结构、不同形态或交互?** → 给现有组件加 `size` / `variant` / `radius` 之类的受控参数。
|
||||
3. **结构不一样?** → 评估新组件:
|
||||
- 会不会在 ≥2 个页面/使用方出现?
|
||||
- 能不能用 props 表达,不夹带业务字段?
|
||||
- 两条都满足才进库,否则先做页面私有实现,并记一笔「候选组件」。
|
||||
|
||||
## 4. 评审要点(给使用方确认的最小信息)
|
||||
|
||||
- **差异表**:每一项「截图值 / 库值 / 结论」;
|
||||
- **API 草案**:新增/修改的 props(只写名字、类型、默认值,不写实现);
|
||||
- **影响面**:会不会改现有组件默认值(破坏性)、要不要同步文档与展示页;
|
||||
- **兼容性提示**(改共享组件必填):
|
||||
- 调用方清单(哪些页面/组件在用);
|
||||
- 变更性质:新增可选 prop(兼容)/ 改默认值(视觉回归)/ 改删 prop(破坏性)/ 只改样式(视觉回归);
|
||||
- 回归范围(要重截哪些页面 / 哪些 demo);
|
||||
- 破坏性改动的旧 → 新写法对照;
|
||||
- **不做的事**:哪些按页面私有处理,理由是什么。
|
||||
|
||||
> 评审通过前**不动代码**。这一步是「先停下来统一方案」的落点。
|
||||
|
||||
## 5. 实施顺序(库先行)
|
||||
|
||||
```text
|
||||
① 令牌(如果是新形态值)
|
||||
② 组件 props / variant / 新组件
|
||||
③ 展示页示例(每个新能力至少一个 demo)
|
||||
④ 契约:COMPONENTS.md 对应小节 + 变更记录
|
||||
⑤ 主题文件(如果只是配色)
|
||||
⑥ 页面接入(只写页面私有的那部分 + 注释说明)
|
||||
```
|
||||
|
||||
## 6. 验收清单(外来风格专项)
|
||||
|
||||
- [ ] 深色主题:新能力在 `data-scheme="dark"` 下不串色(只吃语义令牌)
|
||||
- [ ] 键盘:新交互可聚焦、有焦点环、Esc/方向键行为明确
|
||||
- [ ] `prefers-reduced-motion`:动效有降级
|
||||
- [ ] 移动端:390 宽无横向溢出;触屏不依赖 hover
|
||||
- [ ] 业务隔离:组件不 import 业务类型、不请求数据
|
||||
- [ ] 默认值:外来品牌值没有写进组件默认值,只在主题/页面里
|
||||
- [ ] 文档:契约、展示页、README 组件表、变更记录四处同步
|
||||
|
||||
## 7. 反面样本(不要这么做)
|
||||
|
||||
- 把截图里的品牌色写成组件默认主色;
|
||||
- 在页面里覆盖 `_button_xxx` 这种 hash 类名;
|
||||
- 为了一个页面复制一份组件到项目里改;
|
||||
- 「先页面实现,回头再收」但没登记候选组件;
|
||||
- 截图里每个数值都硬编码,最后页面上出现 6 套圆角。
|
||||
Reference in New Issue
Block a user