From a0d4ab6e504a06c3f9049e75bab366a53640fcf9 Mon Sep 17 00:00:00 2001 From: zhaoge <> Date: Wed, 2 Sep 2026 09:23:27 +0800 Subject: [PATCH] feat: sdd strategy attempt --- docs/guide/SDD.md | 568 ++++++++++++++++++++++++++++++++++ package.json | 3 +- scripts/generate-from-spec.js | 529 +++++++++++++++++++++++++++++++ src/copy/brief/brief.md | 51 +++ src/copy/brief/spec.yaml | 125 ++++++++ src/copy/index.md | 6 +- 6 files changed, 1280 insertions(+), 2 deletions(-) create mode 100644 docs/guide/SDD.md create mode 100644 scripts/generate-from-spec.js create mode 100644 src/copy/brief/brief.md create mode 100644 src/copy/brief/spec.yaml diff --git a/docs/guide/SDD.md b/docs/guide/SDD.md new file mode 100644 index 000000000..e8b153c89 --- /dev/null +++ b/docs/guide/SDD.md @@ -0,0 +1,568 @@ +--- +title: SDD 规格驱动开发 +group: 研发 +order: 6 +--- + +# SDD + Harness 实践指南(dt-react-component) + +**一句话:** 人维护 `brief/brief.md` → AI 生成 `brief/spec.yaml` → 派生**测试、API、实现(④)、Demo(②′ 人工触发)、Harness(②″ 可选)**。 + +**试点组件:** [Copy](../../src/copy/brief/brief.md) + +## 贡献者速查:唤起命令 + +派生步骤**不会自动跑**——需要你在终端打命令拿到 Prompt,再**复制到 Cursor 对话**发送(`api` 除外,脚本直接写文件)。 + +把 `Copy` 换成目标组件名即可: + +```bash +# ① 根据 brief + index.tsx,生成 / 更新 brief/spec.yaml +pnpm spec:generate -- --component Copy --artifact spec + +# ③ 看当前 git diff,告诉你 brief / spec / 实现 / 测试 / API / demo +# 哪些要改、按什么顺序改(改需求时先跑这个统筹,再按输出依次唤起) +pnpm spec:generate -- --component Copy --artifact cascade + +# ④ 按 spec 对 index.tsx 做最小 diff(人审后再合) +pnpm spec:generate -- --component Copy --artifact impl + +# ② 按 spec 生成 / 更新单元测试 +pnpm spec:generate -- --component Copy --artifact test + +# 按 spec 自动同步 index.md 的 API 表(唯一自动写文件;可先 --dry-run) +pnpm spec:generate -- --component Copy --artifact api +pnpm spec:generate -- --component Copy --artifact api --dry-run + +# ②′ 按需生成 dumi demo(先填打印稿里的「设计意图」,再贴到 Cursor) +pnpm spec:generate -- --component Copy --artifact demo + +# ②″ 按需生成 Harness 验规格面板(可选,不阻塞主流程) +pnpm spec:generate -- --component Copy --artifact harness + +# 一次打印全部 Prompt,并同步 API 表 +pnpm spec:generate -- --component Copy --artifact all + +# 查看帮助 +pnpm spec:generate -- --help +``` + +| 步骤 | 命令 | 你要做的事 | +| ------------ | -------------------- | ------------------------------------------------------------------------------------- | +| ① spec | `--artifact spec` | 复制 Prompt → Cursor → 审 `brief/spec.yaml` | +| ③ 改需求统筹 | `--artifact cascade` | 改行为后先跑:AI 读 git diff,输出「还要改什么、按何顺序」;再按清单依次跑 ①/④/②/api… | +| ④ 实现 | `--artifact impl` | 复制 Prompt → Cursor → 人审 `index.tsx` diff | +| ② 测试 | `--artifact test` | 复制 Prompt → Cursor → `pnpm test` | +| API | `--artifact api` | **无需 Cursor**;脚本更新 `index.md` API 区 | +| ②′ Demo | `--artifact demo` | 填设计意图 → Cursor → 人审 → **手贴** index.md | +| ②″ Harness | `--artifact harness` | 复制 Prompt → Cursor(可选) | + +推荐顺序:`spec` → `impl` → `test` → `api` →(按需)`demo` / `harness`。 + +完整 Prompt 正文见下文「Prompt 模板」;与 CLI 打印内容一致。 + +## 流程总览 + +### 架构:谁维护什么 + +```mermaid +flowchart TB + subgraph Human["人维护"] + B["brief/brief.md
行为契约 · Props 语义"] + DEMO_REF["index.md 示例引用
title / description"] + end + + subgraph AI["AI 生成 · 人审"] + S["brief/spec.yaml
机器 SSOT"] + end + + subgraph Derived["从 spec 派生"] + IMPL["index.tsx
Prompt ④"] + TEST["__tests__/*.test.tsx
Prompt ②"] + API["index.md API 表
脚本自动"] + DEMOGEN["demos/*.tsx
Prompt ②′ 人工触发"] + HAR["demos/harness.tsx
Prompt ②″ 可选"] + end + + B -->|"Prompt ①"| S + S --> IMPL + S --> TEST + S --> API + S -.->|"人工命令 AI"| DEMOGEN + S -.-> HAR + DEMOGEN --> DEMO_REF +``` + +### 首次建链(新组件 / 试点接入) + +```mermaid +flowchart TD + Start([开始]) --> S1["写 brief/brief.md"] + S1 --> S2["Prompt ① → brief/spec.yaml"] + S2 --> R1{"审 spec diff
审查三问 #1"} + R1 -->|不通过| S1 + R1 -->|通过| S3["index.md API 区加 @generated 标记
(示例区不动)"] + S3 --> S4["Prompt ④ → index.tsx
新组件写初版 / 存量最小 diff"] + S4 --> R2{"人审实现 diff
审查第四问"} + R2 -->|不通过| S4 + R2 -->|通过| S5["Prompt ② → 单元测试"] + S5 --> S6["pnpm test"] + S6 --> R3{"测试全绿?"} + R3 -->|否| S4 + R3 -->|是| S7["pnpm spec:generate --artifact api"] + S7 --> S8["npm run dev 验收"] + S8 --> OPTD{"需要新 demo?"} + OPTD -->|是| S10["Prompt ②′ 生成 demo
人审 · 手贴 index.md 引用"] + OPTD -->|否| OPT{"需要 Harness?"} + S10 --> OPT + OPT -->|是| S9["Prompt ②″ → harness.tsx"] + OPT -->|否| Done([试点建链完成]) + S9 --> Done +``` + +### 改需求级联(日常维护) + +```mermaid +flowchart TD + Start([需求变更]) --> S1["改 brief/brief.md"] + S1 --> S2["Prompt ③ 或 ①
regenerate brief/spec.yaml"] + S2 --> R1{"审 spec diff"} + R1 -->|不通过| S1 + R1 -->|通过| S3["Prompt ④
index.tsx 最小 diff"] + S3 --> R2{"人审实现"} + R2 -->|不通过| S3 + R2 -->|通过| S4["Prompt ②
regenerate 测试"] + S4 --> S5["pnpm test"] + S5 --> R3{"全绿?"} + R3 -->|否| S3 + R3 -->|是| S6["pnpm spec:generate --artifact api"] + S6 --> OPT1{"regen harness?"} + OPT1 -->|是| S7["Prompt ②″"] + OPT1 -->|否| OPT2{"demo 要更新?"} + S7 --> OPT2 + OPT2 -->|是| S8["Prompt ②′ 生成 demo
人审 · 手贴 index.md 引用"] + OPT2 -->|否| Review + S8 --> Review["审查三问 + 第四问 → PR"] + Review --> Done([完成]) +``` + +### 产物更新方式一览 + +```mermaid +flowchart LR + SPEC["brief/spec.yaml"] + + SPEC -->|"Prompt ④ · 人审"| IMPL["index.tsx"] + SPEC -->|"Prompt ② · regenerate"| TEST["测试"] + SPEC -->|"脚本自动"| API["index.md API"] + SPEC -->|"Prompt ②′ · 人工触发"| DEMO["demos/*.tsx"] + SPEC -.->|"Prompt ②″ · 可选"| HAR["harness"] + DEMO -->|"人贴引用"| IDX["index.md 示例区"] + + style API fill:#e6f7ff + style DEMO fill:#fff7e6 + style IMPL fill:#f6ffed + style IDX fill:#fff7e6 +``` + +### 全库推广(opt-in) + +```mermaid +flowchart TD + Start([团队决策]) --> Q1{"组件类型?"} + Q1 -->|"有行为分支 · 常改"| P0["P0:Copy / StatusTag / FilterRules"] + Q1 -->|"antd 二次封装"| P1["P1:Drawer / PopConfirm / Catalogue"] + Q1 -->|"纯展示 · 少分支"| Skip["暂缓 SDD"] + Q1 -->|"新建组件"| P2["P2:直接走首次建链"] + + P0 --> Onboard["执行「首次建链」流程"] + P1 --> Onboard + P2 --> Onboard + Onboard --> Done["该组件目录出现
brief + spec + @generated API"] + Done --> Loop["改需求走「级联」流程"] +``` + +### 开发者入口:我该走哪条路? + +```mermaid +flowchart TD + Start([开发者接到任务]) --> Q1{"改动类型?"} + + Q1 -->|"首次为组件接入 SDD
(如 Drawer)"| A["场景 A · 首次建链"] + Q1 -->|"已有 brief,要改行为"| B["场景 B · 改需求级联"] + Q1 -->|"新组件从零开发"| C["场景 C · 新组件"] + Q1 -->|"只改样式 / 文案"| D["不走 SDD
直接改代码 + 现有测试"] + Q1 -->|"只补 demo 展示"| E["Prompt ②′ demo
brief 可不动"] + + A --> Branch["git checkout -b feat/sdd-{component}"] + C --> Branch + B --> BriefEdit["改 brief/brief.md"] + BriefEdit --> Cascade + Branch --> Cascade["见下方详细流程"] +``` + +### 场景 A / C:首次建链(Drawer、新组件) + +```mermaid +flowchart TD + Start([开始]) --> S1["1. 建分支 feat/sdd-{component}"] + S1 --> S2["2. 写 src/{c}/brief/brief.md
干什么 · Props 语义 · 场景"] + S2 --> S3["3. Prompt ① → brief/spec.yaml"] + S3 --> R1{"4. 审 spec
props 仅封装层?
extendsAntd 正确?"} + R1 -->|否| S2 + R1 -->|是| S4["5. index.md API 加 @generated 标记
示例区不动"] + S4 --> S5["6. pnpm spec:generate -- --artifact impl
Prompt ④ → 人审 index.tsx"] + S5 --> S6["7. pnpm spec:generate -- --artifact test
Prompt ② → pnpm test"] + S6 --> R2{"测试全绿?"} + R2 -->|否| S5 + R2 -->|是| S7["8. pnpm spec:generate -- --artifact api
(可先 --dry-run)"] + S7 --> S8["9. npm run dev 验收"] + S8 --> OPT1{"需要新 demo?"} + OPT1 -->|是| S9["10. pnpm spec:generate -- --artifact demo
②′ → 人审 → 手贴 index.md"] + OPT1 -->|否| OPT2{"需要 Harness?"} + S9 --> OPT2 + OPT2 -->|是| S10["pnpm spec:generate -- --artifact harness"] + OPT2 -->|否| PR + S10 --> PR["11. PR:brief + spec + 测试 + API 区块
+ index.tsx(如有)"] + PR --> Done([完成]) +``` + +### 场景 B:已有 brief · 组件行为变更 + +```mermaid +flowchart TD + Start([行为变更]) --> S1["1. 改 brief/brief.md"] + S1 --> S2["2. Prompt ① 或 ③
regenerate brief/spec.yaml"] + S2 --> R1{"3. 审 spec diff"} + R1 -->|否| S1 + R1 -->|是| S3["4. pnpm spec:generate -- --artifact impl
Prompt ④ · 人审 index.tsx"] + S3 --> S4["5. pnpm spec:generate -- --artifact test
Prompt ②"] + S4 --> S5["6. pnpm test"] + S5 --> R2{"全绿?"} + R2 -->|否| S3 + R2 -->|是| S6["7. pnpm spec:generate -- --artifact api"] + S6 --> OPT1{"新行为要 demo 展示?"} + OPT1 -->|是| S7["8. pnpm spec:generate -- --artifact demo
人审 · 手贴 index.md"] + OPT1 -->|否| OPT2{"regen harness?"} + S7 --> OPT2 + OPT2 -->|是| S8["Prompt ②″"] + OPT2 -->|否| Review + S8 --> Review["9. 审查清单 → PR"] + Review --> Done([完成]) +``` + +### spec:generate 与各 Prompt 对应关系 + +```mermaid +flowchart LR + subgraph CLI["pnpm spec:generate -- --component X"] + P1["--artifact spec
Prompt ①"] + P3["--artifact cascade
Prompt ③"] + IMPL["--artifact impl
Prompt ④"] + TEST["--artifact test
Prompt ②"] + DEMO["--artifact demo
Prompt ②′"] + API["--artifact api
脚本写 API"] + HAR["--artifact harness
Prompt ②″"] + end + + P1 --> SPEC["brief/spec.yaml"] + SPEC --> IMPL + SPEC --> TEST + SPEC --> DEMO + SPEC --> API + SPEC --> HAR + P3 -.->|"编排顺序"| IMPL +``` + +> 用法:终端跑命令 → 复制打印的 Prompt → 粘贴到 Cursor。详见文首[贡献者速查](#贡献者速查唤起命令)。 + +## 设计原则(低变更) + +| 区块 | 谁维护 | SDD 介入方式 | +| ------------------------------------------------------------- | ------------------------- | ---------------------------------------------- | +| `index.md` **示例引用**(title / description / ``) | 人贴入 | **②′ 输出建议行**,不自动改 index.md | +| `demos/*.tsx` | AI 生成 + **人审** | **Prompt ②′**(人工按需触发,非级联必跑) | +| `index.md` **API 表** | spec 派生 | **自动** — `pnpm spec:generate --artifact api` | +| `index.tsx` **实现** | spec 驱动 + **人审 diff** | **Prompt ④** | +| `__tests__/*.test.tsx` | spec 派生 | **Prompt ②** | +| `demos/harness.tsx` | spec 派生(可选) | **Prompt ②″** | + +### 为什么 `index.tsx` 不用一键 regenerate? + +| 产物 | 与 spec 关系 | 更新方式 | +| ----------- | ------------------------------------- | ----------------------------- | +| 测试 / API | spec 的**直接翻译** | 可整段 regenerate | +| `index.tsx` | spec 的**一种实现**(多种写法都合法) | **Prompt ④ 最小 diff + 人审** | + +spec 约束「该怎么表现」;实现里的 hooks 拆分、性能、样式细节仍由人把关,避免覆盖已有工程决策。 + +SDD 为 **opt-in**:仅带 `brief/brief.md` 的组件走此流程,其它组件零影响。 + +## 目录约定 + +```plain +src/{component}/ +├── brief/ +│ ├── brief.md # 人维护 · 干什么 / Props / 场景 +│ └── spec.yaml # AI 生成 · 人审 +├── index.tsx # Prompt ④ 按 spec 最小更新 · 人审 +├── index.md # 示例手维护;API 表为 @generated 区块 +├── __tests__/{component}.test.tsx +└── demos/ + ├── basic.tsx … # Prompt ②′ 生成 · 人审 + └── harness.tsx # 可选 +``` + +## 工作流 + +### brief 写什么(仅此三节) + +| 节 | 写什么 | 不写什么 | +| -------------- | -------------------------------------------------------------------- | --------------------------------------------- | +| **干什么** | 一句话职责 | 实现细节、文件路径 | +| **Props 语义** | 每个对外 prop 的行为含义;有 antd 继承时在此注明「其余继承 antd X」 | 测试栈、CLI、派生产物流程 | +| **场景** | 应有行为分支(建议带稳定 id,如 `click-copy`);边界可写在对应场景里 | 单独的 outOfScope 大节、Harness / demo 映射表 | + +`outOfScope`、`extendsAntd`、`testFixtures`、`linkedDemos`、`harnessMeta` **由 Prompt ① 写入 spec**(结合 brief + `index.tsx` / 现有 demos),人在审 spec 时裁剪即可。 + +### 首次建链 + +```plain +① 写 brief/brief.md +② Prompt ① → brief/spec.yaml → 审 spec diff +③ Prompt ④ → index.tsx 最小 diff(新组件则实现初始版本)→ 人审 +④ Prompt ② → 测试 → pnpm test +⑤ pnpm spec:generate -- --component {Name} --artifact api +⑥ (按需)Prompt ②′ → demos/*.tsx → 人审 → 手贴 index.md 引用 +⑦ (可选)Prompt ②″ → demos/harness.tsx +``` + +### 改需求(级联 · Prompt ③) + +```plain +改 brief + → Prompt ③ / ① regenerate brief/spec.yaml → 审 spec + → Prompt ④ 更新 index.tsx(最小 diff)→ 人审 + → Prompt ② regenerate 测试 → pnpm test + → pnpm spec:generate -- --artifact api + → (按需)Prompt ②′ demo → 人审 → 手贴 index.md + → (可选)regen harness +``` + +**推荐顺序:** brief → spec → **实现(④)** → 测试(②)→ API(脚本)。先改实现,测试用于验证。 + +## Prompt 模板 + +> **优先用 CLI 打印**(路径已按组件名填好):见文首[贡献者速查](#贡献者速查唤起命令)。 +> 下文为全文存档;将 `Copy` / `copy` 替换为目标组件名亦可手改。 + +### Prompt ① brief + 源码 → spec + +```bash +pnpm spec:generate -- --component Copy --artifact spec +``` + +```text +阅读 src/copy/brief/brief.md 与 src/copy/index.tsx, +生成 src/copy/brief/spec.yaml。 + +brief 通常只有「干什么 / Props 语义 / 场景」三节。 +据此生成:props(**仅封装层**;先属性后方法 onXxx,组内 a–z)、 +scenarios(id 对齐 brief 场景)、outOfScope、testFixtures、 +harnessMeta、linkedDemos(对照现有 demos/)、extendsAntd(仅当 Props 写明继承 antd/rc)。 + +brief 未写清处从源码推断并标 aiSupplement: true。 +@generated-from: brief/brief.md +``` + +### Prompt ② spec → 测试 + +```bash +pnpm spec:generate -- --component Copy --artifact test +``` + +```text +根据 src/copy/brief/spec.yaml 生成 src/copy/__tests__/copy.test.tsx。 +Jest + Testing Library;每个 scenarios.id 一个 it(...);不测 outOfScope。 +@generated-from: brief/spec.yaml +``` + +### Prompt ②′ spec + 设计意图 → Demo(人工触发 · 人审) + +**不在自动级联内**——当你需要新增/改版 demo 时: + +```bash +pnpm spec:generate -- --component Copy --artifact demo +``` + +先填打印稿里的「设计意图」,再在 Cursor 发送。等价全文示例: + +```text +为 Copy 组件生成 dumi demo。 + +【设计意图 — 发送前填写】 +- demo 文件:demos/tooltip-variants.tsx(新建或覆盖) +- 展示目的:对比 tooltip 字符串 / 对象 / 函数三种写法 +- 重点 props:text, tooltip +- 对应 scenarioId(可选):tooltip-string, tooltip-object +- index.md 卡片 title:Tooltip 多种写法 +- index.md 卡片 description:字符串、对象、函数形式配置 tooltip + +【上下文 — AI 必读】 +- src/copy/brief/brief.md +- src/copy/brief/spec.yaml +- src/copy/index.tsx +- 风格参考:src/copy/demos/basic.tsx + +【生成要求】 +1. 写出 demos/tooltip-variants.tsx,import { Copy } from 'dt-react-component' +2. 按「重点 props」组合展示,取值符合 spec.props 语义;不测 outOfScope +3. 遵循 RC demo 风格(BlockHeader / Space / 示例长文本等),单文件聚焦一个主题 +4. 每个子示例用小标题区分,便于 dumi 卡片内阅读 +5. **不要**直接修改 index.md;在回复末尾输出: + - 建议粘贴的 `` 一行 + - 若需更新 spec,输出 linkedDemos 片段供人粘贴 +6. 生成后自查:npm run dev 下该 demo 可独立运行 +``` + +**与 harness 的区别:** ②′ 面向**文档读者**(好看、有叙事);②″ 面向**维护者验 spec**(控件 + 核对清单)。 + +### Prompt ②″ spec → Harness(可选) + +```bash +pnpm spec:generate -- --component Copy --artifact harness +``` + +```text +根据 src/copy/brief/spec.yaml 生成 src/copy/demos/harness.tsx。 +不要修改 index.md 的「示例」区块。 +``` + +### Prompt ④ spec → 实现(最小 diff · 人审) + +```bash +pnpm spec:generate -- --component Copy --artifact impl +``` + +```text +阅读 src/copy/brief/spec.yaml 与 src/copy/index.tsx。 + +1. 逐条对比 spec.scenarios 与当前实现,列出「已满足 / 缺失 / 行为不一致」 +2. 对 index.tsx 做**最小 diff**以满足 spec,要求: + - 只改与 scenarios / props 语义相关的代码 + - 不碰 outOfScope + - 不做无关重构(不重排 import、不改命名风格、不拆 hooks 除非 spec 要求) + - 保持 ICopyProps 与 spec.props 一致 +3. 若 spec 与 brief 冲突,在回复中指出,**不要擅自改 spec** +4. 改完后简述:每个 scenario.id 对应改了哪段逻辑 +``` + +**新组件(无 index.tsx 或空壳):** 同上 Prompt,第 2 步改为「按 spec 实现 index.tsx 初版」,仍遵循 outOfScope 与项目风格。 + +### Prompt ③ 改需求统筹(读 diff → 后续更新顺序) + +改了 brief 或实现后,先跑这条:AI 根据 **git diff** 告诉你还要动哪些派生物、按什么顺序唤起(不是直接改代码)。 + +```bash +pnpm spec:generate -- --component Copy --artifact cascade +``` + +```text +见 git diff(brief / index.tsx / 测试): + +1. 建议 brief/brief.md 还需改哪几段 +2. regenerate brief/spec.yaml 要点 +3. 按顺序列出需执行的 Prompt: + - ④ index.tsx(最小 diff) + - ② 测试 + - pnpm spec:generate -- --component Copy --artifact api + - (按需)②′ demo + - (可选)②″ harness +4. demo 是否需 Prompt ②′(对照 linkedDemos / 新 props 展示) +5. 审查三问 + 实现审查(见下)检查要点 +``` + +## 实现审查(Prompt ④ 之后 · 第四问) + +在「审查三问」基础上,改 `index.tsx` 后追加: + +4. **实现 diff 是否仅满足 spec,无 scope 外改动?** 每个变更能否对应到 scenario.id 或 props 语义? + +## index.md API 标记 + +首次接入时在 `### API` 下增加标记(**示例区块不动**): + +```markdown +### API + + + +| 参数 | 说明 | 类型 | 默认值 | +| ... | + + +``` + +```bash +pnpm spec:generate -- --component Copy --artifact api +pnpm spec:generate -- --component Copy --artifact api --dry-run +``` + +### API 表排序规则(改 props / 同步时必须遵守) + +`pnpm spec:generate --artifact api` 与人工改 API 表时,统一按: + +1. **先属性,后方法** — 方法指 prop 名匹配 `on` + 大写字母(如 `onCopy`、`onChange`);其余为属性(含 `children`、函数型 render 等) +2. **组内 a–z** — 属性组、方法组各自按参数名字母序(大小写不敏感) + +示例顺序:`button` → `className` → `disabled` → `style` → `text` → `tooltip` → `onCopy` + +### 继承 antd props(参考 Drawer) + +API 表只列封装层 props;`extendsAntd` 自动生成 `:::info` 块: + +```yaml +extendsAntd: + component: Drawer + docUrl: https://4x.ant.design/components/drawer-cn/#API + version: '4.x' + omit: "Omit" # 可选 +``` + +## CLI + +与文首[贡献者速查](#贡献者速查唤起命令)相同。`api` 是唯一自动写文件的 artifact;其余只打印 Prompt。 + +| artifact | Prompt | 行为 | +| --------- | ------ | ------------------------------------------------------------------------- | +| `spec` | ① | 打印 Prompt(brief → spec) | +| `cascade` | ③ | 打印 Prompt:读 git diff,列出 brief/spec/实现/测试/API/demo 后续更新顺序 | +| `impl` | ④ | 打印 Prompt(`index.tsx` 最小 diff) | +| `test` | ② | 打印 Prompt(单元测试) | +| `api` | — | **自动**写 `index.md` API 区块 | +| `demo` | ②′ | 打印 Prompt(填设计意图后再发) | +| `harness` | ②″ | 打印 Prompt(可选) | +| `all` | 全部 | 按序打印 + 同步 API | + +## 审查清单 + +1. spec 是否忠实反映 brief? +2. 测试 / API 是否覆盖 spec.props 与 scenarios,且未碰 outOfScope? +3. `pnpm test` 是否绿?API 表与 `index.tsx` interface 是否一致?**顺序是否「先属性后方法、组内 a–z」?** +4. **(改 demo 时)** props 展示与 spec 一致?单 demo 聚焦一主题?index.md 引用已手贴? +5. **(改实现时)** index.tsx diff 是否最小、且每条变更可对应 scenario? + +## 验证清单(Copy 试点) + +- [ ] `brief/brief.md` 入库 +- [ ] `brief/spec.yaml` 审阅通过 +- [ ] Prompt ④ 对照 spec 审过 `index.tsx` +- [ ] `copy.test.tsx` 从 spec 派生,`pnpm test` 绿 +- [ ] `pnpm spec:generate -- --component Copy --artifact api` +- [ ] (按需)Prompt ②′ 生成 demo,`npm run dev` 目视验收 +- [ ] 完成一次 brief 变更 → ④ + ② + api 级联 + +## 延伸阅读 + +- 方法论:`AI Coding 2.0:SDD + Harness 实践指南` +- batch Phase 1:`ResGroupSelector.brief.md` / `ResGroupSelector.spec.yaml` diff --git a/package.json b/package.json index e46dbf8f2..51ddc3887 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "lint:es": "eslint \"src/**/*.{js,jsx,ts,tsx}\" \".dumi/**/*.{js,jsx,ts,tsx}\"", "prepublishOnly": "father doctor && npm run build", "deploy": "npm run docs:build && gh-pages -d docs-dist", - "release": "./scripts/release.sh" + "release": "./scripts/release.sh", + "spec:generate": "node scripts/generate-from-spec.js" }, "authors": [ "dtinsight UED" diff --git a/scripts/generate-from-spec.js b/scripts/generate-from-spec.js new file mode 100644 index 000000000..21d7e374d --- /dev/null +++ b/scripts/generate-from-spec.js @@ -0,0 +1,529 @@ +#!/usr/bin/env node +/** + * SDD spec → artifact generator + * + * - api: deterministic sync of index.md API table from spec.props + * - others: print Cursor prompts for the contributor to paste + * + * Usage: + * pnpm spec:generate -- --component Copy + * pnpm spec:generate -- --component Copy --artifact api + * pnpm spec:generate -- --component Copy --artifact spec|cascade|test|impl|demo|api|harness|all + */ + +const fs = require('fs'); +const path = require('path'); + +/** index.md API 区块标记(相对组件目录:brief/spec.yaml) */ +const GENERATED_START = ''; +const GENERATED_END = ''; + +/** artifact 执行顺序(--artifact all 时) */ +const ARTIFACT_ORDER = ['spec', 'cascade', 'impl', 'test', 'api', 'demo', 'harness']; + +/** 不依赖已有 spec.yaml 的 artifact(首次建链可用) */ +const NO_SPEC_REQUIRED = new Set(['spec', 'cascade']); + +const ARTIFACTS = { + spec: { + label: 'Prompt ① · brief + 源码 → spec.yaml', + output: (dir) => path.join(dir, 'brief', 'spec.yaml'), + prompt: (name) => buildSpecPrompt(name), + write: false, + }, + cascade: { + label: 'Prompt ③ · 改需求统筹(读 git diff → 列出后续更新顺序)', + output: (dir) => path.join(dir, 'brief', 'brief.md'), + prompt: (name) => buildCascadePrompt(name), + write: false, + }, + test: { + label: 'Prompt ② · 单元测试', + output: (dir, _name, kebab) => path.join(dir, '__tests__', `${kebab}.test.tsx`), + prompt: (name) => + `根据 src/${toKebab(name)}/brief/spec.yaml 生成 src/${toKebab( + name + )}/__tests__/${toKebab(name)}.test.tsx。\n` + + `Jest + Testing Library;每个 scenarios.id 一个 it(...);不测 outOfScope。\n` + + `@generated-from: brief/spec.yaml`, + write: false, + }, + api: { + label: 'index.md API 表(脚本自动写文件)', + output: (dir) => path.join(dir, 'index.md'), + prompt: () => '', + write: true, + }, + impl: { + label: 'Prompt ④ · index.tsx 最小 diff · 人审', + output: (dir) => path.join(dir, 'index.tsx'), + prompt: (name) => + `阅读 src/${toKebab(name)}/brief/spec.yaml 与 src/${toKebab(name)}/index.tsx。\n` + + `1. 列出 scenarios 与当前实现的差异\n` + + `2. 对 index.tsx 做**最小 diff**以满足 spec,不碰 outOfScope\n` + + `3. 不做无关重构;保持现有代码风格\n` + + `4. 若 spec 与 brief 冲突,指出而非擅自改 spec`, + write: false, + }, + demo: { + label: 'Prompt ②′ · dumi Demo · 人工触发 · 人审', + output: (dir, _name, kebab) => path.join(dir, 'demos', `${kebab}-demo.tsx`), + prompt: (name) => buildDemoPrompt(name), + write: false, + }, + harness: { + label: 'Prompt ②″ · Harness 交互验证(可选)', + output: (dir) => path.join(dir, 'demos', 'harness.tsx'), + prompt: (name) => + `根据 src/${toKebab(name)}/brief/spec.yaml 生成 demos/harness.tsx。\n` + + `含 props 控件、预设场景按钮、预览区、规格核对清单。\n` + + `不要修改 index.md 的「示例」区块;可选在文档底部增加维护者入口。`, + write: false, + }, +}; + +function toKebab(name) { + return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); +} + +/** 组件根目录下的 brief/ 约定路径 */ +function getBriefPaths(componentDir, kebab) { + const dir = path.join(componentDir, 'brief'); + return { + dir, + briefPath: path.join(dir, 'brief.md'), + specPath: path.join(dir, 'spec.yaml'), + briefRel: `src/${kebab}/brief/brief.md`, + specRel: `src/${kebab}/brief/spec.yaml`, + }; +} + +/** + * Prompt ① — brief + 源码 → spec + * @see docs/guide/SDD.md + */ +function buildSpecPrompt(name) { + const kebab = toKebab(name); + return ( + `阅读 src/${kebab}/brief/brief.md 与 src/${kebab}/index.tsx,\n` + + `生成 src/${kebab}/brief/spec.yaml。\n\n` + + `brief 通常只有「干什么 / Props 语义 / 场景」三节。\n` + + `据此生成:props(**仅封装层**;先属性后方法 onXxx,组内 a–z)、\n` + + `scenarios(id 对齐 brief 场景)、outOfScope、testFixtures、\n` + + `harnessMeta、linkedDemos(对照现有 demos/)、extendsAntd(仅当 Props 写明继承 antd/rc)。\n\n` + + `brief 未写清处从源码推断并标 aiSupplement: true。\n` + + `@generated-from: brief/brief.md` + ); +} + +/** + * Prompt ③ — 变更级联统筹 + * @see docs/guide/SDD.md + */ +function buildCascadePrompt(name) { + return ( + `见 git diff(brief / index.tsx / 测试):\n\n` + + `1. 建议 brief/brief.md 还需改哪几段\n` + + `2. regenerate brief/spec.yaml 要点\n` + + `3. 按顺序列出需执行的 Prompt:\n` + + ` - ④ index.tsx(最小 diff)\n` + + ` - ② 测试\n` + + ` - pnpm spec:generate -- --component ${name} --artifact api\n` + + ` - (按需)②′ demo\n` + + ` - (可选)②″ harness\n` + + `4. demo 是否需 Prompt ②′(对照 linkedDemos / 新 props 展示)\n` + + `5. 审查三问 + 实现审查检查要点(见 docs/guide/SDD.md)` + ); +} + +/** + * Prompt ②′ — demo 由人按需触发,填完「设计意图」段落后交给 AI。 + * @see docs/guide/SDD.md + */ +function buildDemoPrompt(name) { + const kebab = toKebab(name); + return ( + `为 src/${kebab}/ 生成 dumi demo。先阅读:\n` + + `- brief/brief.md、brief/spec.yaml、index.tsx\n` + + `- 现有 demo 风格参考:demos/basic.tsx(或其它已有 demo)\n\n` + + `【使用前请填写以下设计意图,再发送本 Prompt】\n` + + `- demo 文件:demos/__________.tsx(新建或覆盖)\n` + + `- 展示目的:__________(如「对比 tooltip 三种写法」)\n` + + `- 重点 props:__________(如 tooltip, button, disabled)\n` + + `- 对应 scenarioId(可选):__________\n` + + `- index.md 卡片 title:__________\n` + + `- index.md 卡片 description:__________\n\n` + + `生成要求:\n` + + `1. 按上述 props 组合写可运行 demo,import { ${name} } from 'dt-react-component'\n` + + `2. 遵循 RC 现有 demo 风格(BlockHeader / Space / 示例文案等),单 demo 聚焦一个主题\n` + + `3. props 取值与 spec.props 语义一致,不展示 outOfScope 行为\n` + + `4. 输出末尾附:建议写入 index.md 的 一行(**不要**自动改 index.md)\n` + + `5. 若为新场景,建议在 spec linkedDemos 追加映射(输出 yaml 片段供人粘贴)` + ); +} + +function parseArgs(argv) { + const result = { component: null, artifact: 'all', dryRun: false }; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--component' && argv[i + 1]) { + result.component = argv[++i]; + } else if (argv[i] === '--artifact' && argv[i + 1]) { + result.artifact = argv[++i]; + } else if (argv[i] === '--dry-run') { + result.dryRun = true; + } else if (argv[i] === '--help' || argv[i] === '-h') { + result.help = true; + } + } + return result; +} + +function printHelp() { + console.log(` +SDD spec → artifact generator + +用法(把 换成组件名,如 Copy / Drawer): + pnpm spec:generate -- --component --artifact + +贡献者速查(终端打印 Prompt → 复制到 Cursor 发送): + --artifact spec Prompt ① brief + 源码 → 生成/更新 brief/spec.yaml + --artifact cascade Prompt ③ 改需求统筹:读 git diff,列出 brief/spec/实现/测试/API/demo 该改哪些、按何顺序 + --artifact impl Prompt ④ 按 spec 最小改动更新 index.tsx(人审) + --artifact test Prompt ② 按 spec 生成/更新单元测试 + --artifact api 脚本自动写 index.md API 表(唯一自动写文件) + --artifact demo Prompt ②′ 按需生成 dumi demo(先填设计意图再发) + --artifact harness Prompt ②″ 按需生成 Harness 验规格面板(可选) + --artifact all 打印全部 Prompt + 同步 API + +Examples: + pnpm spec:generate -- --component Copy --artifact spec + pnpm spec:generate -- --component Copy --artifact impl + pnpm spec:generate -- --component Copy --artifact test + pnpm spec:generate -- --component Copy --artifact api + pnpm spec:generate -- --component Copy --artifact demo + pnpm spec:generate -- --component Copy --artifact harness + +Docs: docs/guide/SDD.md +`); +} + +function loadSpec(specPath) { + if (!fs.existsSync(specPath)) { + return null; + } + const raw = fs.readFileSync(specPath, 'utf8'); + const idMatches = raw.match(/^\s*-\s*id:/gm); + return { + raw, + path: specPath, + scenarioCount: idMatches ? idMatches.length : 0, + props: parsePropsFromSpec(raw), + extendsAntd: parseExtendsAntdFromSpec(raw), + }; +} + +function unquote(value) { + return value.trim().replace(/^['"]|['"]$/g, ''); +} + +/** + * Parse a flat YAML mapping block (structured subset, no external deps). + */ +function parseFlatYamlBlock(raw, blockName) { + const lines = raw.split('\n'); + const result = {}; + let inBlock = false; + + for (const line of lines) { + if (new RegExp(`^${blockName}:\\s*$`).test(line)) { + inBlock = true; + continue; + } + if (!inBlock) { + continue; + } + if (line.trim() && !line.startsWith(' ') && !line.startsWith('\t')) { + break; + } + const fieldMatch = line.match(/^\s{2}([a-zA-Z_][\w]*):\s*(.*)$/); + if (fieldMatch) { + const [, key, value = ''] = fieldMatch; + result[key] = unquote(value); + } + } + + return Object.keys(result).length ? result : null; +} + +/** + * Parse spec.props block — nested prop definitions. + */ +function parsePropsFromSpec(raw) { + const lines = raw.split('\n'); + const props = {}; + let inProps = false; + let currentProp = null; + const baseIndent = 2; + + for (const line of lines) { + if (/^props:\s*$/.test(line)) { + inProps = true; + continue; + } + if (!inProps) { + continue; + } + if (line.trim() && !line.startsWith(' ') && !line.startsWith('\t')) { + break; + } + const propMatch = line.match(/^(\s*)([a-zA-Z_][\w]*):\s*$/); + if (propMatch && propMatch[1].length === baseIndent) { + currentProp = propMatch[2]; + props[currentProp] = {}; + continue; + } + if (currentProp) { + const fieldMatch = line.match(/^\s+(type|description|default|required):\s*(.+)?$/); + if (fieldMatch) { + const [, key, value = ''] = fieldMatch; + props[currentProp][key] = unquote(value); + } + } + } + + return props; +} + +function parseExtendsAntdFromSpec(raw) { + return parseFlatYamlBlock(raw, 'extendsAntd'); +} + +function formatDefault(value) { + if (value === undefined || value === null || value === '' || value === 'null') { + return '--'; + } + return value; +} + +function escapeTableCell(value) { + return String(value).replace(/\|/g, '\\|'); +} + +/** + * API 表排序规则(与 Drawer 等既有文档一致): + * 1. 先属性,后方法(方法:prop 名匹配 /^on[A-Z]/) + * 2. 属性、方法组内各自按 a–z(localeCompare,大小写不敏感) + */ +function isMethodProp(name) { + return /^on[A-Z]/.test(name); +} + +function sortPropsForApiTable(props) { + const entries = Object.entries(props); + const byNameAsc = (a, b) => a[0].localeCompare(b[0], 'en', { sensitivity: 'base' }); + const attributes = entries.filter(([name]) => !isMethodProp(name)).sort(byNameAsc); + const methods = entries.filter(([name]) => isMethodProp(name)).sort(byNameAsc); + return [...attributes, ...methods]; +} + +function buildApiTable(props) { + const header = ['| 参数 | 说明 | 类型 | 默认值 |', '| --- | --- | --- | --- |']; + const rows = sortPropsForApiTable(props).map(([name, meta]) => { + const desc = escapeTableCell(meta.description || '--'); + const type = meta.type ? `\`${escapeTableCell(meta.type)}\`` : '--'; + const def = escapeTableCell(formatDefault(meta.default)); + return `| ${name} | ${desc} | ${type} | ${def} |`; + }); + return [...header, ...rows].join('\n'); +} + +/** + * Antd inheritance notice — follows Drawer index.md (:::info block). + * @see src/drawer/index.md + */ +function buildExtendsAntdNotice(extendsAntd) { + if (!extendsAntd || !extendsAntd.component) { + return ''; + } + + const version = extendsAntd.version || '4.x'; + const { component, docUrl, omit, notice } = extendsAntd; + + let body = notice; + if (!body) { + if (omit) { + body = `其余参数继承 antd${version} 的 \`${omit}\``; + if (docUrl) { + body += `
详见 [antd${version} 的 ${component}](${docUrl})`; + } + } else if (docUrl) { + body = `其余属性继承 [antd${version} 的 ${component}](${docUrl})`; + } else { + body = `其余属性继承 antd${version} 的 ${component}`; + } + } + + return `\n\n:::info\n${body}\n:::`; +} + +function buildApiBlock(props, extendsAntd) { + return buildApiTable(props) + buildExtendsAntdNotice(extendsAntd); +} + +function syncIndexMdApi(indexPath, apiTable, dryRun) { + if (!fs.existsSync(indexPath)) { + throw new Error(`index.md not found: ${indexPath}`); + } + + const content = fs.readFileSync(indexPath, 'utf8'); + const start = GENERATED_START; + const startIdx = content.indexOf(start); + const endIdx = content.indexOf(GENERATED_END); + + if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) { + throw new Error( + `index.md missing API markers. Add:\n\n${start}\n${apiTable}\n${GENERATED_END}\n\nSee docs/guide/SDD.md` + ); + } + + const before = content.slice(0, startIdx + start.length); + const after = content.slice(endIdx); + const next = `${before}\n\n${apiTable}\n\n${after}`; + + if (dryRun) { + console.log('\n--- API block (dry-run) ---\n'); + console.log(apiTable); + return { updated: false, dryRun: true }; + } + + if (next === content) { + return { updated: false, unchanged: true }; + } + + fs.writeFileSync(indexPath, next, 'utf8'); + return { updated: true }; +} + +function runApiArtifact(component, componentDir, spec, dryRun) { + const props = spec.props; + const propNames = Object.keys(props); + + if (!propNames.length) { + throw new Error('spec.yaml has no parseable props — check props: block format'); + } + + const apiBlock = buildApiBlock(props, spec.extendsAntd); + const indexPath = path.join(componentDir, 'index.md'); + const result = syncIndexMdApi(indexPath, apiBlock, dryRun); + const relIndex = path.relative(process.cwd(), indexPath); + const extendsLabel = spec.extendsAntd?.component + ? `, extends antd ${spec.extendsAntd.component}` + : ''; + + if (result.dryRun) { + console.log(`[api] Would update ${relIndex} (${propNames.length} props${extendsLabel})`); + return; + } + if (result.unchanged) { + console.log( + `[api] ${relIndex} API block already up to date (${propNames.length} props${extendsLabel})` + ); + return; + } + console.log(`[api] Updated ${relIndex} API block (${propNames.length} props${extendsLabel})`); +} + +function resolveArtifactKeys(artifact) { + if (artifact === 'all') { + return ARTIFACT_ORDER.filter((k) => ARTIFACTS[k]); + } + return [artifact]; +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + + if (args.help || !args.component) { + printHelp(); + process.exit(args.help ? 0 : 1); + } + + const component = args.component; + const kebab = toKebab(component); + const componentDir = path.join(process.cwd(), 'src', kebab); + const { briefPath, specPath, briefRel, specRel } = getBriefPaths(componentDir, kebab); + + console.log(`\nSDD generate — ${component}\n`); + + if (!fs.existsSync(briefPath)) { + console.warn(`⚠ Missing brief: ${briefRel}`); + } else { + console.log(`✓ Brief: ${briefRel}`); + } + + const artifactKeys = resolveArtifactKeys(args.artifact); + const unknown = artifactKeys.filter((k) => !ARTIFACTS[k]); + if (unknown.length) { + console.error(`\n✗ Unknown artifact: ${unknown.join(', ')}`); + console.error(` Valid: ${ARTIFACT_ORDER.join(', ')}, all\n`); + process.exit(1); + } + + const needsSpec = artifactKeys.some((k) => !NO_SPEC_REQUIRED.has(k)); + const spec = loadSpec(specPath); + + if (needsSpec && !spec) { + console.error(`\n✗ Spec not found: ${specRel}`); + console.error( + ' 先跑:pnpm spec:generate -- --component ' + + component + + ' --artifact spec\n' + + ' 把打印的 Prompt ① 贴到 Cursor,生成 brief/spec.yaml 后再继续。\n' + ); + process.exit(1); + } + + if (spec) { + console.log( + `✓ Spec: ${specRel} (${spec.scenarioCount} scenario(s), ${ + Object.keys(spec.props).length + } prop(s))` + ); + } else { + console.log(`○ Spec: ${specRel}(尚无,可先跑 --artifact spec)`); + } + + console.log(''); + + artifactKeys.forEach((key) => { + const art = ARTIFACTS[key]; + if (art.write) { + try { + runApiArtifact(component, componentDir, spec, args.dryRun); + } catch (err) { + console.error(`[api] ✗ ${err.message}\n`); + process.exit(1); + } + return; + } + + const out = art.output(componentDir, component, kebab); + const relOut = path.relative(process.cwd(), out); + const exists = fs.existsSync(out); + + console.log(`[${key}] ${art.label}`); + console.log(` Output: ${relOut}${exists ? ' (exists)' : ''}`); + console.log( + ` Prompt(复制到 Cursor 发送):\n ${art.prompt(component).replace(/\n/g, '\n ')}\n` + ); + }); + + console.log('--- 怎么用 ---'); + console.log('1. 上表命令打印 Prompt(api 除外,直接写文件)'); + console.log('2. 复制 Prompt → 粘贴到 Cursor 对话 → 人审 diff'); + console.log('3. demo:先填「设计意图」空白再发送;人审后手贴 index.md 的 '); + console.log('4. 改 props 后:brief → --artifact spec → impl → test → api →(按需)demo'); + console.log(''); +} + +main(); diff --git a/src/copy/brief/brief.md b/src/copy/brief/brief.md new file mode 100644 index 000000000..92bc6f178 --- /dev/null +++ b/src/copy/brief/brief.md @@ -0,0 +1,51 @@ +# Copy + +## 干什么 + +点击复制区域,将 `text` 写入剪贴板;默认带 Tooltip;复制成功后走 `onCopy`(未传时弹出成功提示)。 + +## Props 语义 + +| Prop | 说明 | +| ----------- | ------------------------------------------------------------------------- | +| `button` | 自定义触发节点;默认 `CopyOutlined` 图标 | +| `className` | 透传到复制区域 `.dtc-copy` | +| `disabled` | `true` 时点击不复制、不调用 `onCopy`,并加上 `dtc-copy--disabled` | +| `style` | 透传到复制区域 | +| `text` | 要复制的字符串(必填) | +| `tooltip` | Tooltip 配置:字符串或 `TooltipProps`;`false` / 其它假值时不展示浮层内容 | +| `onCopy` | 复制成功回调,参数为 `text`;默认 `message.success(locale.copied)` | + +## 场景 + +### default-render + +- 渲染可点击的 `.dtc-copy` +- 未传 `button` 时使用默认图标 + +### click-copy + +- `disabled` 为假时,点击调用 `CopyUtils.copy(text, …)` +- 复制成功后调用 `onCopy(text)`(含默认实现) + +### disabled + +- `disabled=true` 时点击不调用 `CopyUtils.copy`、不调用 `onCopy` +- 区域带 `dtc-copy--disabled` + +### custom-button + +- 传入 `button` 时渲染该节点 +- 点击自定义节点同样走复制逻辑 + +### tooltip-string + +- `tooltip` 为字符串时,Tooltip 展示该文案 + +### tooltip-object + +- `tooltip` 为对象时,按对象配置 Tooltip(如 `title`) + +### tooltip-falsy + +- `tooltip` 为假值时不展示 Tooltip 浮层 diff --git a/src/copy/brief/spec.yaml b/src/copy/brief/spec.yaml new file mode 100644 index 000000000..5f3641802 --- /dev/null +++ b/src/copy/brief/spec.yaml @@ -0,0 +1,125 @@ +# @generated-from: brief/brief.md +component: Copy + +# 无 extendsAntd — Copy 不继承 antd 组件 props + +props: + text: + type: string + required: true + description: 需要复制的文本 + button: + type: React.ReactNode + description: 自定义按钮 + default: '``' + className: + type: string + description: 样式 class + disabled: + type: boolean + description: 是否禁用 + default: false + style: + type: React.CSSProperties + description: 样式 + tooltip: + type: "TooltipProps['title'] | TooltipProps" + description: 配置提示信息 + default: '`复制`' + onCopy: + type: '(text: string) => void' + description: 复制后的回调函数 + default: "`() => message.success('复制成功')`" + +scenarios: + - id: default-render + description: 默认渲染可点击区域 + given: + text: hello + expect: + - renders .dtc-copy + + - id: click-copy + description: 点击触发复制 + given: + text: hello + disabled: false + expect: + - CopyUtils.copy called with hello + - onCopy invoked with hello + + - id: disabled + description: 禁用时不触发复制 + given: + text: hello + disabled: true + expect: + - CopyUtils.copy not called + - element has dtc-copy--disabled + + - id: custom-button + description: 自定义 button 节点 + given: + text: hello + button: 复制文本 + expect: + - renders custom button + - click triggers copy + + - id: tooltip-string + description: tooltip 字符串 + given: + text: hello + tooltip: 复制文本 + expect: + - Tooltip title is 复制文本 + + - id: tooltip-object + description: tooltip 对象 + given: + text: hello + tooltip: + title: 复制文本 + expect: + - Tooltip configured with title + + - id: tooltip-falsy + description: tooltip 假值时不展示浮层 + given: + text: hello + tooltip: false + expect: + - no visible Tooltip overlay + +linkedDemos: + - scenarioId: click-copy + demo: demos/basic.tsx + - scenarioId: custom-button + demo: demos/custom.tsx + - scenarioId: disabled + demo: demos/disabled.tsx + +outOfScope: + - CopyUtils implementation + - clipboard permission errors + - Tooltip animation timing + - exact locale strings + +testFixtures: + mocks: + CopyUtils: "jest.fn((text, cb) => cb?.())" + message.success: jest.fn() + +harnessMeta: + controls: + - prop: text + type: string + - prop: disabled + type: boolean + - prop: tooltip + type: string + presets: + - default-render + - click-copy + - disabled + - tooltip-falsy diff --git a/src/copy/index.md b/src/copy/index.md index 7d60e6f6a..4d54f1cfa 100644 --- a/src/copy/index.md +++ b/src/copy/index.md @@ -20,12 +20,16 @@ demo: ### API + + | 参数 | 说明 | 类型 | 默认值 | | --------- | ---------------- | --------------------------------------- | ----------------------------------- | | button | 自定义按钮 | `React.ReactNode` | `` | -| className | 样式 | `string` | -- | +| className | 样式 class | `string` | -- | | disabled | 是否禁用 | `boolean` | false | | style | 样式 | `React.CSSProperties` | -- | | text | 需要复制的文本 | `string` | -- | | tooltip | 配置提示信息 | `TooltipProps['title'] \| TooltipProps` | `复制` | | onCopy | 复制后的回调函数 | `(text: string) => void` | `() => message.success('复制成功')` | + +