From ad2efdcdae39911f6fb478e7a578bce0022a1904 Mon Sep 17 00:00:00 2001 From: cruldra Date: Wed, 30 Sep 2026 16:33:37 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(prototype):=20React=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E9=A1=B5=E7=AA=97=E5=8F=A3=E6=8C=89=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E6=92=91=E9=AB=98=E4=B8=8D=E8=A3=81=EF=BC=8C=E5=9D=97?= =?UTF-8?q?=E6=A0=91=E5=8D=A0=E6=BB=A1=E9=A2=84=E8=A7=88=E5=8C=BA=E5=AE=BD?= =?UTF-8?q?=E5=BA=A6=EF=BC=8C=E4=BE=A7=E6=A0=8F=E6=8B=89=E5=AE=BD=E6=94=BE?= =?UTF-8?q?=E4=B8=8B=E6=B7=B1=E5=B1=82=E9=95=BF=E6=96=87=E4=BB=B6=E5=90=8D?= =?UTF-8?q?=EF=BC=8C=E5=B1=9E=E6=80=A7=E5=90=8D=E4=B8=80=E5=88=97=E6=94=BE?= =?UTF-8?q?=E5=BE=97=E4=B8=8B=E4=BA=94=E4=B8=AA=E5=AD=97=EF=BC=9B=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=8A=A0=E6=8E=92=E5=BE=97=E4=B8=8B=E7=9A=84=E9=98=B2?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code-reader/figma-plugin/README.md | 3 +- .../code-reader/figma-plugin/code.js | 34 ++++++++------- .../code-reader/figma-plugin/test.js | 43 +++++++++++++++++++ 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/docs/prototype/code-reader/figma-plugin/README.md b/docs/prototype/code-reader/figma-plugin/README.md index e02bf1e..7e6b1af 100644 --- a/docs/prototype/code-reader/figma-plugin/README.md +++ b/docs/prototype/code-reader/figma-plugin/README.md @@ -74,7 +74,7 @@ TSX 里的 React 函数组件不再画成普通函数卡片(输入参数一格 | 画板 | 对应源码 | 设计点 | | --- | --- | --- | -| `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 组件 · 简单` | `DesktopTitlebar.tsx` | 编辑器全貌,侧栏 `DesktopTitlebar.tsx` 选中(侧栏拉宽到 340,第 6 层的长文件名放得下)。窗口按内容撑高、预览区不滚动,块树占满预览区宽度,整张卡片都看得到。预览里五条导入,然后是组件卡片:名称后面「具名导出」「客户端组件」(第 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)、注释,每样一句短说明 | @@ -179,6 +179,7 @@ node --test test.js - [ ] 代码差异:删掉的块收起时一行、虚线红边半透明;点开后旧内容整张划掉 - [ ] 代码差异:筛选栏两行不挤;总览条贴在预览右边缘,六段刻度的位置大致对得上改动在文件里的位置 - [ ] 代码差异:点「只看改动」两个窗口互跳,折起的细条比卡片矮很多 +- [ ] React 组件:简单那张窗口按内容撑高,底下状态栏在卡片之后;卡片左右不出预览区,说明在卡片里折行,属性表四列都看得到 - [ ] React 组件:名称后面的小标签和组件名在一行里对齐、比字矮一点;属性表回调 / 子元素小标签贴在名字后面 - [ ] React 组件:各分区的表格左边和属性表对齐;副作用子卡片、事件处理展开的子卡片也和表格左边对齐,嵌在卡片里不显得挤 - [ ] React 组件:渲染大纲每层左边一条竖线,原生标签灰、组件青,一眼分得开;「当 … 时」「对 … 的每一项」「是 / 否」小框和后面的元素在一行里对齐 diff --git a/docs/prototype/code-reader/figma-plugin/code.js b/docs/prototype/code-reader/figma-plugin/code.js index 62e3d3d..9aae098 100644 --- a/docs/prototype/code-reader/figma-plugin/code.js +++ b/docs/prototype/code-reader/figma-plugin/code.js @@ -1736,7 +1736,8 @@ function richText(ctx, parent, name, segments, o = {}) { return node; } -const KEY_W = 76; +// 属性名一列的宽度:放得下带展开箭头的五个字(「读取的数据」) +const KEY_W = 80; /** 属性表的一行:左边属性名,右边着色的值;twisty 表示下面跟着一组子行。 */ function propRow(ctx, parent, label, segments, o = {}) { @@ -2408,27 +2409,29 @@ function windowBoard(ctx, page, lib, name, o) { /** * 窗口外壳:标题栏、活动栏、侧栏、编辑器(标签页 + 面包屑)、状态栏,编辑区里放什么由调用方往 editor 里接着放。 - * o:width / mode / file(FILES 里那种文件描述)/ root(工作区名)/ crumbs(Editor / Breadcrumb 的变体)/ sidebar(main) 往 main 里放侧栏。 + * o:width / mode / file(FILES 里那种文件描述)/ root(工作区名)/ crumbs(Editor / Breadcrumb 的变体)/ sidebar(main) 往 main 里放侧栏 / + * sidebarWidth(侧栏拉宽时的宽度)/ grow:窗口按编辑区的内容撑高,不裁内容(活动栏、侧栏跟着拉到一样高),不给就是固定一屏高、编辑区里滚动。 */ function windowShell(ctx, page, lib, name, o) { const width = o.width || WINDOW_W; - const editorWidth = width - ACTIVITY_W - SIDEBAR_W - 1; - const screen = frame(ctx, `Prototype / ${name}`, { width, height: WINDOW_H, fill: COLORS.editor }); - screen.clipsContent = true; + const editorWidth = width - ACTIVITY_W - (o.sidebarWidth || SIDEBAR_W) - 1; + const screen = frame(ctx, `Prototype / ${name}`, { width, height: o.grow ? undefined : WINDOW_H, fill: COLORS.editor }); + screen.clipsContent = !o.grow; page.appendChild(screen); const title = lib.windowTitle.get("active").createInstance(); title.name = "Title bar"; append(screen, title, true); setText(title, "Title", `${o.file.name} — ${o.root}`); - const main = append(screen, frame(ctx, "Main", { dir: "HORIZONTAL", width, height: MAIN_H })); + const main = append(screen, frame(ctx, "Main", { dir: "HORIZONTAL", width, height: o.grow ? undefined : MAIN_H })); const activity = lib.activity.get("reader").createInstance(); activity.name = "Activity bar"; main.appendChild(activity); activity.layoutSizingVertical = "FILL"; const side = o.sidebar(main); divider(ctx, main, { vertical: true }); + // 撑高的窗口由编辑区的内容定高:编辑区自己按内容撑,别的跟着它;固定高的窗口编辑区占满、里面滚动 const editor = append(main, frame(ctx, "Editor", { width: editorWidth, fill: COLORS.editor })); - editor.layoutSizingVertical = "FILL"; + if (!o.grow) editor.layoutSizingVertical = "FILL"; const titleBar = lib.editorTitle.get(o.mode).createInstance(); titleBar.name = "Editor title"; setText(titleBar, "Name", o.file.name); @@ -3539,6 +3542,8 @@ const HANDLER_KIND = EXTRA_KINDS.find((kind) => kind.label === "事件处理"); // 卡片内容区的宽度:块宽减去左右留白、折叠箭头、种类图标、右上角按钮和它们之间的空隙;表格、子卡片按这个宽度画 const REACT_CONTENT_W = TREE_W - 8 - 10 - 14 - 20 - 50 - 3 * 10; const REACT_ROOT = "luffy-agent-platform"; +// 侧栏拉宽一点:desktop 目录在第 6 层,里面最长的文件名(DesktopSettingsDialog.tsx)放得下,不被侧栏边裁掉 +const REACT_SIDEBAR_W = 340; const REACT_FILES = { titlebar: { name: "DesktopTitlebar.tsx", ext: "tsx", lang: "typescript", crumbs: ["frontend", "client-nextjs", "src", "components", "desktop", "DesktopTitlebar.tsx"] }, @@ -4073,7 +4078,7 @@ function currentTitlebar(ctx) { /** Explorer / ReactSidebar — File=titlebar:侧栏,luffy 的目录展开到两个示例文件,DesktopTitlebar.tsx 选中;行和主画板同一套。 */ function reactSidebar(ctx, lib) { - const node = frame(ctx, "File=titlebar", { component: true, width: SIDEBAR_W, height: MAIN_H, fill: COLORS.side }); + const node = frame(ctx, "File=titlebar", { component: true, width: REACT_SIDEBAR_W, height: MAIN_H, fill: COLORS.side }); node.clipsContent = true; node.overflowDirection = "VERTICAL"; const header = lib.explorerHeader.get("default").createInstance(); @@ -4102,7 +4107,9 @@ function reactFilterBar(ctx, lib) { function reactWindow(ctx, page, lib) { const name = "React 组件 · 简单"; return timed(`Prototype / ${name}`, () => { + // 窗口按内容撑高,整张卡片都看得到;块树和筛选栏占满预览区的宽度,长字在卡片里折行 const { screen, editor, side } = windowShell(ctx, page, lib, name, { mode: "preview", file: REACT_FILES.titlebar, root: REACT_ROOT, crumbs: "titlebar", + grow: true, sidebarWidth: REACT_SIDEBAR_W, sidebar: (main) => { const pane = lib.reactSidebar.get("titlebar").createInstance(); pane.name = "Sidebar"; @@ -4110,18 +4117,15 @@ function reactWindow(ctx, page, lib) { pane.layoutSizingVertical = "FILL"; return pane; } }); - const row = filled(ctx, editor, "Filter row", { align: "CENTER", px: VIEW_PAD, fill: COLORS.editor }); + const row = filled(ctx, editor, "Filter row", { px: VIEW_PAD, fill: COLORS.editor }); row.strokes = paint(COLORS.border); row.strokeBottomWeight = 1; row.strokeTopWeight = row.strokeLeftWeight = row.strokeRightWeight = 0; const bar = lib.reactFilter.get("titlebar").createInstance(); bar.name = "Filter bar"; - append(row, bar); - const view = filled(ctx, editor, "Preview view", { align: "CENTER", px: VIEW_PAD }); - view.layoutSizingVertical = "FILL"; - view.clipsContent = true; - view.overflowDirection = "VERTICAL"; - const tree = append(view, frame(ctx, `Block tree / ${REACT_FILES.titlebar.name}`, { width: TREE_W, py: 20, fill: COLORS.editor })); + append(row, bar, true); + const view = filled(ctx, editor, "Preview view", { px: VIEW_PAD }); + const tree = append(view, frame(ctx, `Block tree / ${REACT_FILES.titlebar.name}`, { width: 10, py: 20, fill: COLORS.editor }), true); const { list } = levelList(ctx, tree, "Level 0"); const blocks = [...TITLEBAR_IMPORTS.map(([line]) => [line, lib.titlebarBlocks.get(line)]), [TITLEBAR_CARD.line, lib.reactCards.get(TITLEBAR_CARD.name)]]; for (const [line, component] of blocks) { diff --git a/docs/prototype/code-reader/figma-plugin/test.js b/docs/prototype/code-reader/figma-plugin/test.js index 8d1aa8b..6b79c3a 100644 --- a/docs/prototype/code-reader/figma-plugin/test.js +++ b/docs/prototype/code-reader/figma-plugin/test.js @@ -788,6 +788,49 @@ test("the React 组件 menu item builds its own page with four boards, under the } }); +// 排得下:自动排版的容器装得下自己的内容(竖排加起来不比容器高、每个不比容器宽;横排不折行的一排加起来不比容器宽; +// 折行的每一项不比容器宽);文字要么占满父容器,要么不比父容器可用的宽度宽。返回放不下的地方。 +function misfits(root) { + const found = []; + const walk = (node) => { + if (!node.visible) return; + if (node.layoutMode !== "NONE") { + const flow = node.children.filter((child) => child.visible && child.layoutPositioning !== "ABSOLUTE"); + const innerW = node.width - node.paddingLeft - node.paddingRight + 0.5; + const innerH = node.height - node.paddingTop - node.paddingBottom + 0.5; + const gaps = Math.max(0, flow.length - 1) * node.itemSpacing; + const sum = (key) => flow.reduce((total, child) => total + child[key], 0) + gaps; + if (node.layoutMode === "VERTICAL" && sum("height") > innerH) found.push(`${node.name} 高度不够`); + if (node.layoutMode === "HORIZONTAL" && node.layoutWrap !== "WRAP" && sum("width") > innerW) found.push(`${node.name} 一排放不下`); + for (const child of flow) { + if (child.width > innerW) found.push(`${child.name} 比 ${node.name} 宽`); + if (child.type === "TEXT" && child.layoutSizingHorizontal !== "FILL" && child.width > innerW) found.push(`文字 ${child.name} 超出 ${node.name}`); + } + } + node.children.forEach(walk); + }; + walk(root); + return found; +} + +test("React boards show everything: windows grow with the preview, cards fit the preview width, nothing is cut", async () => { + const { result } = await reactPage(); + for (const screen of result.screens) assert.deepEqual(misfits(screen), [], `${screen.name} has nothing cut off`); + const { simple } = result.boards; + // 窗口不裁:按内容撑高,预览区不滚动、装得下整棵块树;块树占满预览区的宽度 + assert.equal(simple.screen.clipsContent, false); + assert.equal(simple.view.clipsContent, false); + const main = named(simple.screen, "Main"); + assert.ok(simple.screen.height >= named(simple.screen, "Title bar").height + main.height + named(simple.screen, "Status bar").height); + assert.ok(simple.view.height >= simple.tree.height + simple.view.paddingTop + simple.view.paddingBottom); + assert.ok(named(main, "Editor").height >= named(main, "Editor").children.reduce((total, child) => total + child.height, 0)); + assert.equal(simple.tree.width, simple.view.width - 2 * simple.view.paddingLeft); + assert.equal(named(simple.tree, `Block / L${TITLEBAR_CARD.line}`).layoutSizingHorizontal, "FILL"); + // 活动栏、侧栏跟着编辑区拉到一样高 + assert.equal(named(main, "Activity bar").height, main.height); + assert.equal(named(main, "Sidebar").height, main.height); +}); + 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;