@captain_z/zsk-skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +263 -0
- package/bundles.yaml +104 -0
- package/design-handoff/.gitkeep +0 -0
- package/design-handoff/figma-to-code/SKILL.md +265 -0
- package/design-handoff/ue-mcp/SKILL.md +184 -0
- package/frontend/.gitkeep +0 -0
- package/frontend/a11y-web/SKILL.md +169 -0
- package/frontend/api-contract-ts/SKILL.md +275 -0
- package/frontend/css-bem/SKILL.md +268 -0
- package/frontend/design-frontend/SKILL.md +163 -0
- package/frontend/dor-dod-frontend/SKILL.md +114 -0
- package/frontend/feature-tasks-frontend/SKILL.md +246 -0
- package/frontend/i18n/SKILL.md +296 -0
- package/frontend/nfr-web/SKILL.md +258 -0
- package/frontend/performance-web/SKILL.md +299 -0
- package/frontend/react-components/SKILL.md +211 -0
- package/frontend/react-naming/SKILL.md +224 -0
- package/frontend/review-frontend/SKILL.md +126 -0
- package/frontend/security-web/SKILL.md +286 -0
- package/frontend/spec-frontend/SKILL.md +141 -0
- package/frontend/testing-web/SKILL.md +252 -0
- package/frontend/typescript/SKILL.md +219 -0
- package/meta/.gitkeep +0 -0
- package/meta/philosophy/SKILL.md +221 -0
- package/package.json +24 -0
- package/quality/.gitkeep +0 -0
- package/quality/a11y-principles/SKILL.md +155 -0
- package/quality/code-hygiene/SKILL.md +126 -0
- package/quality/release/SKILL.md +209 -0
- package/quality/security-owasp/SKILL.md +157 -0
- package/quality/testing-pyramid/SKILL.md +173 -0
- package/sdlc/.gitkeep +0 -0
- package/sdlc/archive/SKILL.md +267 -0
- package/sdlc/bugfix/SKILL.md +181 -0
- package/sdlc/bugfix-tasks/SKILL.md +232 -0
- package/sdlc/coding/SKILL.md +177 -0
- package/sdlc/design/SKILL.md +299 -0
- package/sdlc/dor-dod/SKILL.md +120 -0
- package/sdlc/feature/SKILL.md +162 -0
- package/sdlc/proposal/SKILL.md +271 -0
- package/sdlc/refactor/SKILL.md +220 -0
- package/sdlc/refactor-tasks/SKILL.md +265 -0
- package/sdlc/reviewing/SKILL.md +197 -0
- package/sdlc/spec/SKILL.md +235 -0
- package/sdlc/task/SKILL.md +116 -0
- package/sdlc/task-evidence/SKILL.md +178 -0
- package/sdlc/task-structure/SKILL.md +153 -0
- package/sdlc/task-tracking/SKILL.md +192 -0
- package/sdlc/verify/SKILL.md +181 -0
- package/system/.gitkeep +0 -0
- package/system/adr/SKILL.md +169 -0
- package/system/architecture/SKILL.md +182 -0
- package/system/glossary/SKILL.md +141 -0
- package/system/nfr-baseline/SKILL.md +156 -0
- package/system/project-constraints-template/SKILL.md +241 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zsk:ue-mcp
|
|
3
|
+
description: Design Source capture & MCP readout for modules with Figma/design
|
|
4
|
+
input — Design Source (URL/node-id), MCP readout status, module-specific UE
|
|
5
|
+
deviations from system baselines, and implementation mapping. Produces
|
|
6
|
+
docs/{module}/ue-mcp.md + structured description.md snapshot in design-assets.
|
|
7
|
+
Only records deviations, not duplicated baseline rules.
|
|
8
|
+
category: resource
|
|
9
|
+
domain: design-handoff
|
|
10
|
+
tier: optional
|
|
11
|
+
related:
|
|
12
|
+
- ../figma-to-code/SKILL.md
|
|
13
|
+
- ../../frontend/a11y-web/SKILL.md
|
|
14
|
+
- ../../sdlc/spec/SKILL.md
|
|
15
|
+
triggers:
|
|
16
|
+
- UE MCP context
|
|
17
|
+
- figma snapshot
|
|
18
|
+
- design source reference
|
|
19
|
+
- module UE deviation
|
|
20
|
+
- implementation mapping
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Supporting Template: UE / MCP
|
|
24
|
+
|
|
25
|
+
> **定位**:阶段 2(Spec)和阶段 3(Design)的支撑模板 — 模块视觉/交互差异记录
|
|
26
|
+
> **核心原则**:**Figma 与 SRS 是唯一事实源**。本文件只沉淀 MCP 读取事实与本模块特殊差异,不重复抄写系统级交互规范。
|
|
27
|
+
> **静态资源存放**:`{{config.paths.design_assets}}/{module}/`
|
|
28
|
+
> **完整 Figma → Code 方法论**:见 [`zsk:figma-to-code`](../figma-to-code/SKILL.md)
|
|
29
|
+
> **系统级规范归**:`SYSTEM-SPEC.md`(UI 组件库、通用交互约定、a11y / 响应式基线 — 见 [`system/nfr-baseline`](../../system/nfr-baseline/SKILL.md) + [`frontend/a11y-web`](../../frontend/a11y-web/SKILL.md) + [`frontend/nfr-web`](../../frontend/nfr-web/SKILL.md))
|
|
30
|
+
|
|
31
|
+
## Skill 映射
|
|
32
|
+
|
|
33
|
+
| 场景 | Primary Skill |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| 读取 Figma | `mcp__figma-mcp-go__*` 系列 MCP 工具 |
|
|
36
|
+
| 视觉评审 | `plan-design-review` / `design-review` |
|
|
37
|
+
| 视觉规范咨询 | `design-consultation` |
|
|
38
|
+
| 系统规范研究 | `context7`(ARIA APG / WCAG 文档) |
|
|
39
|
+
|
|
40
|
+
## 两类产物的边界
|
|
41
|
+
|
|
42
|
+
| 产物 | 位置 | 作用 | 更新时机 |
|
|
43
|
+
| --- | --- | --- | --- |
|
|
44
|
+
| `docs/{module}/ue-mcp.md` | 模块目录 | **人读摘要 + 选定的 UE 契约** | 当前版本演进 |
|
|
45
|
+
| `{{config.paths.design_assets}}/{module}/{snapshot}/description.md` | 静态资源目录 | **结构化快照**(所见即所得的数据) | 快照点不可变 |
|
|
46
|
+
|
|
47
|
+
**分工**:ue-mcp.md 说"我们选择这样做";description.md 说"当时 Figma 长这样"。
|
|
48
|
+
|
|
49
|
+
## 为什么存在
|
|
50
|
+
|
|
51
|
+
解决以下问题:
|
|
52
|
+
|
|
53
|
+
- Figma / MCP 读取信息散落
|
|
54
|
+
- 视觉状态无法追踪到具体设计源
|
|
55
|
+
- 设计稿变更后无法快速识别受影响模块
|
|
56
|
+
- MCP 会话数据仅停留在上下文而不落盘
|
|
57
|
+
|
|
58
|
+
**不解决**(属系统规范职责):
|
|
59
|
+
|
|
60
|
+
- 通用交互规则(弹窗、表单、按钮、焦点环)
|
|
61
|
+
- 组件库导入策略(由项目 SYSTEM-SPEC.md 约定)
|
|
62
|
+
- 键盘导航通用矩阵(见 [`quality/a11y-principles`](../../quality/a11y-principles/SKILL.md))
|
|
63
|
+
- 响应式断点基线(见 [`frontend/nfr-web`](../../frontend/nfr-web/SKILL.md))
|
|
64
|
+
|
|
65
|
+
## 必填结构(精简版)
|
|
66
|
+
|
|
67
|
+
1. **Design Source** — Figma URL / Node ID / 页面名 + **本地归档路径**
|
|
68
|
+
2. **MCP Source** — 服务名 / 读取时间 / 可用性
|
|
69
|
+
3. **模块特有状态** — 仅记录偏离系统基线的状态
|
|
70
|
+
4. **Implementation Mapping** — UE 区域 → 实现文件
|
|
71
|
+
5. **Gaps / Risks** — 缺失项与降级方案
|
|
72
|
+
|
|
73
|
+
(结构化节点 / 尺寸 / token 等详细机器元数据放到 `{{config.paths.design_assets}}/{module}/{snapshot}/description.md`,不在本文件重抄)
|
|
74
|
+
|
|
75
|
+
## 核心要求
|
|
76
|
+
|
|
77
|
+
### 1. MCP 读取落盘
|
|
78
|
+
|
|
79
|
+
- 优先通过 `.mcp.json` 声明的 MCP 服务读取
|
|
80
|
+
- 无法读取时显式记录"已尝试但资源未暴露"及降级方案
|
|
81
|
+
- 读取结果落到 `{{config.paths.design_assets}}/{module}/{snapshot}/`,包括:
|
|
82
|
+
- `description.md`(结构化 YAML frontmatter + 人读章节)
|
|
83
|
+
- `screenshot-*.png`(截图)
|
|
84
|
+
- `raw/mcp-response.json`(可选,`.gitignore`)
|
|
85
|
+
|
|
86
|
+
### 2. 只记录"差异",不重抄通用规范
|
|
87
|
+
|
|
88
|
+
- 与系统基线一致的状态(如 `hover / focus-visible / disabled`)直接引用 `SYSTEM-SPEC.md`
|
|
89
|
+
- 仅当本模块有**偏离系统基线**的状态时才展开
|
|
90
|
+
|
|
91
|
+
### 3. a11y / 响应式 / 动效
|
|
92
|
+
|
|
93
|
+
- 遵循系统基线(WCAG 2.1 AA、Reduced Motion — 见 `quality/a11y-principles`)
|
|
94
|
+
- 仅记录本模块**偏离基线**或**超出基线**的特殊要求
|
|
95
|
+
|
|
96
|
+
### 4. 与 Figma / SRS 的关系
|
|
97
|
+
|
|
98
|
+
- Figma 缺字段 → SRS 补
|
|
99
|
+
- SRS 与 Figma 冲突 → 记录待确认,不自行判断
|
|
100
|
+
- 本文件不重新定义结构/布局/状态,只做"映射 + 读取事实 + 选定契约"
|
|
101
|
+
|
|
102
|
+
## ue-mcp.md 最小模板
|
|
103
|
+
|
|
104
|
+
```md
|
|
105
|
+
# {模块} UE / MCP Context
|
|
106
|
+
|
|
107
|
+
## 1. Design Source
|
|
108
|
+
|
|
109
|
+
- Figma URL:
|
|
110
|
+
- Node ID:
|
|
111
|
+
- 页面 / Frame:
|
|
112
|
+
- 最后确认日期:
|
|
113
|
+
- **本地归档**: `{{config.paths.design_assets}}/{module}/{snapshot-name}/`
|
|
114
|
+
- **结构化描述**: `{{config.paths.design_assets}}/{module}/{snapshot-name}/description.md`
|
|
115
|
+
|
|
116
|
+
## 2. MCP Source
|
|
117
|
+
|
|
118
|
+
- `.mcp.json` 服务名:
|
|
119
|
+
- MCP 读取时间:
|
|
120
|
+
- MCP 可用性:
|
|
121
|
+
- 读取方式:
|
|
122
|
+
|
|
123
|
+
## 3. 模块特有状态(偏离系统基线)
|
|
124
|
+
|
|
125
|
+
> 系统基线状态(hover / focus-visible / disabled / selected)默认遵循 `SYSTEM-SPEC.md` 与 `quality/a11y-principles`,本节仅记录偏离或独有状态。
|
|
126
|
+
|
|
127
|
+
| 状态 | 触发条件 | 偏离点 | 视觉表现 |
|
|
128
|
+
| --- | --- | --- | --- |
|
|
129
|
+
|
|
130
|
+
## 4. Implementation Mapping
|
|
131
|
+
|
|
132
|
+
| UE 区域 | 实现文件 | 说明 |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
|
|
135
|
+
## 5. Gaps / Risks
|
|
136
|
+
|
|
137
|
+
- MCP 暂不可达的资源:
|
|
138
|
+
- 与 Figma 的差异:
|
|
139
|
+
- 等待后端 / 设计确认项:
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## description.md 模板(存放在 `{{config.paths.design_assets}}/{module}/{snapshot}/description.md`)
|
|
143
|
+
|
|
144
|
+
```md
|
|
145
|
+
---
|
|
146
|
+
module_id: {module-id}
|
|
147
|
+
snapshot_name: {YYYY-MM-DD}-{描述}
|
|
148
|
+
figma_url: https://www.figma.com/...?node-id=...
|
|
149
|
+
node_id: "<int:int>"
|
|
150
|
+
page: {Figma 页面名}
|
|
151
|
+
frame: {Frame 名}
|
|
152
|
+
generated_at: {ISO 8601 时间}
|
|
153
|
+
mcp_service: figma-mcp-go
|
|
154
|
+
purpose: {本次快照的触发原因,如"Spec 冻结基线"}
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
# {模块} Figma 快照:{snapshot-name}
|
|
158
|
+
|
|
159
|
+
## 节点层级
|
|
160
|
+
## 布局(Auto-layout / Flex / Grid)
|
|
161
|
+
## 设计 Token
|
|
162
|
+
## 状态矩阵
|
|
163
|
+
## 交互(如有 Figma 原型)
|
|
164
|
+
## Gaps / 差异
|
|
165
|
+
## 附件清单
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## 高保真验收规则
|
|
169
|
+
|
|
170
|
+
- 没有 `ue-mcp.md` + 对应 `{{config.paths.design_assets}}/{module}/` 快照,不得声称"已完成高保真还原"
|
|
171
|
+
- 缺 node-id / 状态表 / 差异记录时,不得通过视觉验收
|
|
172
|
+
- MCP 无法读取时必须显式记录原因和降级方案
|
|
173
|
+
- a11y 矩阵未填时,不得通过可访问性验收
|
|
174
|
+
|
|
175
|
+
## 质量门禁
|
|
176
|
+
|
|
177
|
+
- [ ] `docs/{module}/ue-mcp.md` 存在
|
|
178
|
+
- [ ] `{{config.paths.design_assets}}/{module}/{snapshot}/description.md` 存在
|
|
179
|
+
- [ ] Design Source 含 URL + node-id + 确认日期 + 本地归档路径
|
|
180
|
+
- [ ] MCP 读取结果已落盘(即便为空也有记录)
|
|
181
|
+
- [ ] 未重抄系统级规范
|
|
182
|
+
- [ ] 仅记录本模块偏离基线的状态
|
|
183
|
+
- [ ] Implementation Mapping 与实际文件一致
|
|
184
|
+
- [ ] Gaps / Risks 已记录
|
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zsk:a11y-web
|
|
3
|
+
description: React/JSX accessibility implementation — label association
|
|
4
|
+
(htmlFor/wrap), aria props usage, Modal focus trap (inert + initialFocus +
|
|
5
|
+
restoreFocus), dialog role, live regions for async, skip link, axe-core +
|
|
6
|
+
jsx-a11y toolchain, keyboard event handlers, prefers-reduced-motion.
|
|
7
|
+
Implementation layer; principles (WCAG, POUR, contrast) live in
|
|
8
|
+
quality/a11y-principles.
|
|
9
|
+
category: standard
|
|
10
|
+
domain: frontend
|
|
11
|
+
tier: optional
|
|
12
|
+
related:
|
|
13
|
+
- ../react-components/SKILL.md
|
|
14
|
+
- ../i18n/SKILL.md
|
|
15
|
+
- ../testing-web/SKILL.md
|
|
16
|
+
- ../../quality/a11y-principles/SKILL.md
|
|
17
|
+
triggers:
|
|
18
|
+
- jsx a11y
|
|
19
|
+
- focus trap
|
|
20
|
+
- Modal accessibility
|
|
21
|
+
- axe-core
|
|
22
|
+
- aria implementation
|
|
23
|
+
- skip link
|
|
24
|
+
- live region
|
|
25
|
+
- reduced motion
|
|
26
|
+
- keyboard events React
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# a11y-web · 无障碍实现(JSX 层)
|
|
30
|
+
|
|
31
|
+
> **范围**:React / JSX 的**实现细节** — label 关联 / aria 属性 / Modal 焦点陷阱 / 键盘 handler / 工具链
|
|
32
|
+
> **原则层**(WCAG / POUR / 键盘矩阵 / 对比度 / Reduced Motion 原则)→ 见 [`quality/a11y-principles`](../../quality/a11y-principles/SKILL.md)
|
|
33
|
+
> **前置**:本文件假设你已读过原则层
|
|
34
|
+
|
|
35
|
+
## 1. label 关联(表单)
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// ✅ 强关联(优先)
|
|
39
|
+
<label htmlFor="email">邮箱</label>
|
|
40
|
+
<input id="email" type="email" />
|
|
41
|
+
|
|
42
|
+
// ✅ wrap(无 id 需求时)
|
|
43
|
+
<label>邮箱 <input type="email" /></label>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**禁用**:无关联 label / `<div>` 当 label / `placeholder` 代替 label。
|
|
47
|
+
|
|
48
|
+
## 2. 命名与描述
|
|
49
|
+
|
|
50
|
+
**组件命名优先级**:可见文字 > `aria-labelledby` > `aria-label`(仅图标按钮等无可见文字时)。
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<button>删除订单</button> // ✅ 可见文字
|
|
54
|
+
<button aria-label={t('app.order.btn.delete', '删除订单')}> // ✅ 图标按钮
|
|
55
|
+
<IconTrash aria-hidden="true" />
|
|
56
|
+
</button>
|
|
57
|
+
<button aria-label="删除">删除订单</button> // ❌ 冲突
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**描述**:`aria-describedby` 关联提示与错误。
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<input id="password" aria-describedby="pwd-hint pwd-error" />
|
|
64
|
+
<p id="pwd-hint">至少 8 位,含字母与数字</p>
|
|
65
|
+
<p id="pwd-error" role="alert">{error}</p>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 3. aria 实战要点
|
|
69
|
+
|
|
70
|
+
**最常用状态属性**:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<button aria-expanded={isOpen}>展开</button>
|
|
74
|
+
<li aria-selected={isSelected} role="option">...</li>
|
|
75
|
+
<input aria-invalid={hasError} aria-describedby="err-1" />
|
|
76
|
+
<div aria-hidden="true">{/* 装饰元素 */}</div>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**aria-live**(异步内容变化):`role="status"` + `aria-live="polite"` 用于礼貌通知;`role="alert"` + `aria-live="assertive"` 仅用于紧急打断(勿滥用)。
|
|
80
|
+
|
|
81
|
+
**禁用**:同时 `aria-label` + `aria-labelledby`;`aria-hidden="true"` 包可交互元素;装饰元素加 `role`。
|
|
82
|
+
|
|
83
|
+
## 4. Modal / Dialog 焦点陷阱
|
|
84
|
+
|
|
85
|
+
**六项硬门禁**:`role="dialog"` + `aria-modal` + `aria-labelledby` / 打开时首焦 / Tab 循环 / Esc 关闭 / 关闭时焦点回归触发按钮 / 背景 `inert`。
|
|
86
|
+
|
|
87
|
+
**优先用原生 `<dialog>`**(浏览器处理 Esc、焦点陷阱、背景 inert):
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
91
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
92
|
+
|
|
93
|
+
const open = () => { triggerRef.current = document.activeElement as HTMLButtonElement; dialogRef.current?.showModal(); };
|
|
94
|
+
const close = () => { dialogRef.current?.close(); triggerRef.current?.focus(); };
|
|
95
|
+
|
|
96
|
+
<dialog ref={dialogRef} aria-labelledby="dlg-title">
|
|
97
|
+
<h2 id="dlg-title">{t('app.confirm.title', '确认')}</h2>
|
|
98
|
+
</dialog>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
自研 Modal 必须显式实现全部六项。第三方选一不混:`focus-trap-react` / `@radix-ui/react-dialog` / `react-aria` Dialog。
|
|
102
|
+
|
|
103
|
+
## 5. 键盘事件 handler
|
|
104
|
+
|
|
105
|
+
**优先语义元素**(`<button>` / `<a>` 天生支持 Enter / Space)。必须用非语义元素时补齐 `role + tabIndex + onKeyDown`:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
<div role="button" tabIndex={0} onClick={handleClick}
|
|
109
|
+
onKeyDown={(e) => {
|
|
110
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleClick(); }
|
|
111
|
+
}}>
|
|
112
|
+
...
|
|
113
|
+
</div>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**组合键位**(列表 / 树 / Select)全集见 [`quality/a11y-principles`](../../quality/a11y-principles/SKILL.md) 键盘矩阵;React 实现按 `switch (e.key)` 分派 Arrow / Home / End / Enter / Escape。
|
|
117
|
+
|
|
118
|
+
**`:focus-visible` 样式**(禁止裸用 `outline: none`):
|
|
119
|
+
|
|
120
|
+
```less
|
|
121
|
+
button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 6. Skip Link、图片与图标、prefers-reduced-motion
|
|
125
|
+
|
|
126
|
+
**Skip Link**:`<a href="#main" className="skip-link">...</a>` + `<main id="main" tabIndex={-1}>`;CSS 默认 `left: -9999px`,`:focus-visible` 时 `left: 0; top: 0;`。
|
|
127
|
+
|
|
128
|
+
**图片 / 图标**:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
<img src="/chart.png" alt={t('app.sales.chart.alt', '2026 年销售趋势图')} /> // 信息图
|
|
132
|
+
<img src="/divider.svg" alt="" /> // 装饰图
|
|
133
|
+
<IconCheck aria-hidden="true" /> // 装饰图标
|
|
134
|
+
<button aria-label={t('app.close', '关闭')}><IconX aria-hidden="true" /></button> // 图标按钮
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**prefers-reduced-motion**:CSS `@media (prefers-reduced-motion: reduce)` 将动画 / 过渡改为 `0.01ms !important`;JS 用 `window.matchMedia('(prefers-reduced-motion: reduce)').matches` 判断后降级装饰性动画。
|
|
138
|
+
|
|
139
|
+
## 7. 工具链
|
|
140
|
+
|
|
141
|
+
**Lint**:`eslint-plugin-jsx-a11y` 启用全套规则(`alt-text` / `label-has-associated-control` / `click-events-have-key-events` / `aria-role` / `role-has-required-aria-props` / `no-static-element-interactions` / `anchor-is-valid`),CI 零 error。
|
|
142
|
+
|
|
143
|
+
**运行时**:DEV 环境动态加载 `@axe-core/react`,扫描 DOM。**硬门禁**:零 `critical` / `serious` violation。
|
|
144
|
+
|
|
145
|
+
**测试**:`jest-axe` + `@axe-core/playwright` 的集成手法见 [`testing-web`](../testing-web/SKILL.md) 第 8 / 9 节(单元 + E2E axe 扫描)。
|
|
146
|
+
|
|
147
|
+
**手动**:键盘跑关键流程一遍(不碰鼠标);VoiceOver / NVDA 抽检;浏览器缩放 200% 检查布局。
|
|
148
|
+
|
|
149
|
+
## 反模式(禁止)
|
|
150
|
+
|
|
151
|
+
- ❌ `<div onClick>` 无键盘支持
|
|
152
|
+
- ❌ `outline: none` 不补 `:focus-visible`
|
|
153
|
+
- ❌ Modal 缺 role / Esc / 焦点陷阱 / 焦点回归 任一项
|
|
154
|
+
- ❌ `aria-hidden="true"` 包裹可交互元素
|
|
155
|
+
- ❌ `aria-label` / `alt` / `placeholder` 硬编码文案(应走 i18n)
|
|
156
|
+
- ❌ 关闭 `jsx-a11y` 规则或在 CI 容忍其 warning
|
|
157
|
+
|
|
158
|
+
## 质量门禁
|
|
159
|
+
|
|
160
|
+
- [ ] 表单 label 关联(htmlFor / wrap)
|
|
161
|
+
- [ ] 图标按钮有 `aria-label`(经 i18n)
|
|
162
|
+
- [ ] Modal 六项齐全(role / aria / 初始焦点 / 循环 / Esc / 回归 / inert 背景)
|
|
163
|
+
- [ ] 非语义交互元素补全 `role + tabIndex + onKeyDown`
|
|
164
|
+
- [ ] `:focus-visible` 样式存在
|
|
165
|
+
- [ ] `prefers-reduced-motion` 场景降级
|
|
166
|
+
- [ ] `jsx-a11y` lint 零 error
|
|
167
|
+
- [ ] `axe-core` 零 critical / serious violation
|
|
168
|
+
- [ ] 键盘手动回归关键流程通过
|
|
169
|
+
- [ ] 原则层门禁见 [`quality/a11y-principles`](../../quality/a11y-principles/SKILL.md)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zsk:api-contract-ts
|
|
3
|
+
description: TypeScript frontend × backend API contract — never hand-write API
|
|
4
|
+
types, always consume from the shared types package or generated codegen.
|
|
5
|
+
Covers source-of-truth pattern (backend-owned types), codegen workflows
|
|
6
|
+
(OpenAPI / GraphQL / tRPC / Protobuf), breaking-change handling, version
|
|
7
|
+
discipline, runtime validation (Zod at boundary), and service-layer shape.
|
|
8
|
+
Pairs with typescript.md red lines.
|
|
9
|
+
category: standard
|
|
10
|
+
domain: frontend
|
|
11
|
+
tier: optional
|
|
12
|
+
related:
|
|
13
|
+
- ../typescript/SKILL.md
|
|
14
|
+
- ../testing-web/SKILL.md
|
|
15
|
+
- ../../sdlc/spec/SKILL.md
|
|
16
|
+
- ../../sdlc/design/SKILL.md
|
|
17
|
+
triggers:
|
|
18
|
+
- API type sync
|
|
19
|
+
- do not hand-write API types
|
|
20
|
+
- OpenAPI codegen
|
|
21
|
+
- GraphQL codegen
|
|
22
|
+
- tRPC types
|
|
23
|
+
- Zod runtime validation
|
|
24
|
+
- breaking API change
|
|
25
|
+
- service layer shape
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# API Contract TS · 前后端类型契约
|
|
29
|
+
|
|
30
|
+
> **范围**:TypeScript 前端**不手写** API 类型,类型来自后端单一事实源(SOT)
|
|
31
|
+
> **为什么**:手写类型 = 两处事实源 → 运行时才发现不一致;机器生成的类型随后端变更立即让 `tsc` 报错
|
|
32
|
+
> **配合**:[`typescript`](../typescript/SKILL.md) 红线 · [`testing-web`](../testing-web/SKILL.md) 的 MSW 契约
|
|
33
|
+
|
|
34
|
+
## 1. 核心原则
|
|
35
|
+
|
|
36
|
+
- **后端拥有 schema**(OpenAPI / GraphQL SDL / tRPC router / Protobuf)
|
|
37
|
+
- **前端从 schema 生成类型**,不手抄
|
|
38
|
+
- **运行时边界验证**(Zod / io-ts)在 service 层做一次,内部代码信任类型
|
|
39
|
+
- **Service 层是唯一 HTTP 入口**,业务代码只调 service,不直接 fetch
|
|
40
|
+
|
|
41
|
+
## 2. 类型来源三种模式
|
|
42
|
+
|
|
43
|
+
### 模式 A:共享 npm 包(monorepo / 私服)
|
|
44
|
+
|
|
45
|
+
后端 + 前端同一个 monorepo 或后端发布类型包:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// frontend/src/services/user.ts
|
|
49
|
+
import type { UserResponse, UserCreateRequest } from '@company/api-types';
|
|
50
|
+
|
|
51
|
+
export async function getUser(id: string): Promise<UserResponse> {
|
|
52
|
+
const res = await request.get<UserResponse>(`/api/users/${id}`);
|
|
53
|
+
return res.data;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
优点:完全同步;版本对齐靠 semver。
|
|
58
|
+
|
|
59
|
+
### 模式 B:从 OpenAPI 生成(REST)
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
# scripts/codegen-api.sh
|
|
63
|
+
npx openapi-typescript https://api.example.com/openapi.json -o src/types/api.ts
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// src/types/api.ts(自动生成,不手改)
|
|
68
|
+
export interface paths {
|
|
69
|
+
'/api/users/{id}': {
|
|
70
|
+
get: {
|
|
71
|
+
parameters: { path: { id: string } };
|
|
72
|
+
responses: { 200: { content: { 'application/json': components['schemas']['User'] } } };
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export interface components { schemas: { User: { id: string; name: string; ... } } }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 模式 C:从 GraphQL 生成
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
# codegen.yml
|
|
83
|
+
schema: https://api.example.com/graphql
|
|
84
|
+
documents: 'src/**/*.graphql'
|
|
85
|
+
generates:
|
|
86
|
+
src/types/graphql.ts:
|
|
87
|
+
plugins:
|
|
88
|
+
- typescript
|
|
89
|
+
- typescript-operations
|
|
90
|
+
- typescript-react-apollo # 或其他 client
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 模式 D:tRPC(TS-to-TS 一体化)
|
|
94
|
+
|
|
95
|
+
后端是 TS + tRPC 时,类型**端到端**自动:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import type { AppRouter } from '@company/server';
|
|
99
|
+
import { createTRPCProxyClient, httpLink } from '@trpc/client';
|
|
100
|
+
const trpc = createTRPCProxyClient<AppRouter>({ links: [httpLink({ url: '/api/trpc' })] });
|
|
101
|
+
// trpc.user.getById.query('1') 完全类型安全,无 codegen 步骤
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 模式 E:Protobuf(gRPC-Web / Connect)
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
buf generate # 按 buf.gen.yaml
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
生成 TS 客户端 + 类型。
|
|
111
|
+
|
|
112
|
+
## 3. 选型决策
|
|
113
|
+
|
|
114
|
+
| 后端栈 | 推荐 |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| Node/TS(同 monorepo) | tRPC 或共享 types 包 |
|
|
117
|
+
| REST + 有 OpenAPI | `openapi-typescript` / `orval` / `openapi-fetch` |
|
|
118
|
+
| GraphQL | `graphql-codegen` |
|
|
119
|
+
| gRPC-Web / Connect | `buf` + `@bufbuild/protoc-gen-es` |
|
|
120
|
+
| 无 schema(老后端) | **先补 schema**,不接受手写类型长期 |
|
|
121
|
+
|
|
122
|
+
## 4. 禁止:手写 API 类型
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// ❌ 双事实源
|
|
126
|
+
type UserResponse = { id: string; name: string; email: string };
|
|
127
|
+
|
|
128
|
+
async function getUser(id: string): Promise<UserResponse> { ... }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**问题**:后端加字段前端看不见;后端改字段类型前端还按旧的用。
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// ✅ 单事实源
|
|
135
|
+
import type { UserResponse } from '@company/api-types';
|
|
136
|
+
// 或
|
|
137
|
+
import type { components } from '@/types/api';
|
|
138
|
+
type UserResponse = components['schemas']['User'];
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 5. Service 层封装
|
|
142
|
+
|
|
143
|
+
### 职责
|
|
144
|
+
|
|
145
|
+
- 唯一 HTTP 调用入口
|
|
146
|
+
- 请求前 URL / 参数归一化
|
|
147
|
+
- 响应后 shape 归一化(`null` ↔ `undefined`、时间字符串 → `Date` 可选)
|
|
148
|
+
- **运行时边界校验**(对抗后端不守契约)
|
|
149
|
+
|
|
150
|
+
### 形态(示例:REST + openapi-typescript + zod)
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// src/services/user.ts
|
|
154
|
+
import { z } from 'zod';
|
|
155
|
+
import { request } from './request';
|
|
156
|
+
import type { components } from '@/types/api';
|
|
157
|
+
|
|
158
|
+
type UserFromApi = components['schemas']['User'];
|
|
159
|
+
|
|
160
|
+
const UserRuntimeSchema = z.object({
|
|
161
|
+
id: z.string(),
|
|
162
|
+
name: z.string(),
|
|
163
|
+
email: z.string().email(),
|
|
164
|
+
createdAt: z.string().datetime(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export type User = z.infer<typeof UserRuntimeSchema>;
|
|
168
|
+
|
|
169
|
+
export async function getUser(id: string): Promise<User> {
|
|
170
|
+
const res = await request.get<UserFromApi>(`/api/users/${id}`);
|
|
171
|
+
return UserRuntimeSchema.parse(res.data); // 越界就报错
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 运行时校验的位置
|
|
176
|
+
|
|
177
|
+
- **入 service 边界**:响应解析(必做)
|
|
178
|
+
- **出 service 边界**:请求参数(可选,有类型基本够了)
|
|
179
|
+
- **UI 组件内部**:**不做**(内部代码信任类型系统)
|
|
180
|
+
|
|
181
|
+
## 6. Breaking Change 处理
|
|
182
|
+
|
|
183
|
+
### 后端声明变更
|
|
184
|
+
|
|
185
|
+
- 提前 1 个 sprint 在 `CHANGELOG.md` 声明 breaking field
|
|
186
|
+
- 前端 codegen 同步 → `tsc` 立即报错 → 修复或 pin 旧版本
|
|
187
|
+
|
|
188
|
+
### 前端应对策略
|
|
189
|
+
|
|
190
|
+
1. **版本 pin**:`package.json` 锁住 `@company/api-types@1.x.x`
|
|
191
|
+
2. **迁移期共存**:新旧字段并存,UI 读新优先、fallback 旧
|
|
192
|
+
3. **断舍离**:小项目 / 单用户期直接跟进
|
|
193
|
+
|
|
194
|
+
### 禁止
|
|
195
|
+
|
|
196
|
+
- ❌ "我先 `as any` 绕过 TS 报错,后面再补"(永远不会补)
|
|
197
|
+
|
|
198
|
+
## 7. 版本与同步节奏
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
// 推荐:每次 PR 都跑一次 codegen(CI)
|
|
202
|
+
- name: sync API types
|
|
203
|
+
run: npm run codegen:api
|
|
204
|
+
|
|
205
|
+
- name: check no diff
|
|
206
|
+
run: git diff --exit-code src/types/api.ts
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**门禁**:PR 合并前类型必须同步(无未提交的 codegen 差异)。
|
|
210
|
+
|
|
211
|
+
## 8. 与 MSW 的契约对齐
|
|
212
|
+
|
|
213
|
+
MSW handler 的返回类型也应引自生成类型,保证 mock 不漂移:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// test/handlers.ts
|
|
217
|
+
import { http, HttpResponse } from 'msw';
|
|
218
|
+
import type { UserResponse } from '@company/api-types';
|
|
219
|
+
|
|
220
|
+
const mockUser: UserResponse = { id: '1', name: 'Alice', email: 'a@x.com', createdAt: '2026-04-19T00:00:00Z' };
|
|
221
|
+
|
|
222
|
+
export const handlers = [
|
|
223
|
+
http.get('/api/users/:id', () => HttpResponse.json(mockUser)),
|
|
224
|
+
];
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
后端加字段 → `UserResponse` 类型变 → MSW handler 缺字段 → 测试 TS 报错。
|
|
228
|
+
|
|
229
|
+
## 9. 错误响应也要类型化
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
// api 定义
|
|
233
|
+
type ApiError = components['schemas']['ErrorResponse']; // { code: string; message: string; details?: ... }
|
|
234
|
+
|
|
235
|
+
// service
|
|
236
|
+
export async function getUser(id: string): Promise<User> {
|
|
237
|
+
try {
|
|
238
|
+
const res = await request.get<UserFromApi>(`/api/users/${id}`);
|
|
239
|
+
return UserRuntimeSchema.parse(res.data);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (axios.isAxiosError<ApiError>(err)) {
|
|
242
|
+
throw new ApiError(err.response?.data.code ?? 'UNKNOWN', err.response?.data.message ?? '');
|
|
243
|
+
}
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## 10. 实际落地 checklist
|
|
250
|
+
|
|
251
|
+
- [ ] 项目选定一种类型来源模式(A / B / C / D / E)
|
|
252
|
+
- [ ] `package.json` / CI 有 `codegen:api` 脚本
|
|
253
|
+
- [ ] Service 层是唯一 HTTP 入口
|
|
254
|
+
- [ ] 响应 shape 有运行时校验(Zod / io-ts)
|
|
255
|
+
- [ ] MSW handler 引用生成类型
|
|
256
|
+
- [ ] 无手写的 API 响应 / 请求类型
|
|
257
|
+
- [ ] 后端变更 → `tsc --noEmit` 自动报错
|
|
258
|
+
|
|
259
|
+
## 反模式(禁止)
|
|
260
|
+
|
|
261
|
+
- ❌ 手写 `type UserResponse = { ... }` 后调 API
|
|
262
|
+
- ❌ `as any` / `as unknown as X` 绕过类型检查(见 [`typescript`](../typescript/SKILL.md))
|
|
263
|
+
- ❌ 业务组件直接 fetch(绕过 service 层)
|
|
264
|
+
- ❌ Mock 数据手写 shape(应引生成类型)
|
|
265
|
+
- ❌ 运行时校验放在 UI 组件(应在 service 边界)
|
|
266
|
+
- ❌ "先手写类型、以后换生成"("以后"永远不来)
|
|
267
|
+
|
|
268
|
+
## 质量门禁
|
|
269
|
+
|
|
270
|
+
- [ ] API 类型来自单一事实源(后端 schema)
|
|
271
|
+
- [ ] Service 层有运行时边界校验
|
|
272
|
+
- [ ] `codegen:api` 在 CI 上跑通 + 无未提交 diff
|
|
273
|
+
- [ ] MSW mock 类型对齐
|
|
274
|
+
- [ ] 无手写 API 响应类型(grep 检查)
|
|
275
|
+
- [ ] 无业务代码直接 fetch(service 层统一)
|