✨ feat(prototype): 代码阅读原型加 React 组件一页(组件卡片:名称行标签、按属性展开的属性表、状态 / 引用 / 读取的数据 / 派生值 / 副作用 / 事件处理分区、渲染结构大纲;简单、复杂、对照、标记说明四张画板);插件菜单加 React 组件

This commit is contained in:
cruldra committed 2026-10-03 03:11:46 +08:00
1 parent b0f67cbfcd
commit caeeabebb3
4 files changed
+1108 -30

No files matched your search

@@ -68,11 +68,28 @@
交互:点「只看改动」在两个窗口间切换;点删掉的块展开 / 收起(组件里连的,说明板上的实例也能点)。「… N 块没变」和上一处 / 下一处只画样子,没连。
## React 组件(插件菜单「React 组件」,单独一页 `代码阅读器 · React 组件`)
TSX 里的 React 函数组件不再画成普通函数卡片(输入参数一格塞整个解构和类型字面量、输出参数「未标注」、钩子和 JSX 都是普通语句),而是一张「React 组件」卡片(原子图标,悬停提示「React 组件」)。示例是 luffy 的真实代码:`frontend/client-nextjs/src/components/desktop/DesktopTitlebar.tsx`(简单)、`components/LegalDocPage.tsx`(复杂)。
| 画板 | 对应源码 | 设计点 |
| --- | --- | --- |
| `Prototype / React 组件 · 简单` | `DesktopTitlebar.tsx` | 编辑器全貌,侧栏 `DesktopTitlebar.tsx` 选中。预览里五条导入,然后是组件卡片:名称后面「具名导出」「客户端组件」(第 1 行 `"use client"`、第 11 行文档注释并进卡片,不单独成块);说明;属性表 `platform · DesktopShell["platform"] · — · 是`;读取的数据(`usePathname()` → 当前路径,`useDesktopShell((s) => s.title)` → 从 desktop store 读 title);派生值 center(title;没有就按路径查 PAGE_TITLES;再没有就空);渲染大纲 |
| `Prototype / React 组件 · 复杂` | `LegalDocPage.tsx` | 属性来自 `LegalDocPageProps`(多一列说明);状态两个(渲染时按 doc 重置写进「改它的」);引用 scrollRef;读取的数据(自定义钩子 useSiteConfig、useRouter);派生值(三元取正文、useMemo 抽标题,写什么时候重算);副作用两张子卡片(时机 / 做什么 / 清理,第二张有清理、里面定义的 handleScroll 收着);事件处理表(名称 / 绑定在 / 做什么);渲染大纲有 JSX 注释、列表(对 headings 的每一项 h,key h.id,其中 isActive = …)、三元(如果 markdown.trim() 有内容 → 是 / 否)、随条件切换的类 |
| `Prototype / React 组件 · 对照` | `DesktopTitlebar.tsx` | 左边照画现在的普通函数卡片(输入参数 `{ platform }` / `{ platform: DesktopShell["platform"] }`、输出参数「未标注」、三句变量块、一整段 JSX 译文),右边是新卡片 |
| `Prototype / React 组件 · 标记说明` | 见下 | 名称行五种小标签;属性表的「来自 / 回调 / 子元素」(ShareButton、PosterDialog 的 StepLabel);钩子放进哪个分区、副作用时机的读法;渲染大纲的写法:原生标签和组件、样式小标(收起 / 点开)、条件、三元(DeleteAppDialog 的删除按钮)、列表(DeleteAppDialog)、提前返回和多个 return(DesktopSettingsDialog)、片段(AppVaultSummary)、注释,每样一句短说明 |
卡片的分区按这个顺序,没有就不画:属性 → 状态(useState / useReducer:名称 / 初始值 / 改它的)→ 引用(useRef)→ 读取的数据(其他钩子:变量 / 来自 / 意思)→ 派生值(常量和 useMemo / useCallback:名称 / 怎么算 / 什么时候算)→ 副作用(每个 useEffect 一张子卡片)→ 事件处理(名称 / 绑定在 / 做什么)→ 渲染。渲染大纲每行一个元素:原生标签灰色,组件用组件色(Cmd+单击跳到定义);长 className 收成「样式 N 个类」,按条件切换的写「随 X 变」;`cond && <X/>` 前缀「当 … 时」;三元「如果 …」下面「是 / 否」两支;`.map` 写「对 … 的每一项」并标 key;提前 `return null` 写「当 … 时 不渲染」,多个 return 分几段;`<></>` 写「片段」。子卡片里的语句照常是一块块,一块一行模板直译的译文。
组件集:`React / 组件卡片`(`Component=DesktopTitlebar | LegalDocPage`)、`React / 副作用`(`Card=effect-1 | effect-2`、`State=open | closed`)、`React / 事件处理`(`Open=none | handleSectionClick | handlePrint | handleBack`)、`React / StyleChip`(`State=closed | open`)、`React / DesktopTitlebar.tsx`(五条导入,`Line=<行号>`)、`React / 现状`、`Explorer / ReactSidebar`、`Reader / FilterBar`(`File=titlebar`),外加主画板那套窗口外壳和 `Explorer / TreeRow`。卡片外壳、属性行、表格、导入卡片、块种类图标都是主画板那一套。
交互(都连在组件里,各处实例都能点):点「样式 N 个类」弹出全部类名、再点收起;点副作用子卡片左上角的箭头展开 / 收起;点事件处理表里的函数名在表下面展开它的子卡片(那一行淡淡高亮),再点同一个名字或子卡片的箭头收起,点别的名字换成那个;窗口侧栏点 `LegalDocPage.tsx` 跳到复杂那张。块右上角的「问 AI / 编辑」这一页只画样子,没连。
## 导入与运行
1. 打开 Figma Desktop,打开任意 Design 文件。
2. `Plugins > Development > Import plugin from manifest…`,选本目录的 `manifest.json`(已经导入过就跳过)。
3. `Plugins > Development > 代码阅读器 Design Lab`,选「主画板」「Shell 脚本」「Dockerfile」或「代码差异」,各自新建一页。改过 `code.js` 后要重启 Figma 才会用新代码。
3. `Plugins > Development > 代码阅读器 Design Lab`,选「主画板」「Shell 脚本」「Dockerfile」「代码差异」或「React 组件」,各自新建一页。改过 `code.js` 后要重启 Figma 才会用新代码。
## 运行条件
@@ -162,3 +179,8 @@ node --test test.js
- [ ] 代码差异:删掉的块收起时一行、虚线红边半透明;点开后旧内容整张划掉
- [ ] 代码差异:筛选栏两行不挤;总览条贴在预览右边缘,六段刻度的位置大致对得上改动在文件里的位置
- [ ] 代码差异:点「只看改动」两个窗口互跳,折起的细条比卡片矮很多
- [ ] React 组件:名称后面的小标签和组件名在一行里对齐、比字矮一点;属性表回调 / 子元素小标签贴在名字后面
- [ ] React 组件:各分区的表格左边和属性表对齐;副作用子卡片、事件处理展开的子卡片也和表格左边对齐,嵌在卡片里不显得挤
- [ ] React 组件:渲染大纲每层左边一条竖线,原生标签灰、组件青,一眼分得开;「当 … 时」「对 … 的每一项」「是 / 否」小框和后面的元素在一行里对齐
- [ ] React 组件:点「样式 N 个类」弹出的类名盖在下面的行上面(不被后面的字压住),再点收起
- [ ] React 组件:点副作用子卡片的箭头能收起成一行、再展开;点事件处理的函数名在表下面展开子卡片,箭头收起
File diff suppressed because it is too large. Load diff
@@ -27,6 +27,10 @@
{
"name": "代码差异",
"command": "diff"
},
{
"name": "React 组件",
"command": "react"
}
]
}
+191 -3
View File
@@ -2,7 +2,7 @@ const assert = require("node:assert/strict");
const { test } = require("node:test");
const fs = require("node:fs");
const path = require("node:path");
const { DIFF_PAGE_NAME, DIFF_BLOCKS, DIFF_COLORS, DIFF_SAMPLE, UNCOMMITTED_GROUPS, diffParts, diffStatsText, NEXT_DOCKERFILE, BACKEND_DOCKERFILE, DOCKER_PAGE_NAME, SHELL_BLOCKS, SHELL_PAGE_NAME, NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
const { REACT_PAGE_NAME, TITLEBAR_CARD, LEGAL_CARD, LEGAL_EFFECTS, TITLEBAR_IMPORTS, EXTRA_KINDS, DIFF_PAGE_NAME, DIFF_BLOCKS, DIFF_COLORS, DIFF_SAMPLE, UNCOMMITTED_GROUPS, diffParts, diffStatsText, NEXT_DOCKERFILE, BACKEND_DOCKERFILE, DOCKER_PAGE_NAME, SHELL_BLOCKS, SHELL_PAGE_NAME, NESTED_CALL_SAMPLE, parseCall, ROUTER_SAMPLE, ROUTE_SAMPLE, REVIEW, REVIEW_ISSUES, ASK_SAMPLE, BEAT_SCHEDULE, ENUM_SAMPLE, SQLA_MODEL, ITERATION_SAMPLES, iterationSegments, facetCounts, filteredOut, filterPresets, FILTER_FILE, NOTES, NOTE_DRAFT, PLAIN_KINDS, build, run, nextPageName, PAGE_NAME, NAMESPACE, WORKSPACES, FILES, CODE_TREE, BLOCK_KINDS, SOURCES, blockCode, lineRange, walkTree, translateBlock, highlightCode, parseSignature, parseImport, parseVariable, typeSegments } = require("./code.js");
const { fakeFigma } = require("../../figma-lab/fake-figma");
function action(node) { return node.reactions[0].actions[0]; }
@@ -100,9 +100,10 @@ test("manifest stays local, without a fabricated plugin ID", () => {
assert.deepEqual(manifest.editorType, ["figma"]);
assert.deepEqual(manifest.networkAccess.allowedDomains, ["none"]);
assert.equal("id" in manifest, false);
// 插件菜单:主画板、Shell 脚本、Dockerfile、代码差异,各自只生成自己那一页
assert.deepEqual(manifest.menu.map((item) => item.command), ["main", "shell", "docker", "diff"]);
// 插件菜单:主画板、Shell 脚本、Dockerfile、代码差异、React 组件,各自只生成自己那一页
assert.deepEqual(manifest.menu.map((item) => item.command), ["main", "shell", "docker", "diff", "react"]);
assert.equal(manifest.menu.find((item) => item.command === "diff").name, "代码差异");
assert.equal(manifest.menu.find((item) => item.command === "react").name, "React 组件");
});
test("plugin source passes Figma's load-time scan", () => {
@@ -755,6 +756,193 @@ test("running the diff command twice numbers the page", async () => {
assert.equal(second.page.name, `${DIFF_PAGE_NAME} 2`);
});
// React 组件页:一次生成,下面几个测试共用
let reactRun;
function reactPage() {
if (!reactRun) {
const api = fakeFigma();
reactRun = build(api, () => {}, "react").then((result) => ({ api, result }));
}
return reactRun;
}
const childNames = (node) => node.children.map((child) => child.name);
test("the React 组件 menu item builds its own page with four boards, under the layer budget", async () => {
const { result } = await reactPage();
assert.equal(result.page.name, REACT_PAGE_NAME);
assert.deepEqual(result.screens.map((screen) => screen.name),
["Prototype / React 组件 · 简单", "Prototype / React 组件 · 复杂", "Prototype / React 组件 · 对照", "Prototype / React 组件 · 标记说明"]);
assert.equal(result.page.flowStartingPoints[0].nodeId, result.boards.simple.screen.id);
let owned = 0;
const walk = (node) => { owned += 1; if (node.type !== "INSTANCE") node.children.forEach(walk); };
result.page.children.forEach(walk);
assert.ok(owned < 3000, `React page owns ${owned} layers`);
const blocks = [...result.screens, ...result.components.sets];
for (const block of blocks) assert.equal(block.parent, result.page);
for (let i = 0; i < blocks.length; i += 1) {
for (let j = i + 1; j < blocks.length; j += 1) assert.equal(overlaps(blocks[i], blocks[j]), false, `${blocks[i].name} overlaps ${blocks[j].name}`);
}
const setIds = new Set(result.components.sets.flatMap((set) => set.children.map((component) => component.id)));
for (const screen of result.screens) {
for (const instance of screen.findAll((node) => node.type === "INSTANCE")) assert.ok(setIds.has(instance.mainComponentId), `${instance.name} comes from a set`);
}
});
test("React window: DesktopTitlebar.tsx selected in the sidebar, its imports then the React card in the preview", async () => {
const { result } = await reactPage();
const { lib } = result.components;
const { simple } = result.boards;
assert.equal(named(simple.screen, "Title").characters, `DesktopTitlebar.tsx — luffy-agent-platform`);
assert.equal(named(simple.sidebar, "Row / DesktopTitlebar.tsx").mainComponentId, lib.row.get("file/selected").id);
assert.equal(named(simple.sidebar, "Row / LegalDocPage.tsx").mainComponentId, lib.row.get("file/default").id);
assert.ok(named(simple.screen, "Filter bar"));
// "use client" 和文档注释并进卡片,不单独成块:导入五块,然后是组件卡片
const placed = simple.tree.findAll((node) => node.type === "INSTANCE" && node.name.startsWith("Block / "));
assert.deepEqual(placed.map((node) => node.name), [...TITLEBAR_IMPORTS.map(([line]) => `Block / L${line}`), `Block / L${TITLEBAR_CARD.line}`]);
assert.equal(placed[placed.length - 1].mainComponentId, lib.reactCards.get("DesktopTitlebar").id);
assert.equal(named(named(lib.titlebarBlocks.get(5), "Prop / 仅类型"), "Value").characters, "是");
});
test("React cards: name tags, one row per destructured prop, sections in order, no 输入参数 / 输出参数", async () => {
const { result } = await reactPage();
const { lib } = result.components;
const titlebar = named(lib.reactCards.get("DesktopTitlebar"), "Content");
assert.ok(named(lib.reactCards.get("DesktopTitlebar"), "Kind / React 组件"));
assert.deepEqual(childNames(titlebar), ["Prop / 名称", "Prop / 说明", "Prop / 属性", "Props", "Prop / 读取的数据", "Reads", "Prop / 派生值", "Derived", "Prop / 渲染", "Outline"]);
assert.deepEqual(named(titlebar, "Prop / 名称").findAll((node) => node.name.startsWith("Tag / ")).map((node) => node.name), ["Tag / 具名导出", "Tag / 客户端组件"]);
assert.deepEqual(texts(named(titlebar, "Property / platform")), ["platform", 'DesktopShell["platform"]', "—", "是"]);
assert.deepEqual(texts(named(titlebar, "Row / title")), ["title", "useDesktopShell((s) => s.title)", "从 desktop store 读 title"]);
assert.deepEqual(texts(named(titlebar, "Row / center")), ["center", "title;没有就按路径查 PAGE_TITLES;再没有就空", "每次渲染"]);
const legal = named(lib.reactCards.get("LegalDocPage"), "Content");
assert.deepEqual(childNames(legal).filter((name) => name.startsWith("Prop / ")),
["Prop / 名称", "Prop / 说明", "Prop / 属性", "Prop / 状态", "Prop / 引用", "Prop / 读取的数据", "Prop / 派生值", "Prop / 副作用", "Prop / 事件处理", "Prop / 渲染"]);
for (const card of [titlebar, legal]) {
assert.equal(named(card, "Prop / 输入参数"), null);
assert.equal(named(card, "Prop / 输出参数"), null);
}
// props 来自接口:展开字段,「属性」后面写来自哪里;有说明就多一列
assert.equal(named(named(legal, "Prop / 属性"), "Value").characters, "1 个 · 来自 LegalDocPageProps");
assert.equal(texts(named(legal, "Property / doc")).pop(), "terms = 用户服务协议; privacy = 隐私政策");
assert.equal(texts(named(legal, "Row / headings")).pop(), "markdown 变了时(useMemo)");
// 回调、子元素:名字后面跟小标签,回调的类型写成函数签名
const legend = result.boards.legend;
const callback = named(legend, "Property / onOpenChange");
assert.ok(named(callback, "Tag / 回调"));
assert.equal(texts(callback)[2], "(next: 布尔) => 无");
assert.ok(named(named(legend, "Property / children"), "Tag / 子元素"));
});
test("React render outline: native tags grey, components coloured, style chips, conditions, ternaries, lists, early returns, fragments", async () => {
const { result } = await reactPage();
const { lib } = result.components;
const outline = named(lib.reactCards.get("DesktopTitlebar"), "Outline");
const windowControls = named(outline, "Line / WindowControls");
assert.equal(named(named(windowControls, "Pill / when"), "Text").characters, '当 platform 是 "windows" 时');
assert.ok(sameColor(colorOf(named(windowControls, "Element")), "#61dafb"), "components in the component colour");
assert.ok(sameColor(colorOf(named(named(outline, "Line / span"), "Element")), "#808080"), "native tags grey");
const rootStyle = named(named(outline, "Line / div"), "Style");
assert.equal(rootStyle.mainComponentId, lib.styleChip.get("closed").id);
assert.equal(named(rootStyle, "Label").characters, "样式 8 个类");
assert.equal(named(rootStyle, "Text").characters, TITLEBAR_CARD.render[0].cls);
assert.equal(named(lib.styleChip.get("closed"), "Classes").visible, false);
assert.equal(named(lib.styleChip.get("open"), "Classes").visible, true);
const legal = named(lib.reactCards.get("LegalDocPage"), "Outline");
const list = named(legal, "Line / 列表");
assert.equal(named(named(list, "Pill / each"), "Text").characters, "对 headings 的每一项 h");
assert.equal(named(list, "Key").characters, "key h.id");
const ternary = named(legal, "Line / 三元");
assert.equal(named(named(ternary, "Pill / when"), "Text").characters, "如果 markdown.trim() 有内容");
const branches = ternary.parent.children[ternary.parent.children.indexOf(ternary) + 1];
assert.deepEqual(branches.findAll((node) => node.name === "Pill / branch").map((node) => named(node, "Text").characters), ["是", "否"]);
assert.ok(texts(legal).includes("// 顶部导航栏: 返回 + 文档切换 + 打印"), "JSX comments stay as comment lines");
const legend = result.boards.legend;
const early = named(named(legend, "Sample / 提前返回"), "Line / 不渲染");
assert.deepEqual(texts(early), ["当 section 没有值 时", "不渲染"]);
assert.equal(named(named(named(legend, "Sample / 提前返回"), "Line / 返回"), "Text").characters, "否则");
assert.deepEqual(texts(named(named(legend, "Sample / 片段"), "Line / <>")), ["<>", "片段,不产生元素"]);
assert.equal(named(named(legend, "Sample / 样式"), "Line / Link").findOne((node) => node.name === "Style").mainComponentId, lib.styleChip.get("open").id);
});
test("React effects and handlers: sub-cards with timing, body and cleanup; the handler table opens a sub-card under it", async () => {
const { result } = await reactPage();
const { lib } = result.components;
const effect = (key, state) => lib.effects.get(`${key}/${state}`).node;
assert.equal(named(named(effect("effect-2", "open"), "Prop / 时机"), "Value").characters, "doc、headings 变了时");
assert.equal(named(named(effect("effect-2", "open"), "Prop / 清理"), "Value").characters, 'container.removeEventListener("scroll", handleScroll)');
assert.equal(named(effect("effect-1", "open"), "Prop / 清理"), null, "cleanup only when the effect returns one");
assert.equal(named(effect("effect-2", "open"), "Body").findAll((node) => node.name.startsWith("Statement / ")).length, LEGAL_EFFECTS[1].body.length);
assert.equal(named(effect("effect-2", "closed"), "Summary").characters, "doc、headings 变了时 · 滚动时按标题的位置高亮当前章节 · 有清理");
assert.ok(named(effect("effect-1", "open"), "Kind / 副作用"));
const placed = named(lib.reactCards.get("LegalDocPage"), "Content").findAll((node) => node.type === "INSTANCE" && node.name.startsWith("Effect / "));
assert.deepEqual(placed.map((node) => node.mainComponentId), LEGAL_EFFECTS.map((item) => effect(item.key, "open").id));
const none = lib.handlers.get("none");
assert.equal(named(none.node, "Handler card"), null);
assert.deepEqual(texts(named(none.node, "Handler / handleBack")), ["handleBack", "<button>(返回)的 onClick", "有上一页就后退,没有就去 /home"]);
const back = lib.handlers.get("handleBack");
assert.ok(named(back.node, "Kind / 事件处理"));
assert.ok(sameColor(colorOf(named(back.node, "Handler / handleBack")), EXTRA_KINDS.find((kind) => kind.label === "事件处理").color));
assert.equal(named(named(lib.handlers.get("handleSectionClick").node, "Prop / 参数"), "Value").characters, "id: 字符串");
});
test("React demo: style chips, effect folds and handler names switch variants in place; the sidebar opens the complex board", async () => {
const { api, result } = await reactPage();
const { lib } = result.components;
// 样式小标 2;两个副作用各展开 / 收起 2 = 4;事件处理 4 个变体 × 3 个名称 = 12,加 3 个展开的子卡片折叠箭头 = 15;侧栏 1;合计 22
assert.equal(api.connected, 22);
assert.equal(action(lib.styleChip.get("closed")).destinationId, lib.styleChip.get("open").id);
assert.equal(action(lib.styleChip.get("open")).destinationId, lib.styleChip.get("closed").id);
for (const item of LEGAL_EFFECTS) {
const open = lib.effects.get(`${item.key}/open`);
const closed = lib.effects.get(`${item.key}/closed`);
assert.equal(action(open.toggle).navigation, "CHANGE_TO");
assert.equal(action(open.toggle).destinationId, closed.node.id);
assert.equal(action(closed.toggle).destinationId, open.node.id);
}
const variant = (open) => lib.handlers.get(open);
assert.equal(action(variant("none").names.get("handleBack")).destinationId, variant("handleBack").node.id);
assert.equal(action(variant("handleBack").names.get("handleBack")).destinationId, variant("none").node.id, "clicking the open name again closes it");
assert.equal(action(variant("handleBack").names.get("handlePrint")).destinationId, variant("handlePrint").node.id);
assert.equal(action(variant("handleBack").fold).destinationId, variant("none").node.id);
const toComplex = action(named(result.boards.simple.sidebar, "Row / LegalDocPage.tsx"));
assert.equal(toComplex.navigation, "NAVIGATE");
assert.equal(toComplex.destinationId, result.boards.complex.id);
for (const node of result.page.findAll((item) => item.reactions.length > 0)) {
const target = api.nodes.get(action(node).destinationId);
assert.ok(target, `${node.name} targets a node`);
let top = target;
while (top.parent && top.parent.type !== "PAGE") top = top.parent;
assert.equal(top.parent, result.page, `${node.name} targets a node on the React page`);
}
});
test("React comparison: now the whole destructuring sits in one parameter cell and the output is 未标注; next to it the React card", async () => {
const { result } = await reactPage();
const { lib } = result.components;
const now = named(result.boards.compare, "Block / Now");
assert.equal(now.mainComponentId, lib.current.get("DesktopTitlebar").id);
assert.deepEqual(texts(named(now, "Param / { platform }")).slice(0, 2), ["{ platform }", '{ platform: DesktopShell["platform"] }']);
assert.equal(named(named(now, "Prop / 输出参数"), "Value").characters, "未标注");
assert.ok(named(named(now, "Block / L17"), "Plain").characters.startsWith("返回 (<div"));
assert.equal(named(result.boards.compare, "Block / New").mainComponentId, lib.reactCards.get("DesktopTitlebar").id);
// 复杂那张:LegalDocPage 的卡片实例
assert.equal(named(result.boards.complex, `Block / L${LEGAL_CARD.line}`).mainComponentId, lib.reactCards.get("LegalDocPage").id);
});
test("the React kinds join the fixed icon set and the main legend", async () => {
for (const [label, iconName] of [["React 组件", "atom"], ["副作用", "zap"], ["事件处理", "mouse-pointer-click"]])
assert.equal(EXTRA_KINDS.find((kind) => kind.label === label).icon, iconName);
const api = fakeFigma();
const { kindBoard } = await build(api);
assert.ok(named(kindBoard, "Kind row / React 组件"));
});
test("running the react command twice numbers the page", async () => {
const api = fakeFigma();
await build(api, () => {}, "react");
const second = await build(api, () => {}, "react");
assert.equal(second.page.name, `${REACT_PAGE_NAME} 2`);
});
test("block kinds show as icons, the legend lists every kind with its name", async () => {
const api = fakeFigma();
const { kindBoard } = await build(api);