📝 docs(code-reader): React useState 状态卡片新设计的原型
This commit is contained in:
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 |
+20
-986
File diff suppressed because it is too large.
Load diff
+1106
File diff suppressed because it is too large.
Load diff
@@ -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\";",
|
||||
"",
|
||||
"/**",
|
||||
" * 桌面壳的唯一登录面:没有首页、没有弹窗、没有关闭。",
|
||||
" *",
|
||||
" * ",
|
||||
" */",
|
||||
"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
Reference in new issue
Block a user