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.
- package/.agents/skills/garden-ui/SKILL.md +11 -10
- package/README.en.md +1 -1
- package/README.md +2 -2
- package/app/components/garden/GardenBlock.tsx +20 -4
- package/app/components/garden/blocks.tsx +6 -118
- package/app/globals.css +1 -0
- package/app/lib/garden-docs-meta.ts +5 -3
- package/app/lib/generated-content.ts +1 -1
- package/app/lib/i18n.ts +0 -6
- package/app/lib/remark-garden.mjs +61 -5
- package/app/styles/a2.css +25 -112
- package/app/styles/tokens.css +5 -0
- package/docs/handbook/ai-writing.md +3 -3
- package/docs/handbook/components/compose.md +134 -0
- package/docs/handbook/components/html.md +1 -1
- package/docs/handbook/components/image.md +1 -1
- package/docs/handbook/components/interactive.md +4 -1
- package/docs/handbook/components/link-card.md +1 -1
- package/docs/handbook/components.md +23 -35
- package/docs/handbook/design.md +25 -34
- package/docs/handbook/en/ai-writing.md +3 -3
- package/docs/handbook/en/components/compose.md +123 -0
- package/docs/handbook/en/components/html.md +1 -1
- package/docs/handbook/en/components/image.md +1 -1
- package/docs/handbook/en/components/interactive.md +4 -1
- package/docs/handbook/en/components.md +23 -36
- package/docs/handbook/en/design.md +25 -33
- package/docs/handbook/en/index.md +5 -5
- package/docs/handbook/en/reference/compose-icons.md +863 -0
- package/docs/handbook/en/reference/compose.md +177 -0
- package/docs/handbook/en/reference.md +3 -2
- package/docs/handbook/en/workflow.md +8 -6
- package/docs/handbook/index.md +5 -5
- package/docs/handbook/reference/compose-icons.md +863 -0
- package/docs/handbook/reference/compose.md +177 -0
- package/docs/handbook/reference.md +3 -2
- package/docs/handbook/workflow.md +8 -6
- package/editors/dist/typora/main.js +269 -241
- package/editors/dist/vscode/garden-ui-0.1.0.vsix +0 -0
- package/package.json +14 -2
- package/scripts/gen-compose-handbook.mjs +427 -0
- package/scripts/gen-compose-icons.mjs +236 -0
- package/scripts/handbook-allowlist.mjs +0 -8
- package/scripts/handbook-facts.mjs +6 -36
- package/scripts/ui-cli.mjs +5 -1
- package/templates/garden-blocks.md +34 -31
- package/templates/garden-snippets.plist +3 -8
- package/templates/skills-en/garden-ui/SKILL.md +11 -10
- package/ui/README.md +1 -0
- package/ui/compose/compose.css +1324 -0
- package/ui/compose/dil.mjs +443 -0
- package/ui/compose/examples.mjs +28 -0
- package/ui/compose/icons.mjs +774 -0
- package/ui/compose/index.mjs +8 -0
- package/ui/compose/markup.mjs +727 -0
- package/ui/compose/parse.mjs +28 -0
- package/ui/compose/render.mjs +385 -0
- package/ui/compose/tokens.mjs +50 -0
- package/ui/core/cli-locale.mjs +4 -0
- package/ui/core/fence.mjs +13 -1
- package/ui/core/fix-prompt.mjs +1 -0
- package/ui/core/garden-fence.mjs +4 -1
- package/ui/core/scan.mjs +28 -2
- package/ui/core/transform.mjs +12 -0
- package/ui/core/validate.mjs +6 -4
- package/ui/garden/catalog.mjs +58 -35
- package/ui/garden/check.mjs +22 -6
- package/ui/garden/parse.mjs +26 -0
- package/ui/garden/snippets.mjs +15 -13
- package/ui/garden/word-count.mjs +10 -18
- package/ui/preview/dist/garden-ui.esm.js +268 -242
- package/ui/preview/dist/garden-ui.iife.js +268 -242
- package/ui/prompt/build.mjs +1 -0
- package/ui/prompt/recipes/compose.md +40 -0
- package/ui/prompt/rules.md +3 -3
- package/ui/react/CatalogPage.tsx +2 -2
- package/ui/react/runtime.tsx +5 -1
- package/docs/handbook/components/figure.md +0 -59
- package/docs/handbook/en/components/figure.md +0 -59
- package/docs/handbook/en/reference/figure.md +0 -84
- 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
|
|
6
|
-
html), fixes broken garden
|
|
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
|
|
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
|
|
49
|
+
- **写 `garden compose`**:读者需要拖、点、翻才能看懂时。
|
|
50
50
|
- **写一段 `diff`**:想法紧跟在一段完整代码后面,说的是「和上一版比改了什么」时。按原话写出 diff,用一段普通 Markdown 写回(只有这一个 `diff` 围栏)。写回后它紧挨着那段代码,页面会合成「代码 / 对比上一版」切换;diff 里的新行要和上面的完整代码一致。
|
|
51
|
-
- **写别的 `garden` 组件**:一张或多张资源卡(`card` / `cards`)、几个指标(`stat`)、两种做法并排或好坏对照(`compare` / `verdict: good|bad`)、一条时间线(`timeline`)、一张静态图表(`chart
|
|
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
|
|
55
|
-
- 格式:围栏头写 `garden
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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 "
|
|
41
|
-
return <
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
560
|
-
const
|
|
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
|
@@ -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/
|
|
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/
|
|
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/
|
|
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
|
|
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\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
|
|
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
|
|