📝 docs(code-reader): React useState 状态卡片新设计的原型

This commit is contained in:
cruldra committed 2026-10-16 21:11:47 +11:00
1 parent f421d9c609
commit 785bfd4f78
6 files changed
+20560 -986

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+250
View File
@@ -0,0 +1,250 @@
// 代码阅读 React 状态卡片:useState 不再写成「钩子 useState」,而是一张「状态」卡片,讲清楚五件事:
// 它是普通变量也是 React 状态(左边变量、状态两个图标);它是数组解构(名称照源码写出解构,展开是每一项);
// 它来自 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`。
// ─── 样例源码 ────────────────────────────────────────────────────────────────
const LOGIN = sampleFile({
project: "client-nextjs",
file: "page.tsx",
crumbs: ["src", "app", "desktop", "login", "page.tsx"],
tree: [
{ kind: "folder", name: "app", open: true, depth: 0 },
{ kind: "folder", name: "desktop", open: true, depth: 1 },
{ kind: "folder", name: "login", open: true, depth: 2 },
{ kind: "file", name: "page.test.tsx", depth: 3 },
{ kind: "file", name: "page.tsx", depth: 3 },
{ kind: "file", name: "layout.test.tsx", depth: 2 },
{ kind: "file", name: "layout.tsx", depth: 2 },
{ kind: "folder", name: "desktop-lab", open: false, depth: 1 },
{ kind: "folder", name: "home", open: false, depth: 1 },
],
count: 7,
source: [
"\"use client\";",
"",
"import { useState } from \"react\";",
"import { useRouter, useSearchParams } from \"next/navigation\";",
"",
"import { LoginPanel } from \"@/components/auth/login/LoginPanel\";",
"import type { LoginTab, PasswordView } from \"@/components/auth/login/types\";",
"import { DESKTOP_ENTRY } from \"@/lib/desktop-shell\";",
"import { isSafeReturnTo } from \"@/lib/safe-redirect\";",
"",
"/**",
" * 桌面壳的唯一登录面:没有首页、没有弹窗、没有关闭。",
" *",
" * ![桌面登录](docs/prototype/desktop-login/openpencil/exports/desktop-login.png)",
" */",
"const DesktopLoginPage = () => {",
" const router = useRouter();",
" const from = useSearchParams().get(\"from\");",
" const [activeTab, setActiveTab] = useState<LoginTab>(\"sms\");",
" const [passwordView, setPasswordView] = useState<PasswordView>(\"login\");",
"",
" return (",
" <main className=\"flex flex-1 items-center justify-center\">",
" <div className=\"home-paper w-115 rounded-3xl bg-surface shadow-card\">",
" <LoginPanel",
" activeTab={activeTab}",
" onTabChange={setActiveTab}",
" passwordView={passwordView}",
" onPasswordView={setPasswordView}",
" onSuccess={() => router.replace(from && isSafeReturnTo(from) ? from : DESKTOP_ENTRY)}",
" />",
" </div>",
" </main>",
" );",
"};",
"",
"export default DesktopLoginPage;",
],
});
const VARIANTS = sampleFile({
project: "web",
file: "examples.tsx",
crumbs: ["src", "examples.tsx"],
tree: [
{ kind: "folder", name: "src", open: true, depth: 0 },
{ kind: "file", name: "examples.tsx", depth: 1 },
],
count: 3,
source: [
"const [open, setOpen] = useState(false)",
"const [items, setItems] = useState<Item[]>(() => loadItems())",
"const [count] = useState(0)",
],
});
// ─── 状态卡片 ────────────────────────────────────────────────────────────────
// 种类「状态」:描边、淡底、两个图标都用变量色;左边先是变量卡片的图标,再是状态图标,悬停才出名字
const STATE = { label: "状态", color: "#9cdcfe", icon: "variable" };
const FROM_TIP = "React 内置钩子";
/**
* 「来自」那一行悬停时的气泡在卡片里的位置:在 `useState` 几个字正上方居中、隔 6px。
* 内容区从卡片内边距(上 8、左 8)、折叠箭头一列(14)、两个图标(42)各隔 10px 往后排;
* 「来自」前面是名称一行和解构表(表头一行、每项一行,各带 1px 分隔线、上下各 2px 留白)。
*/
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 };
}
/**
* 一处 useState:名称照源码写出解构(后面灰字「数组解构」,展开是「第几项 | 名称 | 是什么」)/ 来自 / 状态类型 / 默认值。
* `names` 是解构出的名字,第二个是修改函数;有修改函数时右上角有调用树,`calls` 是它被调用几次。
* `type` 是状态类型(没写时「未标注」),`initial` 是默认值的着色段。
* `bubble` 画出「来自」的悬停气泡:卡片和气泡放进一个自由摆放的框,气泡在前,才画在卡片上层。
*/
function stateCard(s) {
return (parent, w) => {
const icons = [
{ 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));
prop(ctx, "名称", (frame) => {
const line = row(frame, "Destructure", { gap: 8, height: 20, width: "fit_content" });
const parts = [seg("[", C.fg)];
s.names.forEach((name, i) => {
if (i > 0)
parts.push(seg(", ", C.fg));
parts.push(seg(name, i === 0 ? C.variable : C.fn, { w: 600 }));
});
parts.push(seg("]", C.fg));
text(line, parts, { mono: true });
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));
prop(ctx, "状态类型", s.type || px("未标注", C.desc), { mono: Boolean(s.type) });
prop(ctx, "默认值", s.initial);
};
}
/** 「第几项」一格:只有一个数字,宽度定死,按内容长会被导出时挤小。 */
function indexCell(n) {
return { est: String(n), draw: (frame) => text(frame, String(n), { size: 12.5, color: C.desc, lh: 20, fixedW: 12 }) };
}
// ─── 画板一:登录页 ──────────────────────────────────────────────────────────
const LOGIN_PAGE = componentCard({
name: "DesktopLoginPage",
fold: "open",
tags: [DEFAULT_EXPORT, CLIENT],
doc: ["桌面壳的唯一登录面:没有首页、没有弹窗、没有关闭。"],
props: [],
});
function routerHook(parent, w) {
hookCall(parent, w, {
lib: "next/navigation",
name: "useRouter",
target: "router",
returns: [["router", "路由器,用于页面跳转"]],
});
}
function searchParamsHook(parent, w) {
hookCall(parent, w, {
lib: "next/navigation",
name: "useSearchParams",
target: "from",
returns: [["from", [seg("URL 查询参数里 ", C.fg), seg("\"from\"", C.string), seg(" 的值", C.fg)]]],
});
}
const ACTIVE_TAB = stateCard({
names: ["activeTab", "setActiveTab"],
calls: 1,
type: [link("LoginTab", C.type)],
initial: px("\"sms\"", C.string),
bubble: true,
});
const PASSWORD_VIEW = stateCard({
names: ["passwordView", "setPasswordView"],
calls: 1,
type: [link("PasswordView", C.type)],
initial: px("\"login\"", C.string),
});
// DesktopLoginPage 返回的 JSX:className 收成「样式 N 个类」,回调照译
const LOGIN_OUTLINE = [
{
line: [el("main"), cls("样式 4 个类")],
kids: [
{
line: [el("div"), cls("样式 5 个类")],
kids: [
{
line: [
el("LoginPanel"),
at("activeTab", [link("activeTab", C.variable)]),
at("onTabChange", [link("setActiveTab", C.fn)]),
at("passwordView", [link("passwordView", C.variable)]),
at("onPasswordView", [link("setPasswordView", C.fn)]),
at("onSuccess", cx("() => router.replace(如果 from 并且 isSafeReturnTo(from) 则 from 否则 DESKTOP_ENTRY)")),
],
},
],
},
],
},
];
windowOf("Prototype / React 状态 · 登录页", 0, LOGIN, [
{ from: 1, to: 2 },
{ from: 3, to: 3, draw: importCard("\"react\"", 1) },
{ from: 4, to: 5, draw: importCard("\"next/navigation\"", 2) },
{ from: 6, to: 6, draw: importCard("\"@/components/auth/login/LoginPanel\"", 1) },
{ from: 7, to: 7, draw: importCard("\"@/components/auth/login/types\"", 2, true) },
{ from: 8, to: 8, draw: importCard("\"@/lib/desktop-shell\"", 1) },
{ from: 9, to: 10, draw: importCard("\"@/lib/safe-redirect\"", 1) },
{ from: 11, to: 16, draw: LOGIN_PAGE },
{ from: 17, to: 17, depth: 1, draw: routerHook },
{ from: 18, to: 18, depth: 1, draw: searchParamsHook },
{ from: 19, to: 19, depth: 1, draw: ACTIVE_TAB },
{ from: 20, to: 21, depth: 1, draw: PASSWORD_VIEW },
{ from: 22, to: 34, depth: 1, last: true, draw: renderCard(LOGIN_OUTLINE, "DesktopLoginPage") },
{ from: 35, to: 37 },
]);
// ─── 画板二:其他写法 ────────────────────────────────────────────────────────
windowOf("Prototype / React 状态 · 其他写法", WIN_W + BOARD_GAP, VARIANTS, [
{
from: 1,
to: 1,
draw: stateCard({ names: ["open", "setOpen"], calls: 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()")) }),
},
{
from: 3,
to: 3,
draw: stateCard({ names: ["count"], initial: cx("0") }),
},
]);
File diff suppressed because it is too large. Load diff