hd-garden 0.10.4 → 0.12.0

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.
Files changed (81) hide show
  1. package/.agents/skills/garden-ui/SKILL.md +11 -10
  2. package/README.en.md +1 -1
  3. package/README.md +2 -2
  4. package/app/components/garden/GardenBlock.tsx +20 -4
  5. package/app/components/garden/blocks.tsx +6 -118
  6. package/app/globals.css +1 -0
  7. package/app/lib/garden-docs-meta.ts +5 -3
  8. package/app/lib/generated-content.ts +1 -1
  9. package/app/lib/i18n.ts +0 -6
  10. package/app/lib/remark-garden.mjs +61 -5
  11. package/app/styles/a2.css +25 -112
  12. package/app/styles/tokens.css +5 -0
  13. package/docs/handbook/ai-writing.md +3 -3
  14. package/docs/handbook/components/compose.md +134 -0
  15. package/docs/handbook/components/html.md +1 -1
  16. package/docs/handbook/components/image.md +1 -1
  17. package/docs/handbook/components/interactive.md +4 -1
  18. package/docs/handbook/components/link-card.md +1 -1
  19. package/docs/handbook/components.md +23 -35
  20. package/docs/handbook/design.md +25 -34
  21. package/docs/handbook/en/ai-writing.md +3 -3
  22. package/docs/handbook/en/components/compose.md +123 -0
  23. package/docs/handbook/en/components/html.md +1 -1
  24. package/docs/handbook/en/components/image.md +1 -1
  25. package/docs/handbook/en/components/interactive.md +4 -1
  26. package/docs/handbook/en/components.md +23 -36
  27. package/docs/handbook/en/design.md +25 -33
  28. package/docs/handbook/en/index.md +5 -5
  29. package/docs/handbook/en/reference/compose-icons.md +863 -0
  30. package/docs/handbook/en/reference/compose.md +177 -0
  31. package/docs/handbook/en/reference.md +3 -2
  32. package/docs/handbook/en/workflow.md +8 -6
  33. package/docs/handbook/index.md +5 -5
  34. package/docs/handbook/reference/compose-icons.md +863 -0
  35. package/docs/handbook/reference/compose.md +177 -0
  36. package/docs/handbook/reference.md +3 -2
  37. package/docs/handbook/workflow.md +8 -6
  38. package/editors/dist/typora/main.js +269 -241
  39. package/editors/dist/vscode/garden-ui-0.1.0.vsix +0 -0
  40. package/package.json +14 -2
  41. package/scripts/gen-compose-handbook.mjs +427 -0
  42. package/scripts/gen-compose-icons.mjs +236 -0
  43. package/scripts/handbook-allowlist.mjs +0 -8
  44. package/scripts/handbook-facts.mjs +6 -36
  45. package/scripts/ui-cli.mjs +5 -1
  46. package/templates/garden-blocks.md +34 -31
  47. package/templates/garden-snippets.plist +3 -8
  48. package/templates/skills-en/garden-ui/SKILL.md +11 -10
  49. package/ui/README.md +1 -0
  50. package/ui/compose/compose.css +1324 -0
  51. package/ui/compose/dil.mjs +443 -0
  52. package/ui/compose/examples.mjs +28 -0
  53. package/ui/compose/icons.mjs +774 -0
  54. package/ui/compose/index.mjs +8 -0
  55. package/ui/compose/markup.mjs +727 -0
  56. package/ui/compose/parse.mjs +28 -0
  57. package/ui/compose/render.mjs +385 -0
  58. package/ui/compose/tokens.mjs +50 -0
  59. package/ui/core/cli-locale.mjs +4 -0
  60. package/ui/core/fence.mjs +13 -1
  61. package/ui/core/fix-prompt.mjs +1 -0
  62. package/ui/core/garden-fence.mjs +4 -1
  63. package/ui/core/scan.mjs +28 -2
  64. package/ui/core/transform.mjs +12 -0
  65. package/ui/core/validate.mjs +6 -4
  66. package/ui/garden/catalog.mjs +58 -35
  67. package/ui/garden/check.mjs +22 -6
  68. package/ui/garden/parse.mjs +26 -0
  69. package/ui/garden/snippets.mjs +15 -13
  70. package/ui/garden/word-count.mjs +10 -18
  71. package/ui/preview/dist/garden-ui.esm.js +268 -242
  72. package/ui/preview/dist/garden-ui.iife.js +268 -242
  73. package/ui/prompt/build.mjs +1 -0
  74. package/ui/prompt/recipes/compose.md +40 -0
  75. package/ui/prompt/rules.md +3 -3
  76. package/ui/react/CatalogPage.tsx +2 -2
  77. package/ui/react/runtime.tsx +5 -1
  78. package/docs/handbook/components/figure.md +0 -59
  79. package/docs/handbook/en/components/figure.md +0 -59
  80. package/docs/handbook/en/reference/figure.md +0 -84
  81. package/docs/handbook/reference/figure.md +0 -84
@@ -2,8 +2,8 @@
2
2
  name: garden-ui
3
3
  description: >-
4
4
  Compiles the `garden` idea blocks in one Garden post into garden components
5
- (garden interactive, or card / stat / compare / ... , or prose, or garden
6
- html), fixes broken garden interactive blocks, and suggests where a post could
5
+ (garden compose, or card / stat / compare / ... , or prose, or garden
6
+ html), fixes broken garden compose blocks, and suggests where a post could
7
7
  use interaction. Older intent / ui blocks are handled the same way. Use when
8
8
  the user says 编译这篇 / 编译想法 / 重编译 <id> / 这篇哪里适合交互, or pastes a
9
9
  garden ui check fix prompt.
@@ -11,7 +11,7 @@ description: >-
11
11
 
12
12
  # 编译 Garden 交互块
13
13
 
14
- 作者在稿里用不带名字的 `garden` 围栏写一句想法,这个 skill 把它变成 `garden interactive` 或别的 `garden` 组件。交互组件和设计规则在 `npx garden ui prompt` 的输出里,12 个 `garden` 组件的用途和示例在 `node_modules/hd-garden/templates/garden-blocks.md`(在引擎仓库里是 `templates/garden-blocks.md`),以这两份为准,不凭记忆写组件。
14
+ 作者在稿里用不带名字的 `garden` 围栏写一句想法,这个 skill 把它变成 `garden compose`、`garden compose` 或别的 `garden` 组件。`garden compose` 若还是白话,就在同一围栏里填 YAML 树,不要改围栏类型。交互组件和设计规则在 `npx garden ui prompt` 的输出里,各 `garden` 组件的用途和示例在 `node_modules/hd-garden/templates/garden-blocks.md`(在引擎仓库里是 `templates/garden-blocks.md`),以这两份为准,不凭记忆写组件。
15
15
 
16
16
  作者提交时,提交前钩子跑的是同一个 `npx garden ui check`。你用来找块、写回、自查的命令,和拦住作者提交的命令是同一套。
17
17
 
@@ -34,7 +34,7 @@ description: >-
34
34
  npx garden ui scan content/posts/路径.md
35
35
  ```
36
36
 
37
- stdout 是 JSON。`lang` 是 `garden-idea`(想法)、`garden-interactive`,或旧写法的 `intent` / `ui`。每块有 `lang`、`id`、`status`、`startLine`、`hash`(整段围栏的哈希,写回时当 `--base`)、`intentHash`(原话的哈希)、`intent`(原话)、`caption`、`section`(所在小节标题)和 `diagnostics`。不要自己用正则找围栏。
37
+ stdout 是 JSON。`lang` 是 `garden-idea`(想法)、`garden-compose`、`garden-interactive`,或旧写法的 `intent` / `ui`。每块有 `lang`、`id`、`status`、`startLine`、`hash`(整段围栏的哈希,写回时当 `--base`)、`intentHash`(原话的哈希)、`intent`(原话)、`caption`、`section`(所在小节标题)和 `diagnostics`。不要自己用正则找围栏。
38
38
 
39
39
  2. 读规则:
40
40
 
@@ -46,13 +46,14 @@ description: >-
46
46
 
47
47
  3. 每块读原话和所在小节,三选一:
48
48
  - **写回正文**:一句话、一张表或一张 Mermaid 就能说清时。
49
- - **写 `garden interactive`**:读者需要拖、点、翻才能看懂时。
49
+ - **写 `garden compose`**:读者需要拖、点、翻才能看懂时。
50
50
  - **写一段 `diff`**:想法紧跟在一段完整代码后面,说的是「和上一版比改了什么」时。按原话写出 diff,用一段普通 Markdown 写回(只有这一个 `diff` 围栏)。写回后它紧挨着那段代码,页面会合成「代码 / 对比上一版」切换;diff 里的新行要和上面的完整代码一致。
51
- - **写别的 `garden` 组件**:一张或多张资源卡(`card` / `cards`)、几个指标(`stat`)、两种做法并排或好坏对照(`compare` / `verdict: good|bad`)、一条时间线(`timeline`)、一张静态图表(`chart`)、一张分区结构图(`figure`)、一份下载文件(`file`)、3~6 个要素的关系图(`relations`)、紧凑图标列表或分步列表(`list`)、两三个选项的选择卡(`choice`)。字段与校验约束见 `npx garden ui prompt` 与 `templates/garden-blocks.md`。
51
+ - **写别的 `garden` 组件**:一张或多张资源卡(`card` / `cards`)、几个指标(`stat`)、两种做法并排或好坏对照(`compare` / `verdict: good|bad`)、一条时间线(`timeline`)、一张静态图表(`chart`)、一张说明图(`compose`)、一份下载文件(`file`)、3~6 个要素的关系图(`relations`)、紧凑图标列表或分步列表(`list`)、两三个选项的选择卡(`choice`)。字段与校验约束见 `npx garden ui prompt` 与 `templates/garden-blocks.md`。
52
+ - **编译 `garden compose`**:`lang` 是 `garden-compose` 且 `status` 是 `uncompiled` 时,围栏头保持 `garden compose`。把扫描到的原话原样放进 `intent`,正文写成 YAML 树(`steps` + `notes`,或 `tree`)。不要改成 interactive,不要写 HTML、SVG 或脚本。配方在 `npx garden ui prompt` 的说明图一节。
52
53
  - **写 `garden html`**:目录表达不了的画布动画、音频、物理模拟等。按 [garden-interactive-chart](../garden-interactive-chart/SKILL.md) 在同名目录写 HTML,用 `title` 写下判断。
53
54
 
54
- 4. 写 `garden interactive` 时:
55
- - 格式:围栏头写 `garden interactive #<id>`;`---` 上面是 `caption`、`intent`、可选的 `expect`;`---` 下面是 `npx garden ui prompt` 里的组件树。
55
+ 4. 写 `garden compose` 时:
56
+ - 格式:围栏头写 `garden compose #<id>`;`---` 上面是 `caption`、`intent`、可选的 `expect`;`---` 下面是 `npx garden ui prompt` 里的组件树。
56
57
  - 原话原样放进 `intent`,一个字都不改。
57
58
  - `caption` 摘原话里的判断。原话没有判断句,就根据小节内容补一句,并在回复里说明。
58
59
  - 原话里有已知点("batch 1 时是 512 MiB"),写进 `expect`。
@@ -63,7 +64,7 @@ description: >-
63
64
 
64
65
  ````bash
65
66
  npx garden ui apply content/posts/路径.md --id <id> --base <hash> <<'GARDEN_UI'
66
- ```garden interactive #id
67
+ ```garden compose #id
67
68
  caption: ...
68
69
  intent: |
69
70
  ...
@@ -95,7 +96,7 @@ description: >-
95
96
 
96
97
  | id | 选择 | 配方 | 假设 | expect |
97
98
  | --- | --- | --- | --- | --- |
98
- | kv-memory | garden interactive | 参数台 | 默认序列取 4K | batch 1 时 512 MiB |
99
+ | kv-memory | garden compose | 参数台 | 默认序列取 4K | batch 1 时 512 MiB |
99
100
 
100
101
  "假设"写原话没说、你替作者定下的东西(默认值、范围、补的 caption)。没有就写"无"。
101
102
 
package/README.en.md CHANGED
@@ -66,7 +66,7 @@ Where you want a component or interactive calculation, write an unnamed `garden`
66
66
  ```
67
67
  ````
68
68
 
69
- Ask your Agent to compile the post using the bundled `garden-ui` skill: it runs `npx garden ui scan` to locate uncompiled or broken blocks, reads the component catalog from `npx garden ui prompt`, generates a `garden interactive` block (or `card`, `stat`, `compare`, `timeline`, `chart`, `figure`, `html`, `file`, `relations`, `list`, `choice`, or a `diff` block right below a code snippet), and writes only that fence back via `npx garden ui apply`. If you run `git commit` before compiling, the pre-commit hook runs `npx --no-install garden ui check --staged --quiet`, stops the commit, and prints a ready-to-paste fix prompt for your AI.
69
+ Ask your Agent to compile the post using the bundled `garden-ui` skill: it runs `npx garden ui scan` to locate uncompiled or broken blocks, reads the component catalog from `npx garden ui prompt`, generates a `garden compose` block (or `card`, `stat`, `compare`, `timeline`, `chart`, `figure`, `html`, `file`, `relations`, `list`, `choice`, or a `diff` block right below a code snippet), and writes only that fence back via `npx garden ui apply`. If you run `git commit` before compiling, the pre-commit hook runs `npx --no-install garden ui check --staged --quiet`, stops the commit, and prints a ready-to-paste fix prompt for your AI.
70
70
 
71
71
  ## 4. Common commands
72
72
 
package/README.md CHANGED
@@ -68,7 +68,7 @@ Obsidian 插件这一轮不随包安装。
68
68
  ```
69
69
  ````
70
70
 
71
- 让 Agent 按站点里的 `garden-ui` skill 处理这篇稿件:Agent 先用 `npx garden ui scan` 读取待编译块的哈希,再读 `npx garden ui prompt` 的组件规则,生成 `garden interactive`(或 `card`、`stat`、`compare`、`timeline`、`chart`、`figure`、`html`、`file`、`relations`、`list`、`choice` 等内置组件,或在完整代码下方生成 `diff`),最后用 `npx garden ui apply` 只替换该围栏。如果你在编译前直接 `git commit`,pre-commit 钩子会运行同一条 `npx --no-install garden ui check --staged --quiet` 拦下提交,并打印一段可直接粘贴给 AI 的修复提示词。
71
+ 让 Agent 按站点里的 `garden-ui` skill 处理这篇稿件:Agent 先用 `npx garden ui scan` 读取待编译块的哈希,再读 `npx garden ui prompt` 的组件规则,生成 `garden compose`(或 `card`、`stat`、`compare`、`timeline`、`chart`、`compose`、`html`、`file`、`relations`、`list`、`choice` 等内置组件,或在完整代码下方生成 `diff`),最后用 `npx garden ui apply` 只替换该围栏。如果你在编译前直接 `git commit`,pre-commit 钩子会运行同一条 `npx --no-install garden ui check --staged --quiet` 拦下提交,并打印一段可直接粘贴给 AI 的修复提示词。
72
72
 
73
73
  ## 4. 常用命令
74
74
 
@@ -98,7 +98,7 @@ npx garden editors install --yes # 把 Intelligent UI 插件装进
98
98
 
99
99
  本地运行 `make dev` 后访问 `/garden`(英文版在 `/garden/en`),或直接阅读仓库内的文档:
100
100
 
101
- - **[Garden 使用说明(中文)](docs/handbook/index.md)** · **[Garden Handbook (English)](docs/handbook/en/index.md)**:快速开始、[用 AI 写作](docs/handbook/ai-writing.md)、[站点与专栏配置](docs/handbook/config.md)、[发布到 GitHub Pages](docs/handbook/deploy.md)、[写文章与随想](docs/handbook/writing.md)、[阅读排版](docs/handbook/design.md)、[组件](docs/handbook/components.md)(12 个 `garden` 围栏,以及链接卡片、来源胶囊和本地图片)与 [语法检查](docs/handbook/workflow.md)。
101
+ - **[Garden 使用说明(中文)](docs/handbook/index.md)** · **[Garden Handbook (English)](docs/handbook/en/index.md)**:快速开始、[用 AI 写作](docs/handbook/ai-writing.md)、[站点与专栏配置](docs/handbook/config.md)、[发布到 GitHub Pages](docs/handbook/deploy.md)、[写文章与随想](docs/handbook/writing.md)、[阅读排版](docs/handbook/design.md)、[组件](docs/handbook/components.md)(`garden` 围栏,以及链接卡片、来源胶囊和本地图片;说明图是 compose)与 [语法检查](docs/handbook/workflow.md)。
102
102
  - **[从零开始](docs/quick-start.md)** 与 **[GitHub Pages 部署](docs/github-pages.md)**:单公开仓库或「私有源码仓库 + 公开页面仓库」的 Actions 配置与 Giscus 评论接入。
103
103
  - **[独立管理后台](docs/admin-service.md)**:可选的 `/admin` 服务(本地、Docker 或 Cloudflare Workers),支持网页发随想、阿里云 OSS 媒体直传与 `/admin/ui` 组件工作台。
104
104
 
@@ -1,12 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  import { useUiLang } from "../UiLang";
4
+ import { renderCompose } from "../../../ui/compose/render.mjs";
4
5
  import { readGardenPayload } from "../../lib/remark-garden.mjs";
5
6
  import {
6
7
  GardenCard,
7
8
  GardenChart,
8
9
  GardenCompare,
9
- GardenFigure,
10
10
  GardenHtml,
11
11
  GardenIdea,
12
12
  GardenInteractive,
@@ -37,12 +37,28 @@ export function GardenBlock({ dataPayload }: { dataPayload?: string }) {
37
37
  return <GardenTimeline props={props} />;
38
38
  case "chart":
39
39
  return <GardenChart props={props} />;
40
- case "figure":
41
- return <GardenFigure props={props}>{String(block.children ?? "")}</GardenFigure>;
40
+ case "compose":
41
+ if (block.error?.code === "compose-uncompiled") return <GardenIdea text={String(block.idea || block.children || "")} />;
42
+ if (block.error) return <GardenUnknown name="compose" />;
43
+ if (block.mode === "play") {
44
+ // DIL play: prefer lowered uiSource from ui/compose/dil.mjs. children alone is TEMP legacy.
45
+ return (
46
+ <GardenInteractive
47
+ block={{
48
+ id: block.id,
49
+ props,
50
+ children: String(block.children ?? ""),
51
+ uiSource: typeof block.uiSource === "string" ? block.uiSource : undefined,
52
+ }}
53
+ />
54
+ );
55
+ }
56
+ return <div className="g-compose-host" dangerouslySetInnerHTML={{ __html: renderCompose(props) }} />;
42
57
  case "html":
43
58
  return <GardenHtml props={props}>{String(block.children ?? "")}</GardenHtml>;
44
59
  case "interactive":
45
- return <GardenInteractive block={{ id: block.id, props, children: block.children }} />;
60
+ // TEMP compat if a payload still names interactive; parse now rewrites to compose.
61
+ return <GardenInteractive block={{ id: block.id, props, children: String(block.children ?? "") }} />;
46
62
  case "file":
47
63
  // Post bodies render file cards on the server (MarkdownArticle); this path has no Markdown preview.
48
64
  return <GardenFile props={props} lang={lang} />;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useId, useRef, useState, type ReactNode } from "react";
3
+ import { useEffect, useRef, type ReactNode } from "react";
4
4
  import ReactMarkdown from "react-markdown";
5
5
  import { useUi, useUiLang } from "../UiLang";
6
6
  import { langOf, ui } from "../../lib/i18n";
@@ -265,120 +265,6 @@ export function GardenChart({ props }: { props: Record<string, unknown> }) {
265
265
  );
266
266
  }
267
267
 
268
- type Part = { id: string; label: string; text: string };
269
-
270
- function partsOf(props: Record<string, unknown>, partLabel: (n: number) => string): Part[] {
271
- if (!Array.isArray(props.parts)) return [];
272
- return props.parts.map((part, index) => {
273
- const item = part as Record<string, unknown>;
274
- const label = String(item.label ?? item.name ?? partLabel(index + 1));
275
- return { id: String(item.id ?? item.name ?? label), label, text: String(item.text ?? "") };
276
- });
277
- }
278
-
279
- function partOn(selected: string, token: string) {
280
- if (selected === "all" || selected === "全部") return true;
281
- const parts = token.trim().split(/\s+/u).filter(Boolean);
282
- const has = (id: string) => parts.includes(id);
283
- const starts = (prefix: string) => parts.some((part) => part === prefix || part.startsWith(prefix));
284
- if (has(selected)) return true;
285
- if (selected === "q") return has("attn");
286
- if (selected === "k") return starts("k") || has("attn");
287
- if (selected === "v") return starts("v") || has("out");
288
- if (selected === "cache") return has("cache") || has("kn") || has("vn") || starts("k") || starts("v");
289
- if (selected === "out") return has("out");
290
- return false;
291
- }
292
-
293
- export function GardenFigure({ props, children }: { props: Record<string, unknown>; children?: string }) {
294
- const t = useUi();
295
- const parts = partsOf(props, t.part);
296
- const [selected, setSelected] = useState(parts[0]?.id ?? "all");
297
- const svgRef = useRef<HTMLDivElement>(null);
298
- const base = useId();
299
- const fromChild = typeof children === "string" && children.includes("<svg") ? children : "";
300
- const svg = typeof props.svg === "string" && props.svg.includes("<svg") ? props.svg : fromChild;
301
-
302
- useEffect(() => {
303
- const root = svgRef.current;
304
- const drawing = root?.querySelector("svg");
305
- if (!drawing) return;
306
- const shapes = [...drawing.querySelectorAll<SVGGraphicsElement>("rect[data-p], ellipse[data-p], circle[data-p]")];
307
- drawing.querySelectorAll("text").forEach((text) => {
308
- const x = Number(text.getAttribute("x"));
309
- const y = Number(text.getAttribute("y"));
310
- const shape = shapes.find((item) => {
311
- const box = item.getBBox();
312
- return x >= box.x && x <= box.x + box.width && y >= box.y && y <= box.y + box.height + 2;
313
- });
314
- if (!shape) return;
315
- if (!text.hasAttribute("data-p")) text.setAttribute("data-p", shape.getAttribute("data-p") || "");
316
- try {
317
- const room = shape.getBBox().width - 12;
318
- const width = text.getComputedTextLength();
319
- if (width > room && room > 0) {
320
- const size = Number.parseFloat(getComputedStyle(text).fontSize);
321
- text.style.fontSize = `${(size * room / width).toFixed(2)}px`;
322
- }
323
- } catch {
324
- /* getBBox can fail before layout */
325
- }
326
- });
327
- }, [svg]);
328
-
329
- useEffect(() => {
330
- const root = svgRef.current;
331
- if (!root) return;
332
- root.querySelectorAll("[data-p]").forEach((element) => {
333
- const token = ` ${element.getAttribute("data-p") || ""} `;
334
- element.classList.toggle("on", partOn(selected, token));
335
- });
336
- }, [selected, svg]);
337
-
338
- const body = (
339
- <figure className="g-explore" data-g="explore" data-sel={selected} ref={svgRef}>
340
- {parts.length ? (
341
- <div role="tablist" aria-label={t.pickPart}>
342
- {parts.map((part, index) => (
343
- <button
344
- key={part.id}
345
- type="button"
346
- role="tab"
347
- aria-selected={selected === part.id}
348
- tabIndex={selected === part.id ? 0 : -1}
349
- data-sel={part.id}
350
- onClick={() => setSelected(part.id)}
351
- onKeyDown={(event) => {
352
- if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
353
- event.preventDefault();
354
- const next = event.key === "ArrowRight" ? (index + 1) % parts.length : (index - 1 + parts.length) % parts.length;
355
- setSelected(parts[next].id);
356
- }}
357
- >
358
- {part.label}
359
- </button>
360
- ))}
361
- </div>
362
- ) : null}
363
- {svg ? <div className="dx-wrap" dangerouslySetInnerHTML={{ __html: svg }} /> : null}
364
- {typeof props.image === "string" ? (
365
- <div className="fig-card">
366
- {/* eslint-disable-next-line @next/next/no-img-element */}
367
- <img src={publicSrc(props.image)} alt={String(props.alt ?? props.caption ?? "")} />
368
- </div>
369
- ) : null}
370
- {typeof props.mermaid === "string" ? <pre className="code"><code>{props.mermaid}</code></pre> : null}
371
- <div className="dx-exp" aria-live="polite">
372
- {parts.map((part) => (
373
- <p key={part.id} id={`${base}-${part.id}`} hidden={selected !== part.id}><Rich text={part.text} /></p>
374
- ))}
375
- </div>
376
- {typeof props.caption === "string" ? <figcaption><Rich text={props.caption} /></figcaption> : null}
377
- </figure>
378
- );
379
- return parts.length || svg ? <FullscreenFrame label={t.diagram}>{body}</FullscreenFrame> : body;
380
- }
381
-
382
268
  function htmlHasScript(source: string) {
383
269
  return /<script\b(?![^>]*\btype\s*=\s*["']application\/json["'])/iu.test(source);
384
270
  }
@@ -553,11 +439,13 @@ export function GardenHtml({ props, children }: { props: Record<string, unknown>
553
439
  return <FullscreenFrame label={title}><StaticGardenHtml code={code} /></FullscreenFrame>;
554
440
  }
555
441
 
556
- export function GardenInteractive({ block }: { block: { id?: string | null; props: Record<string, unknown>; children?: string } }) {
442
+ export function GardenInteractive({ block }: { block: { id?: string | null; props: Record<string, unknown>; children?: string; uiSource?: string } }) {
557
443
  const t = useUi();
558
444
  const lang = useUiLang();
559
- // Same source `garden ui check` validates (ui/core/garden-fence.mjs).
560
- const { source, header } = interactiveUiSource({ id: block.id ?? null, props: block.props, children: block.children ?? "" });
445
+ // Prefer DIL-lowered uiSource from compose/dil.mjs; else build from PascalCase children (TEMP legacy).
446
+ const built = interactiveUiSource({ id: block.id ?? null, props: block.props, children: block.children ?? "" });
447
+ const source = typeof block.uiSource === "string" && block.uiSource.trim() ? block.uiSource : built.source;
448
+ const header = built.header;
561
449
  const { payload, check } = compileUiBlock(source);
562
450
  if (!payload) {
563
451
  return (
package/app/globals.css CHANGED
@@ -1,5 +1,6 @@
1
1
  @import "tailwindcss";
2
2
  @import "../ui/react/ui.css";
3
+ @import "../ui/compose/compose.css";
3
4
  @import "./admin-ui.css";
4
5
 
5
6
  :root {
@@ -83,7 +83,7 @@ export const COMPONENT_BADGE: Record<string, string> = {
83
83
  "components/compare": "compare",
84
84
  "components/timeline": "timeline",
85
85
  "components/chart": "chart",
86
- "components/figure": "figure",
86
+ "components/compose": "compose",
87
87
  "components/html": "html",
88
88
  "components/interactive": "interactive",
89
89
  "components/file": "file",
@@ -95,7 +95,8 @@ export const COMPONENT_BADGE: Record<string, string> = {
95
95
  "reference/compare": "compare",
96
96
  "reference/timeline": "timeline",
97
97
  "reference/chart": "chart",
98
- "reference/figure": "figure",
98
+ "reference/compose": "compose",
99
+ "reference/compose-icons": "compose",
99
100
  "reference/html": "html",
100
101
  "reference/interactive": "interactive",
101
102
  "reference/file": "file",
@@ -117,7 +118,8 @@ export const NAV_SHORT_TITLE: Record<string, string> = {
117
118
  "components/compare": "并排对比",
118
119
  "components/timeline": "时间线",
119
120
  "components/chart": "条形图表",
120
- "components/figure": "分区结构图",
121
+ "components/compose": "说明图",
122
+ "reference/compose-icons": "compose 图标",
121
123
  "components/html": "嵌入网页片段",
122
124
  "components/interactive": "可调公式与参数",
123
125
  "components/file": "文件下载与预览",
@@ -58,7 +58,7 @@ export const posts = [
58
58
  "kind": "post",
59
59
  "draft": false,
60
60
  "sourcePath": "posts/windowsill-herbs.md",
61
- "content": "北向窗台上有三只盆。这季种了罗勒和薄荷,第三只空着,留给秋天的香菜。\n\n## 两种香草\n\n罗勒要见光,薄荷更耐阴。浇法也不一样。\n\n```garden compare\nitems:\n - title: 罗勒\n text: 土面发白再浇透,托盘里的水倒掉。\n - title: 薄荷\n text: 土保持微湿,避开正午的直射光。\n```\n\n## 这季记下的数\n\n种子是三月中旬播的。盆不深,第一次剪叶等到第六周。\n\n```garden stat\nitems:\n - label: 发芽\n value: 7\n unit: 天\n - label: 盆深\n value: 15\n unit: 厘米\n - label: 第一次剪叶\n value: 6\n unit: 周\n```\n\n## 从播种到剪叶\n\n```garden timeline\nitems:\n - date: 3月12日\n title: 播种\n text: 罗勒和薄荷各一盆,覆土不超过半厘米。\n - date: 3月19日\n title: 出苗\n text: 罗勒先露头,薄荷晚了两天。\n - date: 4月2日\n title: 间苗\n text: 每盆留下最壮的两株。\n - date: 5月1日\n title: 第一次剪叶\n text: 只剪顶端,留下至少四片叶。\n```\n\n> [!NOTE]\n> 北向窗冬天光照不够,罗勒会徒长。把盆移到能见到上午光的那一格。\n\n## 每周的株高\n\n从第二周起,每周一量一次最高的那片叶。\n\n```garden chart\ntype: bar\ntitle: 罗勒株高\nx: [\"第2周\", \"第3周\", \"第4周\", \"第5周\", \"第6周\"]\nseries:\n - name: 株高\n data: [4, 8, 13, 18, 22]\nunit: cm\ncaption: 第六周剪叶前,罗勒大约 22 厘米。\n```\n\n## 窗台怎么摆\n\n靠左的那格上午有光,罗勒放那里。薄荷在中间。右边的空盆先不浇。\n\n```garden figure\ncaption: 朝北的窗台,从左到右是罗勒、薄荷和一只空盆。\nparts:\n - name: 全部\n text: 三只盆并排,靠左的光照最好。\n - name: 罗勒\n text: 靠左,每天都够到上午的光。\n - name: 薄荷\n text: 中间,土干得慢一些。\n - name: 空盆\n text: 右边空着,留给秋天的香菜。\n---\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 220\">\n <rect data-p=\"全部\" x=\"20\" y=\"16\" width=\"600\" height=\"150\" rx=\"10\" fill=\"#d5e3ea\"/>\n <rect data-p=\"罗勒\" x=\"70\" y=\"96\" width=\"90\" height=\"58\" rx=\"8\" fill=\"#c4a574\"/>\n <circle data-p=\"罗勒\" cx=\"115\" cy=\"78\" r=\"30\" fill=\"#3f7d4e\"/>\n <rect data-p=\"薄荷\" x=\"270\" y=\"104\" width=\"90\" height=\"50\" rx=\"8\" fill=\"#c4a574\"/>\n <circle data-p=\"薄荷\" cx=\"315\" cy=\"88\" r=\"26\" fill=\"#6aa87a\"/>\n <rect data-p=\"空盆\" x=\"470\" y=\"118\" width=\"90\" height=\"36\" rx=\"8\" fill=\"#e4d3b8\"/>\n</svg>\n```\n\n## 今天浇不浇\n\n土面还湿就先不动。干了再浇,浇到盆底出水,然后把托盘倒掉。\n\n```mermaid\nflowchart TD\n A[看土面] --> B{还湿吗}\n B -->|湿| C[今天不浇]\n B -->|干| D[浇到盆底出水]\n D --> E[倒掉托盘]\n```\n\n> [!WARNING]\n> 霜前把盆搬进屋。罗勒叶子一碰霜就发黑,回不来。\n\n## 盆口和浇水量\n\n盆口越大,一次浇得越多。12 厘米的盆大约是 70 毫升。\n\n```garden interactive #pot-water\ncaption: 盆口越大,一次浇得越多。12 厘米大约是 70 毫升。\nintent: |\n 让读者拧盆口直径,看这一次该浇多少毫升。\n---\n<State diameter={12} />\n<Slider bind=\"diameter\" label=\"盆口直径\" min={8} max={24} unit=\"cm\" />\n<Let water={round(diameter * diameter * 0.35 + 20)} />\n<Stat label=\"这一次\" value={water} unit=\"ml\" />\n```\n\n## 水怎么往下走\n\n浇下去的水不会停在表面。它慢慢渗过土层,最后从排水孔滴出来。\n\n```garden html\ntitle: 水从土面渗到排水孔,大约要一分钟。\ncode: |\n <style>\n html, body { margin: 0; background: transparent; color: #3c342c; font: 15px/1.5 ui-sans-serif, sans-serif; }\n svg { display: block; width: 100%; height: auto; }\n </style>\n <svg viewBox=\"0 0 640 280\" role=\"img\" aria-label=\"水从土面渗到盆底\">\n <rect x=\"180\" y=\"36\" width=\"280\" height=\"200\" rx=\"18\" fill=\"#efe6d6\" stroke=\"#cbbfa8\"/>\n <rect id=\"water\" x=\"196\" y=\"48\" width=\"248\" height=\"16\" fill=\"#7ea4c4\" opacity=\"0.85\"/>\n <rect x=\"196\" y=\"150\" width=\"248\" height=\"70\" fill=\"#8b6a45\"/>\n <circle cx=\"320\" cy=\"214\" r=\"8\" fill=\"#5c4632\"/>\n <text x=\"320\" y=\"268\" text-anchor=\"middle\" font-size=\"14\" fill=\"#3c342c\">排水孔</text>\n </svg>\n <script>\n var water = document.getElementById(\"water\");\n var y = 48;\n var reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n if (reduce) {\n water.setAttribute(\"y\", \"168\");\n water.setAttribute(\"height\", \"36\");\n } else {\n (function tick() {\n y += 0.45;\n if (y > 168) y = 48;\n water.setAttribute(\"y\", String(y));\n water.setAttribute(\"height\", String(Math.max(16, 204 - y)));\n requestAnimationFrame(tick);\n })();\n }\n </script>\n```\n\n## 浇水纸条\n\n纸条上先记下这一周。周四那次少了,周日又给薄荷补了一次。\n\n```text\n周一 罗勒 80 ml\n周四 罗勒 80 ml\n```\n```diff\n 周一 罗勒 80 ml\n-周四 罗勒 80 ml\n+周四 罗勒 120 ml\n+周日 薄荷 60 ml\n```\n\n## 六周记录\n\n| 周 | 株高 | 新叶 | 浇水 | 土面 | 气温 | 日照 | 备注 |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| 第1周 | 未见 | 无 | 每天喷湿 | 湿 | 16°C | 上午 | 覆土很薄 |\n| 第2周 | 4 cm | 2 片 | 80 ml | 干 | 18°C | 上午 | 罗勒先出 |\n| 第3周 | 8 cm | 4 片 | 80 ml | 干 | 19°C | 上午 | 薄荷跟上 |\n| 第4周 | 13 cm | 6 片 | 100 ml | 干 | 21°C | 上午 | 间苗,留两株 |\n| 第5周 | 18 cm | 8 片 | 120 ml | 干 | 22°C | 上午 | 叶色转深 |\n| 第6周 | 22 cm | 10 片 | 120 ml | 干 | 23°C | 上午 | 剪了顶端 |\n\n想核对叶子是不是对生、花是不是穗状时,看这一页。\n\n```garden card\nurl: https://zh.wikipedia.org/wiki/%E7%BD%97%E5%8B%92\ntitle: 罗勒\ndescription: 叶子对生,花排成穗。窗台上的那盆对得上。\n```\n\n> [!TIP]\n> 剪叶之后把盆转半圈,让另一面也能见到上午的光。"
61
+ "content": "北向窗台上有三只盆。这季种了罗勒和薄荷,第三只空着,留给秋天的香菜。\n\n## 两种香草\n\n罗勒要见光,薄荷更耐阴。浇法也不一样。\n\n```garden compare\nitems:\n - title: 罗勒\n text: 土面发白再浇透,托盘里的水倒掉。\n - title: 薄荷\n text: 土保持微湿,避开正午的直射光。\n```\n\n## 这季记下的数\n\n种子是三月中旬播的。盆不深,第一次剪叶等到第六周。\n\n```garden stat\nitems:\n - label: 发芽\n value: 7\n unit: 天\n - label: 盆深\n value: 15\n unit: 厘米\n - label: 第一次剪叶\n value: 6\n unit: 周\n```\n\n## 从播种到剪叶\n\n```garden timeline\nitems:\n - date: 3月12日\n title: 播种\n text: 罗勒和薄荷各一盆,覆土不超过半厘米。\n - date: 3月19日\n title: 出苗\n text: 罗勒先露头,薄荷晚了两天。\n - date: 4月2日\n title: 间苗\n text: 每盆留下最壮的两株。\n - date: 5月1日\n title: 第一次剪叶\n text: 只剪顶端,留下至少四片叶。\n```\n\n> [!NOTE]\n> 北向窗冬天光照不够,罗勒会徒长。把盆移到能见到上午光的那一格。\n\n## 每周的株高\n\n从第二周起,每周一量一次最高的那片叶。\n\n```garden chart\ntype: bar\ntitle: 罗勒株高\nx: [\"第2周\", \"第3周\", \"第4周\", \"第5周\", \"第6周\"]\nseries:\n - name: 株高\n data: [4, 8, 13, 18, 22]\nunit: cm\ncaption: 第六周剪叶前,罗勒大约 22 厘米。\n```\n\n## 窗台怎么摆\n\n靠左的那格上午有光,罗勒放那里。薄荷在中间。右边的空盆先不浇。\n\n![朝北的窗台,从左到右是罗勒、薄荷和一只空盆。](./windowsill-herbs/sill.svg)\n\n## 今天浇不浇\n\n土面还湿就先不动。干了再浇,浇到盆底出水,然后把托盘倒掉。\n\n```mermaid\nflowchart TD\n A[看土面] --> B{还湿吗}\n B -->|湿| C[今天不浇]\n B -->|干| D[浇到盆底出水]\n D --> E[倒掉托盘]\n```\n\n> [!WARNING]\n> 霜前把盆搬进屋。罗勒叶子一碰霜就发黑,回不来。\n\n## 盆口和浇水量\n\n盆口越大,一次浇得越多。12 厘米的盆大约是 70 毫升。\n\n```garden interactive #pot-water\ncaption: 盆口越大,一次浇得越多。12 厘米大约是 70 毫升。\nintent: |\n 让读者拧盆口直径,看这一次该浇多少毫升。\n---\n<State diameter={12} />\n<Slider bind=\"diameter\" label=\"盆口直径\" min={8} max={24} unit=\"cm\" />\n<Let water={round(diameter * diameter * 0.35 + 20)} />\n<Stat label=\"这一次\" value={water} unit=\"ml\" />\n```\n\n## 水怎么往下走\n\n浇下去的水不会停在表面。它慢慢渗过土层,最后从排水孔滴出来。\n\n```garden html\ntitle: 水从土面渗到排水孔,大约要一分钟。\ncode: |\n <style>\n html, body { margin: 0; background: transparent; color: #3c342c; font: 15px/1.5 ui-sans-serif, sans-serif; }\n svg { display: block; width: 100%; height: auto; }\n </style>\n <svg viewBox=\"0 0 640 280\" role=\"img\" aria-label=\"水从土面渗到盆底\">\n <rect x=\"180\" y=\"36\" width=\"280\" height=\"200\" rx=\"18\" fill=\"#efe6d6\" stroke=\"#cbbfa8\"/>\n <rect id=\"water\" x=\"196\" y=\"48\" width=\"248\" height=\"16\" fill=\"#7ea4c4\" opacity=\"0.85\"/>\n <rect x=\"196\" y=\"150\" width=\"248\" height=\"70\" fill=\"#8b6a45\"/>\n <circle cx=\"320\" cy=\"214\" r=\"8\" fill=\"#5c4632\"/>\n <text x=\"320\" y=\"268\" text-anchor=\"middle\" font-size=\"14\" fill=\"#3c342c\">排水孔</text>\n </svg>\n <script>\n var water = document.getElementById(\"water\");\n var y = 48;\n var reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n if (reduce) {\n water.setAttribute(\"y\", \"168\");\n water.setAttribute(\"height\", \"36\");\n } else {\n (function tick() {\n y += 0.45;\n if (y > 168) y = 48;\n water.setAttribute(\"y\", String(y));\n water.setAttribute(\"height\", String(Math.max(16, 204 - y)));\n requestAnimationFrame(tick);\n })();\n }\n </script>\n```\n\n## 浇水纸条\n\n纸条上先记下这一周。周四那次少了,周日又给薄荷补了一次。\n\n```text\n周一 罗勒 80 ml\n周四 罗勒 80 ml\n```\n```diff\n 周一 罗勒 80 ml\n-周四 罗勒 80 ml\n+周四 罗勒 120 ml\n+周日 薄荷 60 ml\n```\n\n## 六周记录\n\n| 周 | 株高 | 新叶 | 浇水 | 土面 | 气温 | 日照 | 备注 |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| 第1周 | 未见 | 无 | 每天喷湿 | 湿 | 16°C | 上午 | 覆土很薄 |\n| 第2周 | 4 cm | 2 片 | 80 ml | 干 | 18°C | 上午 | 罗勒先出 |\n| 第3周 | 8 cm | 4 片 | 80 ml | 干 | 19°C | 上午 | 薄荷跟上 |\n| 第4周 | 13 cm | 6 片 | 100 ml | 干 | 21°C | 上午 | 间苗,留两株 |\n| 第5周 | 18 cm | 8 片 | 120 ml | 干 | 22°C | 上午 | 叶色转深 |\n| 第6周 | 22 cm | 10 片 | 120 ml | 干 | 23°C | 上午 | 剪了顶端 |\n\n想核对叶子是不是对生、花是不是穗状时,看这一页。\n\n```garden card\nurl: https://zh.wikipedia.org/wiki/%E7%BD%97%E5%8B%92\ntitle: 罗勒\ndescription: 叶子对生,花排成穗。窗台上的那盆对得上。\n```\n\n> [!TIP]\n> 剪叶之后把盆转半圈,让另一面也能见到上午的光。"
62
62
  },
63
63
  {
64
64
  "title": "Intelligent UI 实验室",
package/app/lib/i18n.ts CHANGED
@@ -85,9 +85,6 @@ const zh = {
85
85
  dataTable: "查看数据表",
86
86
  item: "项目",
87
87
  value: "数值",
88
- part: (n: number) => `部分 ${n}`,
89
- pickPart: "选择一个部分",
90
- diagram: "结构图",
91
88
  pickAnswer: "先选一个答案。",
92
89
  correct: "✓ 答对了",
93
90
  wrong: "✗ 再想想",
@@ -370,9 +367,6 @@ const en: typeof zh = {
370
367
  dataTable: "Show data table",
371
368
  item: "Item",
372
369
  value: "Value",
373
- part: (n: number) => `Part ${n}`,
374
- pickPart: "Choose a part",
375
- diagram: "Diagram",
376
370
  pickAnswer: "Pick an answer first.",
377
371
  correct: "✓ Correct",
378
372
  wrong: "✗ Try again",
@@ -1,3 +1,4 @@
1
+ import { renderCompose } from "../../ui/compose/render.mjs";
1
2
  import { diffToNewCode, protectMathIdentifiers } from "../../ui/garden/enhance.mjs";
2
3
  import { classifyFence } from "../../ui/garden/parse.mjs";
3
4
 
@@ -29,12 +30,58 @@ function fenceNodeToRecord(node) {
29
30
  return { lang: String(node.lang || "").toLowerCase(), meta: String(node.meta || ""), info: `${node.lang || ""} ${node.meta || ""}`.trim() };
30
31
  }
31
32
 
32
- function rewriteChildren(node) {
33
+ function htmlElement(value) {
34
+ return { type: "html", value };
35
+ }
36
+
37
+ const PAIR_ALT = new Set(["ChatGPT 原图", "ChatGPT original"]);
38
+
39
+ function pairImage(node) {
40
+ if (node?.type !== "paragraph" || !Array.isArray(node.children)) return null;
41
+ const images = node.children.filter((child) => child?.type === "image");
42
+ if (images.length !== 1 || node.children.some((child) => child?.type !== "image" && String(child.value ?? "").trim())) return null;
43
+ const image = images[0];
44
+ const alt = String(image.alt ?? "").trim();
45
+ return PAIR_ALT.has(alt) ? { image, alt } : null;
46
+ }
47
+
48
+ function element(tag, className, children) {
49
+ return {
50
+ type: "paragraph",
51
+ data: { hName: tag, hProperties: { className: [className] } },
52
+ children,
53
+ };
54
+ }
55
+
56
+ function composePair(image, alt, composeHtml) {
57
+ return element("div", "g-compose-pair", [
58
+ element("figure", "g-compose-pair-ref", [
59
+ element("figcaption", "g-compose-pair-k", [{ type: "text", value: alt }]),
60
+ image,
61
+ ]),
62
+ element("div", "g-compose-pair-live", [
63
+ element("p", "g-compose-pair-k", [{ type: "text", value: "Garden Compose" }]),
64
+ htmlElement(composeHtml),
65
+ ]),
66
+ ]);
67
+ }
68
+
69
+ function rewriteChildren(node, nextScope) {
33
70
  if (!Array.isArray(node.children)) return;
34
71
  const next = [];
35
72
  for (let index = 0; index < node.children.length; index += 1) {
36
73
  const child = node.children[index];
37
74
  const following = node.children[index + 1];
75
+ const pair = pairImage(child);
76
+ if (pair && following?.type === "code") {
77
+ const record = fenceNodeToRecord(following);
78
+ const kind = classifyFence({ ...record, body: following.value, line: 1, endLine: 1 });
79
+ if (kind.kind === "garden" && kind.block?.name === "compose" && !kind.block.error && kind.block.mode !== "play") {
80
+ next.push(composePair(pair.image, pair.alt, renderCompose(kind.block.props, { scope: nextScope() })));
81
+ index += 1;
82
+ continue;
83
+ }
84
+ }
38
85
  if (child?.type === "code" && following?.type === "code" && String(following.lang || "").toLowerCase() === "diff") {
39
86
  const current = fenceNodeToRecord(child);
40
87
  const kind = classifyFence({ lang: current.lang, meta: current.meta, info: current.info });
@@ -51,6 +98,10 @@ function rewriteChildren(node) {
51
98
  next.push(gardenElement({ legacy: true, from: kind.lang || record.lang, suggestion: kind.suggestion }));
52
99
  continue;
53
100
  }
101
+ if (kind.kind === "garden" && kind.block?.name === "compose" && !kind.block.error && kind.block.mode !== "play") {
102
+ next.push(htmlElement(renderCompose(kind.block.props, { scope: nextScope() })));
103
+ continue;
104
+ }
54
105
  if (kind.kind === "garden" && kind.block) {
55
106
  next.push(gardenElement(kind.block));
56
107
  continue;
@@ -61,20 +112,25 @@ function rewriteChildren(node) {
61
112
  node.children = next;
62
113
  }
63
114
 
64
- function walk(node) {
115
+ function walk(node, nextScope) {
65
116
  if (!node || typeof node !== "object") return;
66
117
  if (node.type === "math" || node.type === "inlineMath") {
67
118
  node.value = protectMathIdentifiers(node.value);
68
119
  }
69
120
  if (Array.isArray(node.children)) {
70
- for (const child of node.children) walk(child);
71
- rewriteChildren(node);
121
+ for (const child of node.children) walk(child, nextScope);
122
+ rewriteChildren(node, nextScope);
72
123
  }
73
124
  }
74
125
 
75
126
  export function remarkGarden() {
127
+ let composeCount = 0;
128
+ const nextScope = () => {
129
+ composeCount += 1;
130
+ return `gc${composeCount}`;
131
+ };
76
132
  return (tree) => {
77
- walk(tree);
133
+ walk(tree, nextScope);
78
134
  };
79
135
  }
80
136