hd-garden 0.3.11 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ import { ParityInteractions } from "../../ui/react/parity";
2
+
1
3
  const slices = [
2
4
  { label: "商品销售", value: 78.2, color: "#539af8" },
3
5
  { label: "平台与营销服务", value: 9.4, color: "#6bc67f" },
@@ -37,13 +39,13 @@ function Loop() {
37
39
 
38
40
  export default function UiParityPage() {
39
41
  const total = slices.reduce((sum, slice) => sum + slice.value, 0);
40
- let angle = -Math.PI / 2;
41
- const arcs = slices.map((slice) => {
42
+ const arcs = slices.reduce<{ angle: number; items: Array<(typeof slices)[number] & { path: string }> }>((state, slice) => {
42
43
  const sweep = (slice.value / total) * Math.PI * 2;
43
- const path = ring(angle, sweep);
44
- angle += sweep;
45
- return { ...slice, path };
46
- });
44
+ return {
45
+ angle: state.angle + sweep,
46
+ items: [...state.items, { ...slice, path: ring(state.angle, sweep) }],
47
+ };
48
+ }, { angle: -Math.PI / 2, items: [] }).items;
47
49
  const rows = [
48
50
  ["消费者状态", "不选择拼多多的原因", "对应的经济学概念"],
49
51
  ["不知道拼多多更便宜", "没有进行跨平台比价", "信息不对称、搜索成本"],
@@ -115,6 +117,7 @@ export default function UiParityPage() {
115
117
  <p>更丰富的供给与交易数据</p>
116
118
  <Loop />
117
119
  </section>
120
+ <ParityInteractions />
118
121
  </main>
119
122
  );
120
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-garden",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "private": false,
5
5
  "description": "用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。",
6
6
  "license": "MIT",
@@ -74,8 +74,8 @@
74
74
  "@codemirror/state": "^6.7.6",
75
75
  "@codemirror/view": "^6.43.14",
76
76
  "@phosphor-icons/react": "^2.1.10",
77
- "@tailwindcss/postcss": "4.2.1",
78
- "@vitejs/plugin-react": "6.0.4",
77
+ "@tailwindcss/postcss": "4.3.3",
78
+ "@vitejs/plugin-react": "6.1.2",
79
79
  "@vitejs/plugin-rsc": "0.5.30",
80
80
  "acorn": "^8.19.0",
81
81
  "fuse.js": "^7.5.0",
@@ -91,10 +91,10 @@
91
91
  "micromark-extension-mdx-expression": "^3.0.1",
92
92
  "micromark-extension-mdx-jsx": "^3.0.2",
93
93
  "next": "16.3.0",
94
- "react": "19.2.8",
95
- "react-dom": "19.2.8",
94
+ "react": "19.3.0",
95
+ "react-dom": "19.3.0",
96
96
  "react-markdown": "^10.1.0",
97
- "react-server-dom-webpack": "19.2.8",
97
+ "react-server-dom-webpack": "19.3.0",
98
98
  "rehype-autolink-headings": "^7.1.0",
99
99
  "rehype-highlight": "^7.0.2",
100
100
  "rehype-katex": "^7.0.1",
@@ -116,8 +116,8 @@
116
116
  },
117
117
  "devDependencies": {
118
118
  "@types/node": "22.19.19",
119
- "@types/react": "19.2.17",
120
- "@types/react-dom": "19.2.3",
119
+ "@types/react": "19.3.0",
120
+ "@types/react-dom": "19.3.0",
121
121
  "esbuild": "^0.28.2",
122
122
  "eslint": "9.39.4",
123
123
  "eslint-config-next": "16.2.12"
package/ui/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # Intelligent UI
2
+
3
+ `ui/` 是一块可以单独拿走的交互组件模块。它解析作者写好的 `ui` 块、检查目录、算出静态快照,并在浏览器里把块画出来。它不知道文章存在哪、谁登录了后台、模型地址是什么。
4
+
5
+ 现在它还放在 Garden 仓库里。拆仓库以后再做。这一层只把接口和依赖方向定死:模块不引用 Garden,Garden 作为宿主调用模块。
6
+
7
+ ## 模块拥有什么
8
+
9
+ | 路径 | 职责 |
10
+ | --- | --- |
11
+ | `index.mjs` | 给非 React 宿主的稳定入口:编译、检查、沙箱协议 |
12
+ | `core/` | 解析、表达式、目录、校验、静态快照。不依赖 React |
13
+ | `host.mjs` | 宿主契约和 `<Sandbox>` 的 iframe 协议 |
14
+ | `react/` | `UiFigure` 和目录里的组件,样式在 `react/ui.css` |
15
+ | `prompt/` | 写给模型的目录说明、配方和规则。宿主负责发送 |
16
+ | `preview/` | 编辑器里的预览挂载 |
17
+ | `evals/` | 评测样本,由宿主的工作台读取 |
18
+
19
+ ## 宿主可以做的事
20
+
21
+ Garden 是当前的宿主,以后也可以是别的静态站点。宿主负责:
22
+
23
+ - 从 Markdown 里找出 `ui` 围栏,调用 `compileUiBlock` / `checkUiBlock`
24
+ - 把通过检查的 payload 交给 `UiFigure`
25
+ - 在写作期调用模型。阅读页不调用模型
26
+ - 决定要不要实现 `issuePrompt`。公开博客留空,聊天宿主可以接上
27
+
28
+ ```js
29
+ import { compileUiBlock, createHost } from "./ui/index.mjs";
30
+ import { UiFigure } from "./ui/react";
31
+
32
+ const { payload } = compileUiBlock(body);
33
+ const host = createHost({
34
+ openUrl(url) {},
35
+ copy(text) {},
36
+ // issuePrompt 只有聊天宿主才提供
37
+ });
38
+ ```
39
+
40
+ `UiFigure` 的 `host` 不传时,沙箱里的 `GardenUI.issuePrompt` 返回 false。`openUrl` 缺省时模块用 `window.open` 打开 https 链接。`copy` 缺省时模块写剪贴板。
41
+
42
+ 流式补全留在宿主。模块提供 `parseUiBodyPartial`:丢掉最后一行不完整的标签,返回已经闭合的树。阅读页渲染的是提交进 Git 的完整块。
43
+
44
+ ## 不能跨过去的边界
45
+
46
+ `ui/` 里的源文件不能 import `app/`、`scripts/`、`bin/`、`content/`、`editors/`,也不能 import Next 或 Vinext。这条由 `tests/ui-boundary.test.mjs` 守着。
47
+
48
+ 宿主可以 import `ui/`。模块不能 import 宿主。
49
+
50
+ 同名目录里的图片和 CSV 由宿主在编译时通过 `compileUiBlock` 的 `resolveAsset` 换成公开地址。模块不读文件系统上的文章。
51
+
52
+ ## Sandbox
53
+
54
+ 目录画不了的画布(地图、三维、自由动画)写成 `<Sandbox>`,不要为此再加组件。`html`、`css`、`js` 必须是字面量。
55
+
56
+ iframe 使用 `sandbox="allow-scripts"`,不带 `allow-same-origin`。文档带上 `default-src 'none'`,只允许内联样式、内联脚本和 `data:` 图片。远程脚本、远程图片、`url(http…)`、动态 `import()` 在校验和绘制时都会被拒绝。
57
+
58
+ 主题变量由模块注入,跟随页面的 `data-theme`:
59
+
60
+ `--viz-text` `--viz-muted` `--viz-subtle` `--viz-panel` `--viz-border` `--viz-accent` `--viz-series` `--viz-track`
61
+
62
+ 画布里的脚本通过 `GardenUI` 跟宿主说话。`postMessage` 的 `source` 固定是 `garden-ui-sandbox`。父页面只接受来自这块 iframe 的消息。
63
+
64
+ | 方法 | 宿主行为 |
65
+ | --- | --- |
66
+ | `GardenUI.openUrl(url)` | 只接受 `https` |
67
+ | `GardenUI.issuePrompt(text)` | 宿主没实现时返回 false,不调用模型 |
68
+ | `GardenUI.copy(text)` | 把最多 4000 字交给宿主复制 |
69
+
70
+ 画布自己上报高度,模块把 iframe 撑到内容高度,上限 4000px。
@@ -467,6 +467,20 @@ export const COMPONENTS = {
467
467
  children: "none",
468
468
  example: "<Code lang=\"go\" highlight=\"2\" code={`a := 1\nb := a + 1`} />",
469
469
  },
470
+ Sandbox: {
471
+ group: "媒体与代码",
472
+ phase: 6,
473
+ summary: "自包含画布。地图、三维和目录里没有的动画写在这里,不能加载远程资源",
474
+ phrases: ["做一个小工具", "画一张能转的图", "在图上播放"],
475
+ props: {
476
+ title: { type: "string", desc: "画布名称,读屏用" },
477
+ html: { type: "string", literal: true, required: true, desc: "片段 HTML,不能含远程地址" },
478
+ css: { type: "string", literal: true, desc: "只作用于这块画布的样式" },
479
+ js: { type: "string", literal: true, desc: "内联脚本。可用 GardenUI.openUrl、GardenUI.copy 和 GardenUI.issuePrompt" },
480
+ },
481
+ children: "none",
482
+ example: "<Sandbox title=\"计数\" html={\"<p id=\\\"n\\\">0</p><button id=\\\"b\\\">加一</button>\"} js=\"document.getElementById('b').onclick=()=>{const n=document.getElementById('n');n.textContent=String(Number(n.textContent)+1)}\" />",
483
+ },
470
484
  Diagram: {
471
485
  group: "媒体与代码",
472
486
  phase: 6,
@@ -161,6 +161,8 @@ export function toDoc(payload) {
161
161
  case "Data":
162
162
  case "Button":
163
163
  return [];
164
+ case "Sandbox":
165
+ return [{ k: "p", inl: [text(`(网页中可交互:${v("title") || "画布"})`)] }];
164
166
  case "If":
165
167
  case "Each":
166
168
  return expand([node], ctx).flatMap((item) => block(item.node, item.ctx));
@@ -1,4 +1,5 @@
1
1
  import { COMPONENTS, COMPONENT_NAMES, TONES } from "./catalog.mjs";
2
+ import { sandboxLimit, sandboxRemote } from "../host.mjs";
2
3
  import { analyzeExpr, createBudget, evaluate, ExprError } from "./expr.mjs";
3
4
  import { compiledOf, ID_PATTERN, intentHash, resolveSidecarPath, splitUiBody } from "./fence.mjs";
4
5
  import { isValidFormat } from "./format.mjs";
@@ -529,6 +530,16 @@ export function checkUiBlock(body, options = {}) {
529
530
  add("ui-prop", bodyLine(node.l), "<Diagram> 只支持 graph、flowchart 和 stateDiagram");
530
531
  }
531
532
  break;
533
+ case "Sandbox": {
534
+ for (const kind of ["html", "css", "js"]) {
535
+ const text = values[kind];
536
+ if (typeof text !== "string") continue;
537
+ if (text.length > sandboxLimit(kind)) add("ui-limit", bodyLine(node.l), `<Sandbox> 的 ${kind} 超过 ${sandboxLimit(kind)} 字`);
538
+ const remote = sandboxRemote(text);
539
+ if (remote) add("ui-prop", bodyLine(node.l), `<Sandbox> 的 ${kind}:${remote}`);
540
+ }
541
+ break;
542
+ }
532
543
  case "Table": {
533
544
  const columns = Array.isArray(values.columns) ? values.columns : [];
534
545
  childContext.columns = columns.length;
package/ui/host.mjs ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Host contract for the Intelligent UI module.
3
+ *
4
+ * The module (`ui/`) parses, checks, and draws blocks. It does not know about
5
+ * Garden posts, Git, admin sessions, or which model is configured. A host
6
+ * supplies those edges. The public blog host leaves `issuePrompt` unset, so a
7
+ * sandbox cannot start a model turn. A chat host may implement it.
8
+ */
9
+
10
+ export const SANDBOX_SOURCE = "garden-ui-sandbox";
11
+
12
+ /** @typedef {{
13
+ * openUrl?: (url: string) => void,
14
+ * issuePrompt?: (text: string) => void,
15
+ * copy?: (text: string) => void,
16
+ * }} UiHost
17
+ */
18
+
19
+ export function createHost(partial = {}) {
20
+ return {
21
+ openUrl: typeof partial.openUrl === "function" ? partial.openUrl : null,
22
+ issuePrompt: typeof partial.issuePrompt === "function" ? partial.issuePrompt : null,
23
+ copy: typeof partial.copy === "function" ? partial.copy : null,
24
+ };
25
+ }
26
+
27
+ const LIMITS = { html: 12000, css: 8000, js: 8000 };
28
+
29
+ export function sandboxLimit(kind) {
30
+ return LIMITS[kind] ?? 0;
31
+ }
32
+
33
+ /** Rejects anything that would pull a remote resource into the frame. */
34
+ export function sandboxRemote(source) {
35
+ const text = String(source ?? "");
36
+ if (/<script\b[^>]*\bsrc\s*=/iu.test(text)) return "不能加载远程脚本";
37
+ if (/\b(?:import|require)\s*\(/u.test(text)) return "不能动态加载模块";
38
+ if (/url\(\s*['"]?https?:/iu.test(text)) return "样式不能引用远程地址";
39
+ if (/<(?:img|iframe|object|embed|link|video|audio)\b[^>]+(?:src|href)\s*=\s*['"]https?:/iu.test(text)) {
40
+ return "不能嵌入远程地址";
41
+ }
42
+ return null;
43
+ }
44
+
45
+ export function isHttpsUrl(value) {
46
+ try {
47
+ const url = new URL(String(value));
48
+ return url.protocol === "https:";
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ const THEMES = {
55
+ light: {
56
+ color: "#0d0d0d",
57
+ text: "#0d0d0d",
58
+ muted: "#5d5d5d",
59
+ subtle: "#8f8f8f",
60
+ panel: "#ffffff",
61
+ border: "#0000001a",
62
+ accent: "#0169cc",
63
+ series: "#0285ff",
64
+ track: "#0d0d0d1f",
65
+ },
66
+ dark: {
67
+ color: "#ededed",
68
+ text: "#ededed",
69
+ muted: "#cdcdcd",
70
+ subtle: "#afafaf",
71
+ panel: "#212121",
72
+ border: "#ffffff1a",
73
+ accent: "#66b5ff",
74
+ series: "#0285ff",
75
+ track: "#ffffff26",
76
+ },
77
+ };
78
+
79
+ function tokenCss(theme) {
80
+ const token = THEMES[theme] ?? THEMES.light;
81
+ return `
82
+ :root {
83
+ color: ${token.color};
84
+ background: transparent;
85
+ font-family: -apple-system-body, ui-sans-serif, system-ui, "Segoe UI", Helvetica, Arial, sans-serif;
86
+ --viz-text: ${token.text};
87
+ --viz-muted: ${token.muted};
88
+ --viz-subtle: ${token.subtle};
89
+ --viz-panel: ${token.panel};
90
+ --viz-border: ${token.border};
91
+ --viz-accent: ${token.accent};
92
+ --viz-series: ${token.series};
93
+ --viz-track: ${token.track};
94
+ }
95
+ body { margin: 0; }
96
+ `;
97
+ }
98
+
99
+ /** Keeps author CSS and script inside their own tags. */
100
+ function keepInside(source, closing) {
101
+ return String(source ?? "").replaceAll(closing, `${closing.slice(0, 1)}\\${closing.slice(1)}`);
102
+ }
103
+
104
+ /**
105
+ * Document for a sandboxed AppBlock. Scripts are inline only. The frame has
106
+ * no network. `canPrompt` is true only when the host implements `issuePrompt`.
107
+ */
108
+ export function buildSrcdoc({ html = "", css = "", js = "", canPrompt = false, theme = "light" } = {}) {
109
+ const bridge = `
110
+ const GardenUI = {
111
+ canPrompt: ${canPrompt ? "true" : "false"},
112
+ openUrl(url) {
113
+ parent.postMessage({ source: ${JSON.stringify(SANDBOX_SOURCE)}, type: "openUrl", url: String(url) }, "*");
114
+ },
115
+ issuePrompt(text) {
116
+ if (!GardenUI.canPrompt) return false;
117
+ parent.postMessage({ source: ${JSON.stringify(SANDBOX_SOURCE)}, type: "issuePrompt", text: String(text).slice(0, 500) }, "*");
118
+ return true;
119
+ },
120
+ copy(text) {
121
+ parent.postMessage({ source: ${JSON.stringify(SANDBOX_SOURCE)}, type: "copy", text: String(text).slice(0, 4000) }, "*");
122
+ return true;
123
+ }
124
+ };
125
+ const report = () => {
126
+ let bottom = 0;
127
+ for (const el of document.body.children) {
128
+ if (el.tagName === "SCRIPT") continue;
129
+ bottom = Math.max(bottom, el.getBoundingClientRect().bottom);
130
+ }
131
+ parent.postMessage({ source: ${JSON.stringify(SANDBOX_SOURCE)}, type: "height", height: Math.ceil(bottom + 8) }, "*");
132
+ };
133
+ addEventListener("load", report);
134
+ if (window.ResizeObserver) new ResizeObserver(report).observe(document.body);
135
+ `;
136
+ const safeCss = keepInside(css, "</style");
137
+ const safeHtml = keepInside(html, "</script");
138
+ const safeJs = keepInside(js, "</script");
139
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:;"><style>${tokenCss(theme)}${safeCss}</style></head><body>${safeHtml}<script>${bridge}<\/script><script>${safeJs}<\/script></body></html>`;
140
+ }
141
+
142
+ export function readSandboxMessage(data) {
143
+ if (!data || typeof data !== "object" || data.source !== SANDBOX_SOURCE) return null;
144
+ if (data.type === "height" && typeof data.height === "number" && data.height > 0 && data.height < 4000) {
145
+ return { type: "height", height: Math.round(data.height) };
146
+ }
147
+ if (data.type === "openUrl" && isHttpsUrl(data.url)) return { type: "openUrl", url: String(data.url) };
148
+ if (data.type === "issuePrompt" && typeof data.text === "string" && data.text.trim()) {
149
+ return { type: "issuePrompt", text: data.text.trim().slice(0, 500) };
150
+ }
151
+ if (data.type === "copy" && typeof data.text === "string") {
152
+ return { type: "copy", text: data.text.slice(0, 4000) };
153
+ }
154
+ return null;
155
+ }
package/ui/index.mjs ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Public entry for hosts that compile and check blocks without rendering.
3
+ * React hosts mount `ui/react`. The module does not import Garden.
4
+ */
5
+ export {
6
+ SANDBOX_SOURCE,
7
+ buildSrcdoc,
8
+ createHost,
9
+ isHttpsUrl,
10
+ readSandboxMessage,
11
+ sandboxLimit,
12
+ sandboxRemote,
13
+ } from "./host.mjs";
14
+ export {
15
+ applyBlock,
16
+ ApplyError,
17
+ bits32,
18
+ bits64,
19
+ blockStatus,
20
+ bytes,
21
+ CATALOG_VERSION,
22
+ catalogReference,
23
+ catalogSchema,
24
+ classifyReplacement,
25
+ collectDataSources,
26
+ collectDecls,
27
+ compileUiBlock,
28
+ compiledOf,
29
+ COMPONENT_NAMES,
30
+ COMPONENTS,
31
+ computeScope,
32
+ contentForDistribution,
33
+ contentForFeed,
34
+ contentToSearchText,
35
+ defaultStateValues,
36
+ docToHtml,
37
+ docToMarkdown,
38
+ docToText,
39
+ fenceHash,
40
+ findBlocks,
41
+ fixed,
42
+ float32,
43
+ formatValue,
44
+ GROUPS,
45
+ idOf,
46
+ intentFence,
47
+ intentHash,
48
+ intentOf,
49
+ isPowerOfTwo,
50
+ isValidFormat,
51
+ jsonFence,
52
+ lookupFrom,
53
+ normalizeNewlines,
54
+ orderLets,
55
+ parseDataFile,
56
+ parseUiBody,
57
+ parseUiBodyPartial,
58
+ PAYLOAD_VERSION,
59
+ percent,
60
+ readPayload,
61
+ renderStatic,
62
+ resolveSidecarPath,
63
+ runExpect,
64
+ sampleDomain,
65
+ scanFences,
66
+ scanMarkdown,
67
+ setCompiled,
68
+ si,
69
+ splitUiBody,
70
+ STARTERS,
71
+ suggestComponent,
72
+ sweepSeries,
73
+ thousands,
74
+ toDoc,
75
+ TONE_LABELS,
76
+ TONES,
77
+ transformUiContent,
78
+ checkUiBlock,
79
+ withRow,
80
+ } from "./core/index.mjs";
@@ -0,0 +1,76 @@
1
+ const header = (id, caption) => `---
2
+ id: ${id}
3
+ caption: ${caption}
4
+ intent: |
5
+ 像素对照
6
+ ---
7
+ `;
8
+
9
+ /** Real fences for the six interactions that were missing from the first crop. */
10
+ export const PARITY_FIXTURES = [
11
+ {
12
+ id: "parity-slider",
13
+ width: 768,
14
+ body: `${header("parity-slider", "序列越长,KV 显存越大。")}
15
+ <State seq={4096} />
16
+ <Slider bind="seq" label="序列长度" min={1024} max={32768} scale="log" format="thousands" unit="token" />
17
+ `,
18
+ },
19
+ {
20
+ id: "parity-chart",
21
+ width: 768,
22
+ body: `${header("parity-chart", "显存随序列长度线性增长。")}
23
+ <State seq={4096} />
24
+ <Let mem={2 * 32 * 8 * 128 * seq * 2} />
25
+ <Chart sweep="seq" from={1024} to={32768} scale="log" height={220} xLabel="序列长度" yLabel="KV 显存">
26
+ <Line y={mem} label="KV 显存" format="bytes" />
27
+ </Chart>
28
+ `,
29
+ },
30
+ {
31
+ id: "parity-steps",
32
+ width: 768,
33
+ body: `${header("parity-steps", "Prefill 一次写满 K/V;Decode 每步只追加一行。")}
34
+ <Steps>
35
+ <Step title="Prefill">一次写下全部 K/V。</Step>
36
+ <Step title="Decode">这一拍只追加一行。</Step>
37
+ <Step title="完成">21 次投影变成 7 次。</Step>
38
+ </Steps>
39
+ `,
40
+ },
41
+ {
42
+ id: "parity-grid",
43
+ width: 768,
44
+ body: `${header("parity-grid", "ECB 下相同的明文块得到相同的密文块。")}
45
+ <Grid cols={["块1", "块2", "块3"]} legend={{ same: "相同的明文得到相同的密文" }}>
46
+ <Row label="明文" cells={["A", "B", "A"]} tones={["same", "plain", "same"]} />
47
+ <Row label="ECB 密文" cells={["X", "Y", "X"]} tones={["same", "plain", "same"]} notes={["和块3一样", "", "和块1一样"]} />
48
+ </Grid>
49
+ `,
50
+ },
51
+ {
52
+ id: "parity-code",
53
+ width: 768,
54
+ body: `${header("parity-code", "这一步执行的是累加。")}
55
+ <Code lang="go" highlight="2" code={\`a := 1
56
+ b := a + 1
57
+ fmt.Println(b)\`} />
58
+ `,
59
+ },
60
+ {
61
+ id: "parity-diagram",
62
+ width: 768,
63
+ body: `${header("parity-diagram", "收到 SYN+ACK 才离开 SYN-SENT。")}
64
+ <Diagram highlight={["synsent"]} mermaid={\`flowchart LR
65
+ closed[CLOSED] --> synsent[SYN-SENT]
66
+ synsent --> established[ESTABLISHED]\`} />
67
+ `,
68
+ },
69
+ {
70
+ id: "parity-sandbox",
71
+ width: 768,
72
+ body: `${header("parity-sandbox", "目录以外的交互放在无网络的画布里。")}
73
+ <Sandbox title="计数" html={"<p id=\\"n\\">0</p><button id=\\"b\\" type=\\"button\\">加一</button>"} css="button{color:var(--viz-accent)}" js="document.getElementById('b').onclick=()=>{const n=document.getElementById('n');n.textContent=String(Number(n.textContent)+1)}" />
74
+ `,
75
+ },
76
+ ];