🐛 fix(prototype): React 组件页窗口按内容撑高不裁,块树占满预览区宽度,侧栏拉宽放下深层长文件名,属性名一列放得下五个字;测试加排得下的防回归检查

This commit is contained in:
cruldra committed 2026-10-03 03:11:46 +08:00
1 parent caeeabebb3
commit ad2efdcdae
3 files changed
+64 -16

No files matched your search

@@ -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 组件:渲染大纲每层左边一条竖线,原生标签灰、组件青,一眼分得开;「当 … 时」「对 … 的每一项」「是 / 否」小框和后面的元素在一行里对齐
+19 -15
View File
@@ -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) {
+43
View File
@@ -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;