hd-garden 0.10.3 → 0.11.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 (85) hide show
  1. package/.agents/skills/garden-ui/SKILL.md +11 -10
  2. package/README.en.md +44 -6
  3. package/README.md +45 -7
  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 +3 -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/bin/garden.mjs +150 -112
  14. package/docs/handbook/ai-writing.md +33 -7
  15. package/docs/handbook/components/compose.md +354 -0
  16. package/docs/handbook/components/html.md +1 -1
  17. package/docs/handbook/components/image.md +1 -1
  18. package/docs/handbook/components/interactive.md +4 -1
  19. package/docs/handbook/components/link-card.md +1 -1
  20. package/docs/handbook/components.md +23 -35
  21. package/docs/handbook/design.md +25 -34
  22. package/docs/handbook/en/ai-writing.md +27 -7
  23. package/docs/handbook/en/components/compose.md +352 -0
  24. package/docs/handbook/en/components/html.md +1 -1
  25. package/docs/handbook/en/components/image.md +1 -1
  26. package/docs/handbook/en/components/interactive.md +4 -1
  27. package/docs/handbook/en/components.md +23 -36
  28. package/docs/handbook/en/design.md +25 -33
  29. package/docs/handbook/en/index.md +6 -5
  30. package/docs/handbook/en/reference/compose.md +99 -0
  31. package/docs/handbook/en/reference.md +2 -2
  32. package/docs/handbook/en/start.md +13 -0
  33. package/docs/handbook/en/workflow.md +47 -7
  34. package/docs/handbook/index.md +6 -5
  35. package/docs/handbook/reference/compose.md +99 -0
  36. package/docs/handbook/reference.md +2 -2
  37. package/docs/handbook/start.md +13 -0
  38. package/docs/handbook/workflow.md +47 -7
  39. package/editors/dist/typora/main.js +412 -0
  40. package/editors/dist/typora/manifest.json +12 -0
  41. package/editors/dist/typora/style.css +1215 -0
  42. package/editors/dist/vscode/garden-ui-0.1.0.vsix +0 -0
  43. package/eslint.config.mjs +1 -0
  44. package/package.json +17 -3
  45. package/scripts/editors-kit.mjs +352 -0
  46. package/scripts/handbook-allowlist.mjs +0 -8
  47. package/scripts/handbook-facts.mjs +6 -36
  48. package/scripts/pack-editors.mjs +241 -0
  49. package/scripts/ui-cli.mjs +5 -1
  50. package/templates/garden-blocks.md +34 -31
  51. package/templates/garden-snippets.plist +3 -8
  52. package/templates/site/README.en.md +2 -0
  53. package/templates/site/README.md +2 -0
  54. package/templates/skills-en/garden-ui/SKILL.md +11 -10
  55. package/ui/README.md +1 -0
  56. package/ui/compose/compose.css +806 -0
  57. package/ui/compose/dil.mjs +443 -0
  58. package/ui/compose/examples.mjs +28 -0
  59. package/ui/compose/index.mjs +7 -0
  60. package/ui/compose/markup.mjs +364 -0
  61. package/ui/compose/parse.mjs +21 -0
  62. package/ui/compose/render.mjs +247 -0
  63. package/ui/core/cli-locale.mjs +4 -0
  64. package/ui/core/fence.mjs +13 -1
  65. package/ui/core/fix-prompt.mjs +1 -0
  66. package/ui/core/garden-fence.mjs +4 -1
  67. package/ui/core/scan.mjs +28 -2
  68. package/ui/core/transform.mjs +12 -0
  69. package/ui/core/validate.mjs +6 -4
  70. package/ui/garden/catalog.mjs +58 -35
  71. package/ui/garden/check.mjs +22 -6
  72. package/ui/garden/parse.mjs +26 -0
  73. package/ui/garden/snippets.mjs +101 -12
  74. package/ui/garden/word-count.mjs +10 -18
  75. package/ui/preview/dist/garden-ui.esm.js +158 -132
  76. package/ui/preview/dist/garden-ui.iife.js +157 -131
  77. package/ui/prompt/build.mjs +1 -0
  78. package/ui/prompt/recipes/compose.md +40 -0
  79. package/ui/prompt/rules.md +3 -3
  80. package/ui/react/CatalogPage.tsx +2 -2
  81. package/ui/react/runtime.tsx +5 -1
  82. package/docs/handbook/components/figure.md +0 -59
  83. package/docs/handbook/en/components/figure.md +0 -59
  84. package/docs/handbook/en/reference/figure.md +0 -84
  85. 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
@@ -17,9 +17,46 @@ npm install
17
17
  make dev
18
18
  ```
19
19
 
20
- The published npm package is `hd-garden`; once installed inside a site, you can run `npx garden` (do not run `npx garden init` directly, as `garden` on npm is an unrelated package). Running `make dev` for the first time creates `.env.local`, installs the three AI skills under `.agents/skills/`, installs the pre-commit check in `.git/hooks/pre-commit`, and starts the dev server at `http://localhost:3000` (with the built-in handbook at `http://localhost:3000/garden/en`).
20
+ The published npm package is `hd-garden`; once installed inside a site, you can run `npx garden` (do not run `npx garden init` directly, as `garden` on npm is an unrelated package). `init` copies the VS Code / Cursor and Typora Intelligent UI plugins into the site and, in an interactive terminal, asks whether to install each one (see the next section). Running `make dev` for the first time creates `.env.local`, installs the three AI skills under `.agents/skills/`, installs the pre-commit check in `.git/hooks/pre-commit`, and starts the dev server at `http://localhost:3000` (with the built-in handbook at `http://localhost:3000/garden/en`).
21
21
 
22
- ## 2. Describe components in plain language (humans and Agents share the same CLI)
22
+ ## 2. Write interactives in VS Code, Cursor, and Typora
23
+
24
+ The Intelligent UI plugins ship inside `hd-garden`. You do not compile them in the site. They insert a one-sentence idea, preview the rendered component in the editor, open the component catalog, and hand the post to the `garden-ui` skill to compile.
25
+
26
+ `npx hd-garden init` copies the files to `.garden/editors/` (a `.vsix` for VS Code / Cursor, and the Typora plugin). In an interactive terminal it asks about VS Code / Cursor first, then Typora. The first `npx garden sync` (`make dev` syncs first) asks again if that prompt has not happened yet. `y` installs; pressing Enter skips.
27
+
28
+ With no TTY (a pipe or a CI log), with `CI=1`, or with `GARDEN_SKIP_EDITORS=1`, the command does not wait for input. It prints the install command below. You can run it later, and running it again is safe:
29
+
30
+ ```bash
31
+ npx garden editors install # Ask again about VS Code and Typora
32
+ npx garden editors install --yes # Install both, without asking
33
+ npx garden editors install --vscode # VS Code / Cursor only
34
+ npx garden editors install --typora # Typora only
35
+ ```
36
+
37
+ ### VS Code / Cursor
38
+
39
+ - **Insert an idea**: `Cmd+Alt+I` on macOS, `Ctrl+Alt+I` on Windows and Linux. The Command Palette and the editor context menu have the same Garden command. It inserts a plain `garden` fence: a short brief for the `garden-ui` skill, which compiles it later. In Typora, type `/` and choose `/garden`; the next list gives each component a small preview image.
40
+ - **Preview**: Open the Markdown preview. A `ui` fence renders the same component as the blog. An `intent` fence renders as an idea card. The extension reads sidecar data files next to the post.
41
+ - **Check**: Errors show up in the Problems panel and as squiggles in the source. A CodeLens above each block says whether it is ok, broken, stale, or not compiled yet.
42
+ - **Catalog**: The Garden catalog command inserts a recipe at the cursor when you choose to start from it. The same catalog from the terminal is `npx garden ui catalog` (it writes `.garden/ui-catalog.html`).
43
+ - **Compile**: Copy the compile prompt. On an English site (`locale: en_US`) it is `Compile this post:` plus the path. Paste that to an Agent that has the `garden-ui` skill. The skill runs `npx garden ui scan`, reads `npx garden ui prompt`, and writes the fence back with `npx garden ui apply`. The plugin does not call a model and does not store a key.
44
+
45
+ When `code` or `cursor` is on `PATH`, `garden editors install` runs `--install-extension`. If both commands exist, it installs into both. Without a CLI, open the Extensions view, choose the `...` menu, then **Install from VSIX**, and select the file under `.garden/editors/vscode/`.
46
+
47
+ ### Typora
48
+
49
+ Typora has no official plugin API. Install [typora-community-plugin](https://github.com/typora-community-plugin/typora-community-plugin/blob/main/docs/en-us/user-guide/1a-installation.md) first (on macOS it patches `index.html` inside `Typora.app`). `garden editors install` looks in the usual locations. If the framework is missing, it still copies the plugin and prints that prerequisite.
50
+
51
+ The plugin is copied to `~/.typora/community-plugins/plugins/hedon954.garden-intelligent-ui/`. On macOS it is also copied to `~/Library/Application Support/abnerworks.Typora/plugins/plugins/hedon954.garden-intelligent-ui/`, which is the directory the Installed Plugins list reads. Open Typora and enable Garden Intelligent UI under Settings, Installed plugins.
52
+
53
+ Use it the same way as VS Code: leaving a fence previews `ui` / `intent` (as with Mermaid, click into the fence to edit the source; clicks on sliders and buttons stay in the preview). Type `/`, choose `/garden`, or use the insert command (`Ctrl+Alt+I` on Windows and Linux; on macOS, Option rewrites the key, so record a shortcut under Settings, Keyboard Shortcuts). Copy compile prompt hands the post to the `garden-ui` skill.
54
+
55
+ A Typora upgrade overwrites the community framework inside the app. Reinstall typora-community-plugin, then enable this plugin again. The fences stay in the Markdown, and `make dev` still previews them.
56
+
57
+ The Obsidian plugin is not installed by this command.
58
+
59
+ ## 3. Describe components in plain language (humans and Agents share the same CLI)
23
60
 
24
61
  Where you want a component or interactive calculation, write an unnamed `garden` fence describing what the reader should adjust and see:
25
62
 
@@ -29,9 +66,9 @@ Where you want a component or interactive calculation, write an unnamed `garden`
29
66
  ```
30
67
  ````
31
68
 
32
- 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.
33
70
 
34
- ## 3. Common commands
71
+ ## 4. Common commands
35
72
 
36
73
  ```bash
37
74
  make new SLUG=writing-pace TITLE="Writing Pace" # Create a draft post in content/posts/
@@ -42,9 +79,10 @@ make update MESSAGE="Add writing-pace post" # Check, commit, and push to ori
42
79
  npx garden check content/posts/writing-pace.md # Validate front matter, components, and assets
43
80
  npx garden ui check --staged # Check staged posts for uncompiled ideas or UI errors
44
81
  npx garden ui catalog # Generate an offline component gallery at .garden/ui-catalog.html
82
+ npx garden editors install --yes # Install the Intelligent UI plugins into VS Code / Cursor and Typora
45
83
  ```
46
84
 
47
- ## 4. Excalidraw
85
+ ## 5. Excalidraw
48
86
 
49
87
  Point a Markdown image at the `.excalidraw` source. `garden build` exports one light flat SVG beside that file, with a transparent background, and embeds that SVG in the article so its text can be selected. No separate export command.
50
88
 
@@ -54,7 +92,7 @@ Point a Markdown image at the `.excalidraw` source. `garden build` exports one l
54
92
 
55
93
  The first page that references `.excalidraw` downloads the exporter (Chromium is not required; fonts are embedded in the SVG). See [Local Image](docs/handbook/en/components/image.md).
56
94
 
57
- ## 5. Handbook and reference docs
95
+ ## 6. Handbook and reference docs
58
96
 
59
97
  Start `make dev` and open `/garden/en` (Chinese at `/garden`), or read the markdown sources directly:
60
98
 
package/README.md CHANGED
@@ -17,11 +17,48 @@ npm install
17
17
  make dev
18
18
  ```
19
19
 
20
- 公开包名是 `hd-garden`,装进站点后可使用 `npx garden`(不要直接运行未安装的 `npx garden init`,npm 上的 `garden` 是另一个包)。首次运行 `make dev` 会生成 `.env.local`、补齐 `.agents/skills/` 下的三个 AI skill,并在 `.git/hooks/pre-commit` 安装提交前检查,默认预览地址是 `http://localhost:3000`(使用说明在 `http://localhost:3000/garden`)。
20
+ 公开包名是 `hd-garden`,装进站点后可使用 `npx garden`(不要直接运行未安装的 `npx garden init`,npm 上的 `garden` 是另一个包)。`init` 会把 VS Code / Cursor 与 Typora 的 Intelligent UI 插件放进站点,并在交互终端里分别询问要不要装上(见下一节)。首次运行 `make dev` 会生成 `.env.local`、补齐 `.agents/skills/` 下的三个 AI skill,并在 `.git/hooks/pre-commit` 安装提交前检查,默认预览地址是 `http://localhost:3000`(使用说明在 `http://localhost:3000/garden`)。
21
21
 
22
22
  如果在本地使用了 npm 镜像,请把 `registry` 留在 `~/.npmrc`,不要让镜像域名写进 `package-lock.json`(提交前可运行 `node node_modules/hd-garden/scripts/check-lockfile-registry.mjs` 检查)。
23
23
 
24
- ## 2. 用一句话写组件(人和 Agent 用同一套工具)
24
+ ## 2. 在 VS Code、Cursor 和 Typora 里写交互
25
+
26
+ Intelligent UI 插件随 `hd-garden` 一起发布,不用在站点里编译源码。它做四件事:插入一句想法、在编辑器里预览画好的组件、打开组件清单,以及把「编译这篇」交给 `garden-ui` skill。
27
+
28
+ `npx hd-garden init` 把产物复制到站点的 `.garden/editors/`(VS Code 的 `.vsix`,以及 Typora 插件目录)。终端可交互时,会先问要不要装进 VS Code / Cursor,再问要不要装进 Typora。第一次 `npx garden sync`(`make dev` 会先同步)如果还没问过,会再问一次。回答 `y` 才会安装;直接回车是不装。
29
+
30
+ 没有 TTY(管道或 CI 日志)、`CI=1`,或设置了 `GARDEN_SKIP_EDITORS=1` 时,命令不会停下来等输入,只打印下面这条安装命令。之后随时可以装,重复执行也安全:
31
+
32
+ ```bash
33
+ npx garden editors install # 再问一次 VS Code 和 Typora
34
+ npx garden editors install --yes # 两个都装,不再询问
35
+ npx garden editors install --vscode # 只装 VS Code / Cursor
36
+ npx garden editors install --typora # 只装 Typora
37
+ ```
38
+
39
+ ### VS Code / Cursor
40
+
41
+ - **插入想法**:macOS 用 `Cmd+Alt+I`,Windows / Linux 用 `Ctrl+Alt+I`,或命令「插入 Garden 块」。插入的是不带组件名的 `garden` 围栏,正文用中文告诉你要写什么,交给 `garden-ui` skill 编译。Typora 里先输入 `/` 选 `/garden`,再从带小预览图的列表里选组件。
42
+ - **预览**:打开 Markdown 预览。`ui` 围栏画成和博客一样的组件,`intent` 围栏显示成想法卡片。同名目录里的数据文件由扩展读取。
43
+ - **检查**:错误进「问题」面板,并在源码上画波浪线。每个块上方的 CodeLens 标出正常、有错、过期或未编译。
44
+ - **组件清单**:命令「查看组件清单」。点「用它开始」把配方插进当前稿件。终端里的同一份清单是 `npx garden ui catalog`(生成 `.garden/ui-catalog.html`)。
45
+ - **编译**:命令「复制编译提示词」。中文站点复制 `编译这篇:<相对路径>`,英文站点(`locale: en_US`)复制 `Compile this post: <相对路径>`。把这句话发给装了 `garden-ui` skill 的 Agent,由它 `npx garden ui scan`、读 `npx garden ui prompt`、再 `npx garden ui apply`。插件不调用模型,也不保存密钥。
46
+
47
+ 本机有 `code` 或 `cursor` 命令时,`garden editors install` 会执行 `--install-extension`。两个命令都在时,会各装一次。没有命令行时,打开扩展视图,点右上角「…」,选 **Install from VSIX**(从 VSIX 安装),文件在 `.garden/editors/vscode/`。
48
+
49
+ ### Typora
50
+
51
+ Typora 没有官方插件接口。先按 [typora-community-plugin 的安装说明](https://github.com/typora-community-plugin/typora-community-plugin/blob/main/docs/en-us/user-guide/1a-installation.md) 装上社区框架(macOS 上它会修改 `Typora.app` 里的 `index.html`)。`garden editors install` 会检查常见安装位置;没找到时仍会复制插件,并打印这个前提。
52
+
53
+ 插件文件复制到 `~/.typora/community-plugins/plugins/hedon954.garden-intelligent-ui/`。macOS 同时写到 `~/Library/Application Support/abnerworks.Typora/plugins/plugins/hedon954.garden-intelligent-ui/`。打开 Typora,在「设置 → 已安装插件」里勾选 Garden Intelligent UI。
54
+
55
+ 用法和 VS Code 同一套:光标离开围栏时预览 `ui` / `intent`(和 Mermaid 一样,点进围栏编辑源码;点滑杆或按钮不会跳进源码);先输入 `/` 再选 `/garden`,或用命令「插入 Garden 块」,都会插入想法围栏(Windows / Linux 默认 `Ctrl+Alt+I`,macOS 的 Option 会改写按键,请到「设置 → 快捷键」里自己录);「Garden:复制编译提示词」交给 `garden-ui` skill。
56
+
57
+ 升级 Typora 会覆盖社区框架改过的程序文件,插件跟着失效。需要重新安装 typora-community-plugin,再勾选本插件。稿件里的围栏原文还在,也可以用 `make dev` 预览。
58
+
59
+ Obsidian 插件这一轮不随包安装。
60
+
61
+ ## 3. 用一句话写组件(人和 Agent 用同一套工具)
25
62
 
26
63
  在正文想放组件的位置写一个不带名字的 `garden` 围栏,只描述想让读者做什么、看到什么:
27
64
 
@@ -31,9 +68,9 @@ make dev
31
68
  ```
32
69
  ````
33
70
 
34
- 让 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 的修复提示词。
35
72
 
36
- ## 3. 常用命令
73
+ ## 4. 常用命令
37
74
 
38
75
  ```bash
39
76
  make new SLUG=writing-pace TITLE="慢慢写" # 在 content/posts/ 下创建文章草稿
@@ -44,9 +81,10 @@ make update MESSAGE="新增一篇文章" # 检查通过后提交并
44
81
  npx garden check content/posts/writing-pace.md # 单独检查一篇文章的标题、组件与资源路径
45
82
  npx garden ui check --staged # 检查暂存区稿件中的想法块与交互组件
46
83
  npx garden ui catalog # 在 .garden/ui-catalog.html 生成离线组件清单
84
+ npx garden editors install --yes # 把 Intelligent UI 插件装进 VS Code / Cursor 与 Typora
47
85
  ```
48
86
 
49
- ## 4. Excalidraw
87
+ ## 5. Excalidraw
50
88
 
51
89
  正文用 Markdown 图片指向 `.excalidraw` 源文件。`garden build` 会在同目录导出一张浅色扁平 SVG,画布背景透明,并把这张 SVG 嵌进正文,图里的文字可以选中。不需要单独的导出命令。
52
90
 
@@ -56,11 +94,11 @@ npx garden ui catalog # 在 .garden/ui-catalog.html 生
56
94
 
57
95
  页面里第一次出现 `.excalidraw` 时,构建会下载导出程序(不需要 Chromium;字体写进 SVG)。说明见[本地图片](docs/handbook/components/image.md)。
58
96
 
59
- ## 5. 使用说明与深入文档
97
+ ## 6. 使用说明与深入文档
60
98
 
61
99
  本地运行 `make dev` 后访问 `/garden`(英文版在 `/garden/en`),或直接阅读仓库内的文档:
62
100
 
63
- - **[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)。
64
102
  - **[从零开始](docs/quick-start.md)** 与 **[GitHub Pages 部署](docs/github-pages.md)**:单公开仓库或「私有源码仓库 + 公开页面仓库」的 Actions 配置与 Giscus 评论接入。
65
103
  - **[独立管理后台](docs/admin-service.md)**:可选的 `/admin` 服务(本地、Docker 或 Cloudflare Workers),支持网页发随想、阿里云 OSS 媒体直传与 `/admin/ui` 组件工作台。
66
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,7 @@ 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
99
  "reference/html": "html",
100
100
  "reference/interactive": "interactive",
101
101
  "reference/file": "file",
@@ -117,7 +117,7 @@ export const NAV_SHORT_TITLE: Record<string, string> = {
117
117
  "components/compare": "并排对比",
118
118
  "components/timeline": "时间线",
119
119
  "components/chart": "条形图表",
120
- "components/figure": "分区结构图",
120
+ "components/compose": "说明图",
121
121
  "components/html": "嵌入网页片段",
122
122
  "components/interactive": "可调公式与参数",
123
123
  "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",