📝 docs(code-reader): useState 状态卡片原型的调用树挪进解构表每一行

This commit is contained in:
cruldra committed 2026-10-16 21:17:22 +11:00
1 parent 785bfd4f78
commit 719453a28b
5 files changed
+2619 -2215

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 KiB

After

Width:  |  Height:  |  Size: 775 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

After

Width:  |  Height:  |  Size: 268 KiB

+28 -18
View File
@@ -503,9 +503,25 @@ function group(parent, segs, color, bg) {
// ─── 卡片 ────────────────────────────────────────────────────────────────────
/**
* 卡片右上角:问 AI、编辑;给了 `calls` 再加调用树,同 webview 的 BlockTools:调用次数大于 0 时图标后面跟一个小圆标
* (徽标底色、字色取 Dark Modern 的 badge),是 0 时只有图标。返回这一排的宽度。
* 调用树按钮,同 webview 的 BlockTools:git-branch 图标,调用次数大于 0 时后面跟一个小圆标
* (徽标底色、字色取 Dark Modern 的 badge),是 0 时只有图标。`tip` 是悬停文字,记在图层名里。返回按钮的宽度。
*/
function callTreeButton(parent, calls, tip) {
const name = tip ? "git-branch / " + tip : "git-branch";
if (calls === 0) {
const tree = row(parent, name, { width: 22, height: 20, justify: "center", radius: 4 });
icon(tree, "git-branch", C.desc, 14);
return 22;
}
const tree = row(parent, name, { width: "fit_content", height: 20, padding: [0, 4], gap: 1, radius: 4 });
icon(tree, "git-branch", C.desc, 14);
const badge = row(tree, "Count", { width: 14, height: 14, justify: "center", radius: 7, fill: "#616161" });
// 一两个字的数字按内容长会被导出时挤小,宽度定死
text(badge, String(calls), { size: 9, weight: 700, color: "#f8f8f8", lh: 14, fixedW: Math.ceil(widthOf(String(calls), 9, false)) + 1 });
return 4 + 14 + 1 + 14 + 4;
}
/** 卡片右上角:问 AI、编辑;给了 `calls` 再加调用树按钮。返回这一排的宽度。 */
function tools(parent, calls) {
const bar = row(parent, "Tools", { width: "fit_content", height: 20, gap: 2 });
for (const name of ["sparkles", "pencil"]) {
@@ -514,28 +530,22 @@ function tools(parent, calls) {
}
if (calls == null)
return 22 * 2 + 2;
if (calls === 0) {
const tree = row(bar, "git-branch", { width: 22, height: 20, justify: "center", radius: 4 });
icon(tree, "git-branch", C.desc, 14);
return 22 * 3 + 2 * 2;
}
const tree = row(bar, "git-branch", { width: "fit_content", height: 20, padding: [0, 4], gap: 1, radius: 4 });
icon(tree, "git-branch", C.desc, 14);
const badge = row(tree, "Count", { width: 14, height: 14, justify: "center", radius: 7, fill: "#616161" });
text(badge, String(calls), { size: 9, weight: 700, color: "#f8f8f8", lh: 14 });
return 22 * 2 + 2 + 2 + (4 + 14 + 1 + 14 + 4);
return 22 * 2 + 2 + 2 + callTreeButton(bar, calls);
}
/** 悬停提示气泡,样子同 webview 的 data-tooltip:悬停浮层的底色、边框、11px 字。`x`、`y` 是它在父节点里的位置。 */
/**
* 悬停提示气泡,样子同 webview 的 data-tooltip:悬停浮层的底色、边框、11px 字。
* `x`、`y` 是它在自由摆放的父节点里的位置;不给时跟着自动排版排在前一个兄弟后面,高度压成一行的 20px。
*/
function tooltip(parent, label, x, y) {
const bw = Math.ceil(widthOf(label, 11, false)) + 16;
const tip = I(parent, {
const placed = x != null ? { x: Math.round(x), y: Math.round(y) } : {};
const tip = I(parent, Object.assign({
type: "frame",
name: "Tooltip",
x: Math.round(x),
y: Math.round(y),
}, placed, {
width: bw,
height: 22,
height: x != null ? 22 : 20,
layout: "horizontal",
alignItems: "center",
justifyContent: "center",
@@ -543,7 +553,7 @@ function tooltip(parent, label, x, y) {
fill: solid("#202020"),
stroke: { thickness: 1, fill: solid("#454545") },
effects: [{ type: "shadow", offsetX: 0, offsetY: 2, blur: 8, spread: 0, color: "rgba(0,0,0,0.36)" }],
});
}));
text(tip, label, { size: 11, color: C.fg, lh: 14 });
return bw;
}
+33 -31
View File
@@ -1,6 +1,6 @@
// 代码阅读 React 状态卡片:useState 不再写成「钩子 useState」,而是一张「状态」卡片,讲清楚五件事:
// 它是普通变量也是 React 状态(左边变量、状态两个图标);它是数组解构(名称照源码写出解构,展开是每一项);
// 它来自 React 内置的 useState(悬停说明);状态类型;默认值。修改函数在哪调用,用右上角的调用树查。
// 它来自 React 内置的 useState(悬停说明);状态类型;默认值。解构出的每一项各有一个调用树按钮:当前值查谁用了它,修改函数查谁改了它。
// 画板一照抄 luffy-agent-platform 的 frontend/client-nextjs/src/app/desktop/login/page.tsx 第 1–37 行,
// 画板二是手写的三行,讲没标类型、函数形式的默认值、只取当前值三种写法。
// 窗口、对齐、别的卡片的画法在同目录 react-shared.js,接在它后面跑:`cat react-shared.js react-state.js | op design - --script`。
@@ -84,26 +84,36 @@ const VARIANTS = sampleFile({
// 种类「状态」:描边、淡底、两个图标都用变量色;左边先是变量卡片的图标,再是状态图标,悬停才出名字
const STATE = { label: "状态", color: "#9cdcfe", icon: "variable" };
const FROM_TIP = "React 内置钩子";
// 解构出的每一项:是什么、它那一行调用树按钮查什么(悬停文字)
const ITEMS = [
{ what: "当前值", tip: "谁用了这个状态" },
{ what: "修改函数", tip: "谁改了这个状态" },
];
/**
* 「来自」那一行悬停时的气泡在卡片里的位置:在 `useState` 几个字正上方居中、隔 6px。
* 内容区从卡片内边距(上 8、左 8)、折叠箭头一列(14)、两个图标(42)各隔 10px 往后排;
* 「来自」前面是名称一行和解构表(表头一行、每项一行,各带 1px 分隔线、上下各 2px 留白)。
* 「是什么」一格:说法后面跟这一项自己的调用树按钮,`calls` 是它在样例里出现几次;
* `bubble` 时按钮右边画出它的悬停气泡(上方是上一行的按钮,放上面会挡住它)。
*/
function fromBubble(labelW, items) {
const tableH = 2 + 24 + items * (24 + 1) + 2;
const rowTop = 8 + 20 + 3 + tableH + 3;
const valueX = 8 + 14 + 10 + 42 + 10 + labelW + 12;
const center = valueX + widthOf("useState", 13, true) / 1.04 / 2;
return { x: center - tooltipWidth(FROM_TIP) / 2, y: rowTop - 6 - 22 };
function whatCell(item, calls, bubble) {
return {
est: item.what,
extra: 6 + 37,
draw: (frame) => {
const line = row(frame, "What", { gap: 6, height: 20, width: "fit_content" });
text(line, item.what, { size: 12.5, lh: 20 });
callTreeButton(line, calls, item.tip);
if (bubble)
tooltip(line, item.tip);
},
};
}
/**
* 一处 useState:名称照源码写出解构(后面灰字「数组解构」,展开是「第几项 | 名称 | 是什么」)/ 来自 / 状态类型 / 默认值。
* `names` 是解构出的名字,第二个是修改函数;有修改函数时右上角有调用树,`calls` 是它被调用几次。
* `type` 是状态类型(没写时「未标注」),`initial` 是默认值的着色段。
* `bubble` 画出「来自」的悬停气泡:卡片和气泡放进一个自由摆放的框,气泡在前,才画在卡片上层。
* 一处 useState:名称照源码写出解构(后面灰字「数组解构」,展开是「第几项 | 名称 | 是什么」,每一项一个调用树按钮)/
* 来自(悬停说是 React 内置钩子)/ 状态类型 / 默认值。
* `names` 是解构出的名字,第二个是修改函数;`calls` 是各项在样例里出现几次。
* `type` 是状态类型(没写时「未标注」),`initial` 是默认值的着色段;`bubble` 画出修改函数那一行调用树按钮的悬停气泡。
*/
function stateCard(s) {
return (parent, w) => {
@@ -111,15 +121,7 @@ function stateCard(s) {
{ name: "variable", color: STATE.color, tip: "变量" },
{ name: "toggle-right", color: STATE.color, tip: "React 状态" },
];
let holder = parent;
let place = {};
if (s.bubble) {
holder = box(parent, "Hover / 来自", { layout: "none", width: w, height: "fit_content" });
const spot = fromBubble(76, s.names.length);
tooltip(holder, FROM_TIP, spot.x, spot.y);
place = { x: 0, y: 0 };
}
const ctx = card(holder, STATE, w, Object.assign({ name: s.names[0], icons, calls: s.names.length > 1 ? s.calls : null }, place));
const ctx = card(parent, STATE, w, { name: s.names[0], icons });
prop(ctx, "名称", (frame) => {
const line = row(frame, "Destructure", { gap: 8, height: 20, width: "fit_content" });
const parts = [seg("[", C.fg)];
@@ -133,8 +135,8 @@ function stateCard(s) {
text(line, "数组解构", { size: 12.5, color: C.desc });
}, { fold: "open" });
table(ctx, [{ name: "第几项", mono: false }, { name: "名称", color: C.variable }, { name: "是什么", mono: false }],
s.names.map((name, i) => [indexCell(i + 1), px(name, i === 0 ? C.variable : C.fn), i === 0 ? "当前值" : "修改函数"]));
prop(ctx, "来自", px("useState", C.fn));
s.names.map((name, i) => [indexCell(i + 1), px(name, i === 0 ? C.variable : C.fn), whatCell(ITEMS[i], s.calls[i], s.bubble && i === 1)]));
prop(ctx, "来自", (frame) => text(frame, px("useState", C.fn), { mono: true, name: "useState(悬停:React 内置钩子)" }));
prop(ctx, "状态类型", s.type || px("未标注", C.desc), { mono: Boolean(s.type) });
prop(ctx, "默认值", s.initial);
};
@@ -175,7 +177,7 @@ function searchParamsHook(parent, w) {
const ACTIVE_TAB = stateCard({
names: ["activeTab", "setActiveTab"],
calls: 1,
calls: [1, 1],
type: [link("LoginTab", C.type)],
initial: px("\"sms\"", C.string),
bubble: true,
@@ -183,7 +185,7 @@ const ACTIVE_TAB = stateCard({
const PASSWORD_VIEW = stateCard({
names: ["passwordView", "setPasswordView"],
calls: 1,
calls: [1, 1],
type: [link("PasswordView", C.type)],
initial: px("\"login\"", C.string),
});
@@ -235,16 +237,16 @@ windowOf("Prototype / React 状态 · 其他写法", WIN_W + BOARD_GAP, VARIANTS
{
from: 1,
to: 1,
draw: stateCard({ names: ["open", "setOpen"], calls: 0, initial: cx("假") }),
draw: stateCard({ names: ["open", "setOpen"], calls: [0, 0], initial: cx("假") }),
},
{
from: 2,
to: 2,
draw: stateCard({ names: ["items", "setItems"], calls: 0, type: [link("Item", C.type), seg("[]", C.fg)], initial: [seg("首次渲染时调用 ", C.fg)].concat(cx("loadItems()")) }),
draw: stateCard({ names: ["items", "setItems"], calls: [0, 0], type: [link("Item", C.type), seg("[]", C.fg)], initial: [seg("首次渲染时调用 ", C.fg)].concat(cx("loadItems()")) }),
},
{
from: 3,
to: 3,
draw: stateCard({ names: ["count"], initial: cx("0") }),
draw: stateCard({ names: ["count"], calls: [0], initial: cx("0") }),
},
]);
File diff suppressed because it is too large. Load diff