hd-garden 0.13.3 → 0.14.2

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 (218) hide show
  1. package/.agents/skills/garden-compose/SKILL.md +24 -0
  2. package/.agents/skills/garden-compose/references/colors.md +11 -0
  3. package/.agents/skills/garden-compose/references/composition-patterns.md +162 -0
  4. package/.agents/skills/garden-compose/references/examples.md +55 -0
  5. package/.agents/skills/garden-compose/references/frames.md +7 -0
  6. package/.agents/skills/garden-compose/references/layout-recipes.md +84 -0
  7. package/.agents/skills/garden-compose/references/rendering-model.md +120 -0
  8. package/.agents/skills/garden-compose/references/static-play.md +19 -0
  9. package/.agents/skills/garden-compose/references/visual-selection.md +38 -0
  10. package/.agents/skills/garden-compose/references/workflow.md +27 -0
  11. package/.agents/skills/garden-compose/scripts/_cli.mjs +12 -0
  12. package/.agents/skills/garden-compose/scripts/apply.mjs +2 -0
  13. package/.agents/skills/garden-compose/scripts/check.mjs +2 -0
  14. package/.agents/skills/garden-compose/scripts/prompt.mjs +2 -0
  15. package/.agents/skills/garden-compose/scripts/scan.mjs +2 -0
  16. package/.agents/skills/garden-post-review/SKILL.md +5 -16
  17. package/Makefile +12 -3
  18. package/README.en.md +10 -5
  19. package/README.md +10 -5
  20. package/app/admin/preview/[...path]/page.tsx +4 -16
  21. package/app/api/admin/ui/blocks/route.ts +1 -1
  22. package/app/components/AdminUiPanels.tsx +6 -3
  23. package/app/components/GardenContentGuide.tsx +7 -8
  24. package/app/components/MarkdownArticle.tsx +2 -0
  25. package/app/components/UiStudio.tsx +29 -32
  26. package/app/components/garden/AuthorRich.tsx +5 -0
  27. package/app/components/garden/ComposePreview.tsx +9 -0
  28. package/app/components/garden/GardenBlock.tsx +12 -10
  29. package/app/components/garden/GardenFile.tsx +1 -1
  30. package/app/components/garden/blocks.tsx +9 -197
  31. package/app/components/garden/file-icons.tsx +1 -1
  32. package/app/components/garden/patterns.tsx +7 -65
  33. package/app/lib/rehype-garden-blocks.mjs +30 -0
  34. package/app/lib/remark-garden.mjs +0 -4
  35. package/app/lib/ui-model.ts +8 -9
  36. package/app/styles/a2.css +17 -1
  37. package/bin/garden.mjs +34 -35
  38. package/docs/handbook/ai-writing.md +19 -22
  39. package/docs/handbook/components/compose.md +16 -7
  40. package/docs/handbook/components/image.md +1 -1
  41. package/docs/handbook/components/link-card.md +2 -2
  42. package/docs/handbook/components.md +5 -213
  43. package/docs/handbook/deploy.md +11 -0
  44. package/docs/handbook/en/ai-writing.md +17 -20
  45. package/docs/handbook/en/components/compose.md +15 -6
  46. package/docs/handbook/en/components/image.md +1 -1
  47. package/docs/handbook/en/components/link-card.md +2 -2
  48. package/docs/handbook/en/components.md +7 -215
  49. package/docs/handbook/en/deploy.md +11 -0
  50. package/docs/handbook/en/index.md +29 -53
  51. package/docs/handbook/en/reference/compose.md +77 -148
  52. package/docs/handbook/en/reference.md +6 -22
  53. package/docs/handbook/en/start.md +1 -1
  54. package/docs/handbook/en/workflow.md +21 -17
  55. package/docs/handbook/en/writing.md +3 -3
  56. package/docs/handbook/index.md +26 -47
  57. package/docs/handbook/reference/compose.md +79 -148
  58. package/docs/handbook/reference.md +6 -22
  59. package/docs/handbook/start.md +1 -1
  60. package/docs/handbook/workflow.md +21 -17
  61. package/docs/handbook/writing.md +3 -3
  62. package/editors/dist/typora/main.js +324 -330
  63. package/editors/dist/typora/style.css +4299 -11
  64. package/editors/dist/vscode/garden-ui-0.14.2.vsix +0 -0
  65. package/eslint.config.mjs +3 -0
  66. package/package.json +5 -3
  67. package/public/images/compose-resources/resource-015b41eb0f32.svg +1 -0
  68. package/public/images/compose-resources/resource-ecee6b47f015.svg +1 -0
  69. package/public/images/compose-resources/resource-fd51121b5e8c.svg +1 -0
  70. package/scripts/build-content.mjs +40 -131
  71. package/scripts/build-ui-preview.mjs +7 -5
  72. package/scripts/check-post.mjs +10 -106
  73. package/scripts/check-site.mjs +80 -0
  74. package/scripts/compose-style-reference.mjs +22 -0
  75. package/scripts/editor-preview-css.mjs +14 -0
  76. package/scripts/file-preview.mjs +1 -1
  77. package/scripts/file-sizes.mjs +7 -7
  78. package/scripts/generated-cache.mjs +36 -0
  79. package/scripts/handbook-facts.mjs +10 -3
  80. package/scripts/image-sizes.mjs +6 -12
  81. package/scripts/link-assets.mjs +35 -10
  82. package/scripts/managed-skills.mjs +56 -0
  83. package/scripts/pack-editors.mjs +12 -1
  84. package/scripts/prepare-pages-output.mjs +1 -1
  85. package/scripts/site-guide.mjs +19 -0
  86. package/scripts/sync-editor-version.mjs +28 -0
  87. package/scripts/ui-cli.mjs +26 -33
  88. package/templates/garden-blocks.md +4 -180
  89. package/templates/garden-snippets.plist +2 -183
  90. package/templates/site/AGENTS.en.md +89 -0
  91. package/templates/site/AGENTS.md +72 -0
  92. package/templates/site/Makefile +12 -3
  93. package/templates/site/Makefile.en +12 -3
  94. package/templates/site/README.en.md +6 -1
  95. package/templates/site/README.md +6 -1
  96. package/templates/skills-en/garden-compose/SKILL.md +24 -0
  97. package/templates/skills-en/garden-post-review/SKILL.md +5 -16
  98. package/templates/workflows/pages.en.yml +11 -0
  99. package/templates/workflows/pages.yml +11 -0
  100. package/ui/compose/compose.css +54 -14
  101. package/ui/compose/contracts.mjs +16 -0
  102. package/ui/compose/dil.mjs +109 -48
  103. package/ui/compose/distribution.mjs +7 -0
  104. package/ui/compose/features.mjs +90 -0
  105. package/ui/compose/icons.mjs +8 -0
  106. package/ui/compose/index.mjs +2 -0
  107. package/ui/compose/inspect.mjs +11 -0
  108. package/ui/compose/markup.mjs +80 -23
  109. package/ui/compose/preview.mjs +13 -0
  110. package/ui/compose/prompt-catalog.mjs +15 -3
  111. package/ui/compose/render.mjs +46 -11
  112. package/ui/compose/scenes.mjs +10 -0
  113. package/ui/core/apply.mjs +12 -45
  114. package/ui/core/catalog.mjs +22 -32
  115. package/ui/core/cli-locale.mjs +8 -18
  116. package/ui/core/color.mjs +34 -0
  117. package/ui/core/compile.mjs +1 -8
  118. package/ui/core/demos.mjs +138 -108
  119. package/ui/core/fence.mjs +14 -70
  120. package/ui/core/fix-prompt.mjs +1 -3
  121. package/ui/core/garden-fence.mjs +3 -63
  122. package/ui/core/index.mjs +3 -5
  123. package/ui/core/parse.mjs +22 -8
  124. package/ui/core/scan.mjs +31 -43
  125. package/ui/core/starters.mjs +3 -3
  126. package/ui/core/static.mjs +36 -6
  127. package/ui/core/transform.mjs +64 -149
  128. package/ui/core/validate.mjs +20 -9
  129. package/ui/garden/article-content.mjs +98 -0
  130. package/ui/garden/catalog.mjs +21 -427
  131. package/ui/garden/check.mjs +18 -60
  132. package/ui/garden/file-block.mjs +1 -1
  133. package/ui/garden/file-paths.mjs +8 -6
  134. package/ui/garden/parse.mjs +19 -182
  135. package/ui/garden/relations.mjs +1 -1
  136. package/ui/garden/snippets.mjs +12 -153
  137. package/ui/garden/source-icon.mjs +17 -9
  138. package/ui/garden/word-count.mjs +16 -85
  139. package/ui/index.mjs +1 -5
  140. package/ui/preview/dist/garden-ui.css +4300 -11
  141. package/ui/preview/dist/garden-ui.esm.js +446 -418
  142. package/ui/preview/dist/garden-ui.iife.js +446 -418
  143. package/ui/preview/index.tsx +31 -98
  144. package/ui/preview/site-config.ts +4 -0
  145. package/ui/prompt/build.mjs +1 -4
  146. package/ui/prompt/recipes/code-walkthrough.md +17 -12
  147. package/ui/prompt/recipes/compare.md +14 -10
  148. package/ui/prompt/recipes/compose.md +3 -3
  149. package/ui/prompt/recipes/converter.md +17 -14
  150. package/ui/prompt/recipes/data-chart.md +9 -8
  151. package/ui/prompt/recipes/inspect-grid.md +9 -8
  152. package/ui/prompt/recipes/param-bench.md +17 -12
  153. package/ui/prompt/recipes/plan.md +4 -3
  154. package/ui/prompt/recipes/stepper.md +18 -16
  155. package/ui/prompt/recipes/trace.md +20 -18
  156. package/ui/prompt/rules.md +26 -133
  157. package/ui/prompt/studio.md +3 -18
  158. package/ui/react/CatalogPage.tsx +19 -21
  159. package/ui/react/recipes.ts +3 -3
  160. package/ui/react/runtime.tsx +23 -2
  161. package/ui/react/ui.css +17 -11
  162. package/vite.config.ts +16 -3
  163. package/.agents/skills/garden-interactive-chart/SKILL.md +0 -84
  164. package/.agents/skills/garden-interactive-chart/references/shell.html +0 -49
  165. package/.agents/skills/garden-interactive-chart/references/style.md +0 -61
  166. package/.agents/skills/garden-ui/SKILL.md +0 -125
  167. package/app/components/UiBlock.tsx +0 -23
  168. package/app/components/WidgetChart.tsx +0 -224
  169. package/app/lib/generated-content.ts +0 -385
  170. package/docs/handbook/components/card.md +0 -67
  171. package/docs/handbook/components/chart.md +0 -38
  172. package/docs/handbook/components/choice.md +0 -42
  173. package/docs/handbook/components/compare.md +0 -53
  174. package/docs/handbook/components/file.md +0 -29
  175. package/docs/handbook/components/html.md +0 -49
  176. package/docs/handbook/components/interactive.md +0 -48
  177. package/docs/handbook/components/list.md +0 -65
  178. package/docs/handbook/components/relations.md +0 -95
  179. package/docs/handbook/components/stat.md +0 -39
  180. package/docs/handbook/components/timeline.md +0 -37
  181. package/docs/handbook/en/components/card.md +0 -67
  182. package/docs/handbook/en/components/chart.md +0 -38
  183. package/docs/handbook/en/components/choice.md +0 -42
  184. package/docs/handbook/en/components/compare.md +0 -53
  185. package/docs/handbook/en/components/file.md +0 -29
  186. package/docs/handbook/en/components/html.md +0 -49
  187. package/docs/handbook/en/components/interactive.md +0 -44
  188. package/docs/handbook/en/components/list.md +0 -65
  189. package/docs/handbook/en/components/relations.md +0 -95
  190. package/docs/handbook/en/components/stat.md +0 -39
  191. package/docs/handbook/en/components/timeline.md +0 -37
  192. package/docs/handbook/en/reference/card.md +0 -85
  193. package/docs/handbook/en/reference/chart.md +0 -68
  194. package/docs/handbook/en/reference/choice.md +0 -69
  195. package/docs/handbook/en/reference/compare.md +0 -59
  196. package/docs/handbook/en/reference/file.md +0 -50
  197. package/docs/handbook/en/reference/html.md +0 -62
  198. package/docs/handbook/en/reference/interactive.md +0 -71
  199. package/docs/handbook/en/reference/list.md +0 -72
  200. package/docs/handbook/en/reference/relations.md +0 -113
  201. package/docs/handbook/en/reference/stat.md +0 -60
  202. package/docs/handbook/en/reference/timeline.md +0 -56
  203. package/docs/handbook/reference/card.md +0 -85
  204. package/docs/handbook/reference/chart.md +0 -68
  205. package/docs/handbook/reference/choice.md +0 -69
  206. package/docs/handbook/reference/compare.md +0 -59
  207. package/docs/handbook/reference/file.md +0 -50
  208. package/docs/handbook/reference/html.md +0 -62
  209. package/docs/handbook/reference/interactive.md +0 -71
  210. package/docs/handbook/reference/list.md +0 -72
  211. package/docs/handbook/reference/relations.md +0 -113
  212. package/docs/handbook/reference/stat.md +0 -60
  213. package/docs/handbook/reference/timeline.md +0 -56
  214. package/editors/dist/vscode/garden-ui-0.1.1.vsix +0 -0
  215. package/templates/skills-en/garden-interactive-chart/SKILL.md +0 -84
  216. package/templates/skills-en/garden-interactive-chart/references/shell.html +0 -49
  217. package/templates/skills-en/garden-interactive-chart/references/style.md +0 -61
  218. package/templates/skills-en/garden-ui/SKILL.md +0 -125
@@ -1,15 +1,12 @@
1
1
  "use client";
2
+ import { AuthorRich as Rich } from "./AuthorRich";
2
3
 
3
- import { useEffect, useRef, type ReactNode } from "react";
4
- import ReactMarkdown from "react-markdown";
4
+ import { type ReactNode } from "react";
5
5
  import { useUi, useUiLang } from "../UiLang";
6
- import { langOf, ui } from "../../lib/i18n";
7
- import { interactiveUiSource } from "../../../ui/core/garden-fence.mjs";
8
- import { markdownPlugins } from "../../lib/markdown-plugins";
9
6
  import { compileUiBlock } from "../../../ui/core/compile.mjs";
10
7
  import { splitCompareLine } from "../../../ui/garden/compare-line.mjs";
11
8
  import { RuntimeLangProvider, UiErrorCard, UiFigure, type Diagnostic, type UiPayload } from "../../../ui/react";
12
- import { FullscreenFrame, SandboxFrame } from "./chrome";
9
+ import { FullscreenFrame } from "./chrome";
13
10
  import { SourceMark, VerdictIcon } from "./patterns";
14
11
 
15
12
  const BASE_PATH = (process.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/+$/u, "");
@@ -20,13 +17,7 @@ function publicSrc(src: string) {
20
17
  return `${BASE_PATH}${src}`;
21
18
  }
22
19
 
23
- function Rich({ text }: { text: string }) {
24
- return (
25
- <ReactMarkdown remarkPlugins={markdownPlugins} components={{ p: ({ children }) => <>{children}</> }}>
26
- {String(text ?? "")}
27
- </ReactMarkdown>
28
- );
29
- }
20
+
30
21
 
31
22
  function hostOf(url: string) {
32
23
  try {
@@ -265,188 +256,11 @@ export function GardenChart({ props }: { props: Record<string, unknown> }) {
265
256
  );
266
257
  }
267
258
 
268
- function htmlHasScript(source: string) {
269
- return /<script\b(?![^>]*\btype\s*=\s*["']application\/json["'])/iu.test(source);
270
- }
271
-
272
- function isLightWidget(source: string) {
273
- return /data-g="(?:quiz|calc|tabs|stepper|kvplay)"|class="[^"]*\b(?:g-steps|g-faq|g-quiz|g-tabs|g-play|g-calc)\b/iu.test(source);
274
- }
275
-
276
- function bindGardenWidgets(root: HTMLElement) {
277
- const attr = root.closest("[lang]")?.getAttribute("lang");
278
- const t = ui(attr ? langOf(attr) : undefined);
279
- root.querySelectorAll<HTMLElement>("[data-g=tabs]").forEach((widget) => {
280
- const list = widget.querySelector<HTMLElement>(":scope > [role=tablist]");
281
- const tabs = [...(list?.querySelectorAll<HTMLElement>(":scope > [role=tab]") ?? [])];
282
- const select = (index: number, focus = false) => {
283
- tabs.forEach((tab, tabIndex) => {
284
- const on = index === tabIndex;
285
- tab.setAttribute("aria-selected", String(on));
286
- tab.tabIndex = on ? 0 : -1;
287
- const panel = document.getElementById(tab.getAttribute("aria-controls") || "");
288
- if (panel) panel.hidden = !on;
289
- });
290
- if (focus) tabs[index]?.focus();
291
- };
292
- tabs.forEach((tab, index) => {
293
- tab.addEventListener("click", () => select(index));
294
- tab.addEventListener("keydown", (event) => {
295
- const count = tabs.length;
296
- const next = event.key === "ArrowRight" ? (index + 1) % count
297
- : event.key === "ArrowLeft" ? (index - 1 + count) % count
298
- : event.key === "Home" ? 0
299
- : event.key === "End" ? count - 1 : -1;
300
- if (next < 0) return;
301
- event.preventDefault();
302
- select(next, true);
303
- });
304
- });
305
- });
306
-
307
- root.querySelectorAll<HTMLFormElement>("[data-g=quiz]").forEach((form) => {
308
- const out = form.querySelector<HTMLElement>(".g-quiz-out");
309
- const explain = form.querySelector<HTMLElement>(".g-quiz-ex");
310
- form.addEventListener("submit", (event) => {
311
- event.preventDefault();
312
- if (!out) return;
313
- const chosen = form.querySelector<HTMLInputElement>("input:checked");
314
- if (!chosen) {
315
- out.className = "g-quiz-out";
316
- out.textContent = t.pickAnswer;
317
- return;
318
- }
319
- const correct = chosen.value === form.dataset.answer;
320
- out.className = `g-quiz-out ${correct ? "ok" : "no"}`;
321
- out.textContent = correct ? t.correct : t.wrong;
322
- if (explain) explain.hidden = !correct;
323
- });
324
- });
325
-
326
- root.querySelectorAll<HTMLElement>("[data-g=stepper]").forEach((stepper) => {
327
- const panels = [...stepper.querySelectorAll<HTMLElement>("[data-step]")];
328
- const dots = [...stepper.querySelectorAll<HTMLElement>(".g-stepper-dots i")];
329
- const count = stepper.querySelector(".g-stepper-n");
330
- const title = stepper.querySelector(".g-stepper-t");
331
- const prev = stepper.querySelector<HTMLButtonElement>("[data-prev]");
332
- const next = stepper.querySelector<HTMLButtonElement>("[data-next]");
333
- let index = 0;
334
- const show = () => {
335
- panels.forEach((panel, panelIndex) => { panel.hidden = panelIndex !== index; });
336
- if (count) count.textContent = `${index + 1} / ${panels.length}`;
337
- if (title) title.textContent = panels[index]?.dataset.title || "";
338
- dots.forEach((dot, dotIndex) => dot.classList.toggle("on", dotIndex <= index));
339
- if (prev) prev.disabled = index === 0;
340
- if (next) next.disabled = index === panels.length - 1;
341
- };
342
- prev?.addEventListener("click", () => { if (index > 0) { index -= 1; show(); } });
343
- next?.addEventListener("click", () => { if (index < panels.length - 1) { index += 1; show(); } });
344
- show();
345
- });
346
-
347
- root.querySelectorAll<HTMLElement>("[data-g=calc]").forEach((calc) => {
348
- const raw = calc.querySelector("script[type='application/json'], script[type=\"application/json\"]")?.textContent;
349
- if (!raw) return;
350
- const spec = JSON.parse(raw) as {
351
- formula: string;
352
- vars: Record<string, { fixed?: number; values?: number[]; label?: string }>;
353
- format?: string;
354
- unit?: string;
355
- show?: string;
356
- };
357
- const names = Object.keys(spec.vars);
358
- const fn = new Function(...names, `return (${spec.formula})`) as (...args: number[]) => number;
359
- const read = (key: string) => {
360
- const variable = spec.vars[key];
361
- if (variable?.fixed !== undefined) return Number(variable.fixed);
362
- const field = calc.querySelector<HTMLInputElement | HTMLSelectElement>(`[data-k="${key}"]:checked`)
363
- ?? calc.querySelector<HTMLInputElement | HTMLSelectElement>(`[data-k="${key}"]`);
364
- if (!field) return 0;
365
- if (variable?.values && field instanceof HTMLInputElement && field.type === "range") {
366
- const value = variable.values[Number(field.value)] ?? variable.values[0];
367
- const output = field.parentElement?.querySelector("output");
368
- if (output) output.textContent = String(value);
369
- field.setAttribute("aria-valuetext", String(value));
370
- return Number(value);
371
- }
372
- const value = Number(field.value);
373
- const output = field.parentElement?.querySelector("output");
374
- if (output && field instanceof HTMLInputElement && field.type === "range") output.textContent = field.value;
375
- return value;
376
- };
377
- const run = () => {
378
- const values = Object.fromEntries(names.map((key) => [key, read(key)]));
379
- const result = fn(...names.map((key) => values[key]));
380
- const out = calc.querySelector("[data-out]");
381
- if (out) {
382
- out.textContent = spec.format === "GB"
383
- ? `${(result / 2 ** 30).toFixed(2)} GB`
384
- : `${Math.round(result * 1e4) / 1e4}${spec.unit ? ` ${spec.unit}` : ""}`;
385
- }
386
- const equation = calc.querySelector("[data-calc]");
387
- if (equation && spec.show) {
388
- const shown = spec.show.replace(/\{(\w+)\}/g, (_, key: string) => String(values[key]));
389
- equation.textContent = `${shown} = ${spec.format === "GB" ? `${Math.round(result).toLocaleString("en-US")} ${t.bytes}` : String(Math.round(result * 1e4) / 1e4)}`;
390
- }
391
- };
392
- calc.addEventListener("input", run);
393
- calc.addEventListener("change", run);
394
- run();
395
- });
396
- }
397
-
398
- function StaticGardenHtml({ code }: { code: string }) {
399
- const ref = useRef<HTMLDivElement>(null);
400
- useEffect(() => {
401
- const host = ref.current;
402
- if (!host) return;
403
- const root = host.shadowRoot ?? host.attachShadow({ mode: "open" });
404
- root.innerHTML = code;
405
- }, [code]);
406
- return (
407
- <div
408
- className="g-html"
409
- ref={ref}
410
- suppressHydrationWarning
411
- dangerouslySetInnerHTML={{ __html: `<template shadowrootmode="open">${code}</template>` }}
412
- />
413
- );
414
- }
415
-
416
- function LightGardenHtml({ code }: { code: string }) {
417
- const ref = useRef<HTMLDivElement>(null);
418
- useEffect(() => {
419
- const host = ref.current;
420
- if (host) bindGardenWidgets(host);
421
- }, [code]);
422
- return <div className="g-widget" ref={ref} dangerouslySetInnerHTML={{ __html: code }} />;
423
- }
424
-
425
- export function GardenHtml({ props, children }: { props: Record<string, unknown>; children?: string }) {
426
- const code = typeof props.code === "string" ? props.code : children || "";
427
- const t = useUi();
428
- const title = String(props.title ?? "HTML");
429
- if (!code && typeof props.src === "string") {
430
- return <p className="g-embed-note">{t.htmlAt(String(props.src))}</p>;
431
- }
432
- if (htmlHasScript(code)) {
433
- return <SandboxFrame html={code} title={title} />;
434
- }
435
- if (isLightWidget(code)) {
436
- const view = <LightGardenHtml code={code} />;
437
- return /class="[^"]*\bg-play\b|data-g="calc"/iu.test(code) ? <FullscreenFrame label={title}>{view}</FullscreenFrame> : view;
438
- }
439
- return <FullscreenFrame label={title}><StaticGardenHtml code={code} /></FullscreenFrame>;
440
- }
441
-
442
- export function GardenInteractive({ block }: { block: { id?: string | null; props: Record<string, unknown>; children?: string; uiSource?: string } }) {
259
+ export function GardenInteractive({ block, renderScene }: { renderScene?: (source: string) => React.ReactNode; block: { id?: string | null; props: Record<string, unknown>; children?: string; uiSource?: string; payload?: UiPayload | null } }) {
443
260
  const t = useUi();
444
261
  const lang = useUiLang();
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;
449
- const { payload, check } = compileUiBlock(source);
262
+ const { payload, check } = block.payload ? {payload:block.payload,check:{header:{id:block.payload.id,caption:block.payload.caption},diagnostics:[]}} : compileUiBlock(block.uiSource ?? "");
263
+ const header = {id:payload?.id??block.id,caption:payload?.caption??block.props.caption};
450
264
  if (!payload) {
451
265
  return (
452
266
  <RuntimeLangProvider lang={lang}>
@@ -462,7 +276,7 @@ export function GardenInteractive({ block }: { block: { id?: string | null; prop
462
276
  <RuntimeLangProvider lang={lang}>
463
277
  <FullscreenFrame label={String(header.caption || t.interactive)}>
464
278
  <div className="g-play">
465
- <UiFigure payload={payload as UiPayload} />
279
+ <UiFigure payload={payload as UiPayload} host={renderScene ? {renderScene} : undefined} />
466
280
  </div>
467
281
  </FullscreenFrame>
468
282
  </RuntimeLangProvider>
@@ -479,9 +293,7 @@ export function GardenIdea({ text }: { text: string }) {
479
293
  );
480
294
  }
481
295
 
482
- export function GardenLegacy({ suggestion }: { suggestion: string }) {
483
- return <p className="g-embed-note">{suggestion}</p>;
484
- }
296
+
485
297
 
486
298
  export function GardenUnknown({ name }: { name: string }) {
487
299
  const t = useUi();
@@ -1,6 +1,6 @@
1
1
  export { safeFileHref } from "../../../ui/garden/file-block.mjs";
2
2
 
3
- // Icons for `garden file`. No hooks: used by the server card and the client drawer alike.
3
+ // Icons for Compose file. No hooks: used by the server card and the client drawer alike.
4
4
  const PAGE = "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z";
5
5
  const FOLD = "M14 3v5h5";
6
6
  const MARKS: Record<string, string> = {
@@ -1,76 +1,18 @@
1
1
  "use client";
2
+ import { hasIcon, iconSvg } from "../../../ui/compose/icons.mjs";
3
+ import { AuthorRich as Rich } from "./AuthorRich";
2
4
 
3
- import { createContext, useContext, useEffect, useId, useRef, useState, type ComponentType, type CSSProperties, type ReactNode } from "react";
5
+ import { createContext, useContext, useEffect, useId, useRef, useState, type CSSProperties, type ReactNode } from "react";
4
6
  import { sourceIcon } from "../../../ui/garden/source-icon.mjs";
5
7
  import { circled, labelFits, legendLine, placeMarks, relationKeyAction, RELATION_COMPACT_WIDTH, RELATION_LABEL_FONT_MIN, relationEdges, relationHeights, relationIndex, relationLayout, relationLines, type RelationEdge as Edge } from "../../../ui/garden/relations.mjs";
6
- import ReactMarkdown from "react-markdown";
7
- import {
8
- Bell,
9
- BookOpen,
10
- ChartBar,
11
- ChatCircle,
12
- CheckCircle,
13
- Clock,
14
- Eye,
15
- FileText,
16
- Flag,
17
- Flask,
18
- Gear,
19
- Heart,
20
- Lightbulb,
21
- LinkSimple,
22
- MagnifyingGlass,
23
- MapPin,
24
- Question,
25
- ShieldCheck,
26
- Star,
27
- Target,
28
- User,
29
- Wallet,
30
- XCircle,
31
- type IconProps,
32
- } from "@phosphor-icons/react/ssr";
8
+ import {CheckCircle, XCircle} from "@phosphor-icons/react/ssr";
33
9
  import { useUi, useUiLang } from "../UiLang";
34
- import { markdownPlugins } from "../../lib/markdown-plugins";
35
10
 
36
- function Rich({ text }: { text: string }) {
37
- return (
38
- <ReactMarkdown remarkPlugins={markdownPlugins} components={{ p: ({ children }) => <>{children}</> }}>
39
- {String(text ?? "")}
40
- </ReactMarkdown>
41
- );
42
- }
43
11
 
44
- /** Line icons an author can name in `icon:`. Unknown names draw nothing. */
45
- export const GARDEN_ICONS: Record<string, ComponentType<IconProps>> = {
46
- search: MagnifyingGlass,
47
- eye: Eye,
48
- heart: Heart,
49
- chat: ChatCircle,
50
- shield: ShieldCheck,
51
- wallet: Wallet,
52
- bell: Bell,
53
- check: CheckCircle,
54
- x: XCircle,
55
- star: Star,
56
- flag: Flag,
57
- clock: Clock,
58
- idea: Lightbulb,
59
- book: BookOpen,
60
- link: LinkSimple,
61
- user: User,
62
- chart: ChartBar,
63
- gear: Gear,
64
- file: FileText,
65
- pin: MapPin,
66
- flask: Flask,
67
- target: Target,
68
- question: Question,
69
- };
70
12
 
71
- export function GardenIcon({ name, size = 18 }: { name: unknown; size?: number }) {
72
- const Icon = typeof name === "string" ? GARDEN_ICONS[name.trim().toLowerCase()] : undefined;
73
- return Icon ? <Icon className="g-ic" size={size} weight="regular" aria-hidden="true" /> : null;
13
+ export function GardenIcon({name,size=18}:{name:unknown;size?:number}) {
14
+ if(!hasIcon(String(name)))return null;
15
+ return <span aria-hidden="true" dangerouslySetInnerHTML={{__html:iconSvg(String(name)).replace('class="g-compose-icon"',`class="g-compose-icon" style="width:${size}px;height:${size}px"`)}}/>;
74
16
  }
75
17
 
76
18
  const textsOf = (value: unknown) => (value == null ? [] : Array.isArray(value) ? value.map(String) : [String(value)]);
@@ -0,0 +1,30 @@
1
+ const FLOW_TAGS = new Set(['garden-block', 'garden-refdemo', 'garden-codediff']);
2
+
3
+ /** Custom elements are phrasing content to the HTML parser, but render as flow.
4
+ * Lift them before React renders them into figures/divs inside a paragraph. */
5
+ export function rehypeGardenBlocks() {
6
+ return tree => {
7
+ function visit(parent) {
8
+ if (!Array.isArray(parent.children)) return;
9
+ parent.children = parent.children.flatMap(node => {
10
+ visit(node);
11
+ if (node.type !== 'element' || node.tagName !== 'p' || !node.children.some(child => child.type === 'element' && FLOW_TAGS.has(child.tagName))) return [node];
12
+ const result = [];
13
+ let prose = [];
14
+ const flush = () => {
15
+ if (prose.some(child => child.type !== 'text' || child.value.trim())) result.push({...node, children:prose});
16
+ prose = [];
17
+ };
18
+ for (const child of node.children) {
19
+ if (child.type === 'element' && FLOW_TAGS.has(child.tagName)) {
20
+ flush();
21
+ result.push(child);
22
+ } else prose.push(child);
23
+ }
24
+ flush();
25
+ return result;
26
+ });
27
+ }
28
+ visit(tree);
29
+ };
30
+ }
@@ -94,10 +94,6 @@ function rewriteChildren(node, nextScope) {
94
94
  if (child?.type === "code") {
95
95
  const record = fenceNodeToRecord(child);
96
96
  const kind = classifyFence({ ...record, body: child.value, line: 1, endLine: 1 });
97
- if (kind.kind === "legacy") {
98
- next.push(gardenElement({ legacy: true, from: kind.lang || record.lang, suggestion: kind.suggestion }));
99
- continue;
100
- }
101
97
  if (kind.kind === "garden" && kind.block?.name === "compose" && !kind.block.error && kind.block.mode !== "play") {
102
98
  next.push(htmlElement(renderCompose(kind.block.props, { scope: nextScope() })));
103
99
  continue;
@@ -9,8 +9,8 @@ import converter from "../../ui/prompt/recipes/converter.md?raw";
9
9
  import plan from "../../ui/prompt/recipes/plan.md?raw";
10
10
  import dataChart from "../../ui/prompt/recipes/data-chart.md?raw";
11
11
  import codeWalkthrough from "../../ui/prompt/recipes/code-walkthrough.md?raw";
12
- import { findBlocks, intentHash, setCompiled, splitUiBody } from "../../ui/core/fence.mjs";
13
- import { checkUiBlock } from "../../ui/core/validate.mjs";
12
+ import { findBlocks } from "../../ui/core/fence.mjs";
13
+ import { checkComposeFence } from "../../ui/core/scan.mjs";
14
14
  import { buildPrompt, RECIPE_FILES } from "../../ui/prompt/build.mjs";
15
15
 
16
16
  const recipeText: Record<string, string> = {
@@ -91,7 +91,7 @@ async function complete(messages: Message[]) {
91
91
  }
92
92
 
93
93
  function extract(content: string) {
94
- const fence = findBlocks(content).find((block) => block.lang === "ui" && block.closed);
94
+ const fence = findBlocks(content).find((block) => block.lang === "garden-compose" && block.closed);
95
95
  const before = fence ? content.slice(0, fence.start) : content;
96
96
  const note = /^提醒[::]\s*(.+)$/mu.exec(before)?.[1]?.trim() ?? null;
97
97
  return { fence, note };
@@ -122,14 +122,13 @@ export async function draftUiBlock(input: { mode: "create" | "revise"; instructi
122
122
  const { fence, note } = extract(reply.content);
123
123
  if (!fence) {
124
124
  if (round === 2) break;
125
- messages.push({ role: "assistant", content: reply.content }, { role: "user", content: "回复里没有 ui 围栏。请只回复一个完整的 ui 围栏。" });
125
+ messages.push({ role: "assistant", content: reply.content }, { role: "user", content: "回复里没有 garden compose 围栏。请只回复一个完整的 garden compose 围栏。" });
126
126
  continue;
127
127
  }
128
128
  // lineOffset 1: in the workbench editor the fence starts on line 1.
129
- const check = checkUiBlock(fence.body, { lineOffset: 1, skipData: true });
129
+ const check = checkComposeFence(fence, {skipData:true});
130
130
  const errors = (check.diagnostics as DraftDiagnostic[]).filter((item) => item.severity === "error");
131
- const { header } = splitUiBody(fence.body);
132
- const stamped = typeof header?.intent === "string" ? setCompiled(fence.raw, intentHash(header.intent)) : fence.raw;
131
+ const stamped = fence.raw;
133
132
  if (!errors.length || round === 2) {
134
133
  return { fence: stamped, note, diagnostics: check.diagnostics as DraftDiagnostic[], rounds: round, usage };
135
134
  }
@@ -137,9 +136,9 @@ export async function draftUiBlock(input: { mode: "create" | "revise"; instructi
137
136
  { role: "assistant", content: reply.content },
138
137
  {
139
138
  role: "user",
140
- content: `程序校验发现这些问题:\n${errors.map((item) => `- 第 ${item.line} 行 ${item.code}:${item.message}`).join("\n")}\n请修正后重新给出完整的 ui 围栏,格式不变。`,
139
+ content: `程序校验发现这些问题:\n${errors.map((item) => `- 第 ${item.line} 行 ${item.code}:${item.message}`).join("\n")}\n请修正后重新给出完整的 garden compose 围栏,格式不变。`,
141
140
  },
142
141
  );
143
142
  }
144
- return { fence: null, note: null, diagnostics: [], rounds: 2, usage, error: "模型没有给出 ui 围栏", raw: lastContent };
143
+ return { fence: null, note: null, diagnostics: [], rounds: 2, usage, error: "模型没有给出 garden compose 围栏", raw: lastContent };
145
144
  }
package/app/styles/a2.css CHANGED
@@ -367,6 +367,18 @@ html[data-theme="dark"] .column-reading-page .e-content {
367
367
  margin-top: 28px;
368
368
  }
369
369
 
370
+ /* H4 sits below H3: same sans family, smaller size, still bold. */
371
+ .markdown-body h4 {
372
+ font: 600 var(--fs-body) / 1.55 var(--sans);
373
+ color: var(--ink);
374
+ letter-spacing: 0;
375
+ margin: 28px 0 10px;
376
+ }
377
+
378
+ .markdown-body h3 + h4 {
379
+ margin-top: 18px;
380
+ }
381
+
370
382
  .sec-no {
371
383
  color: var(--blue);
372
384
  font-variant-numeric: tabular-nums;
@@ -377,6 +389,10 @@ html[data-theme="dark"] .column-reading-page .e-content {
377
389
  margin: 0 0 22px;
378
390
  }
379
391
 
392
+ .markdown-body > :is(ul, ol) + p {
393
+ margin-top: 22px;
394
+ }
395
+
380
396
  .markdown-body strong {
381
397
  font-weight: 600;
382
398
  color: var(--ink);
@@ -1631,7 +1647,7 @@ html[data-theme="dark"] .g-quiz-out.no { color: #f29aab; }
1631
1647
  .g-range { grid-template-columns: 84px minmax(0, 1fr) 54px; gap: 8px; }
1632
1648
  }
1633
1649
 
1634
- /* ---- garden file: a download card, a group of rows, a preview drawer ---- */
1650
+ /* ---- Compose files: a download card, a group of rows, a preview drawer ---- */
1635
1651
  .gf { margin: 24px 0; border: 1px solid var(--rule); border-radius: 12px; background: transparent; font-family: var(--sans); }
1636
1652
  .gf-title { padding: 14px 16px 0; font-size: 14px; font-weight: 600; line-height: 1.4; color: var(--muted); }
1637
1653
  .gf-row { display: flex; align-items: center; gap: 14px; padding: 14px 16px; }
package/bin/garden.mjs CHANGED
@@ -2,6 +2,8 @@
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { startDev } from "../scripts/dev-server.mjs";
5
+ import { installSiteGuide } from "../scripts/site-guide.mjs";
6
+ import { installManagedSkills } from "../scripts/managed-skills.mjs";
5
7
  import fs from "node:fs";
6
8
  import path from "node:path";
7
9
  import { createRequire } from "node:module";
@@ -202,6 +204,7 @@ async function ensureSiteKit(destinationRoot = siteRoot) {
202
204
  const lang = siteCliLang(destinationRoot);
203
205
  if (!samePath(destinationRoot, gardenPackageRoot)) {
204
206
  const added = [];
207
+ if (installSiteGuide({ engineRoot: gardenPackageRoot, siteRoot: destinationRoot, lang })) added.push("AGENTS.md");
205
208
  if (
206
209
  copyIfMissing(
207
210
  path.join(gardenPackageRoot, "templates", "site", lang === "en" ? "Makefile.en" : "Makefile"),
@@ -283,11 +286,17 @@ async function prepare() {
283
286
  await ensureSiteKit();
284
287
  }
285
288
 
286
- async function sync() {
289
+ async function sync({ refreshLinks = false } = {}) {
287
290
  if (!fs.existsSync(path.join(siteRoot, "site.config.yaml"))) {
288
291
  fail(`当前目录不是 Garden 站点(缺少 site.config.yaml):${siteRoot}`);
289
292
  }
290
293
  await prepare();
294
+ // Same path as `garden links` / formal build: collect Markdown URLs (incl. drafts),
295
+ // refresh link-assets.json + public assets, then regenerate. Failures keep usable cache.
296
+ if (refreshLinks && process.env.GARDEN_OFFLINE !== "1") {
297
+ const links = spawnSync(process.execPath, [path.join(gardenPackageRoot, "scripts/link-assets.mjs"), "--dev"], { cwd: siteRoot, env: siteEnv(), stdio: "inherit" });
298
+ if (links.status !== 0) console.warn("链接资源:刷新失败,沿用可用缓存");
299
+ }
291
300
  runNode("scripts/build-content.mjs");
292
301
  }
293
302
 
@@ -320,7 +329,7 @@ function ensureDevGitHubLogin() {
320
329
  async function runVinext(vinextArgs, { copyDist = false } = {}) {
321
330
  const cwd = vinextRoot();
322
331
  if (vinextArgs[0] === "dev") {
323
- const { finished } = await startDev({ cwd, executable: resolveVinext(), args: vinextArgs.slice(1), env: siteEnv(), prepare: sync });
332
+ const { finished } = await startDev({ cwd, executable: resolveVinext(), args: vinextArgs.slice(1), env: siteEnv(), prepare: () => sync({ refreshLinks: true }) });
324
333
  const result = await finished;
325
334
  if (result.status !== 0) process.exit(result.status || 1);
326
335
  return;
@@ -341,7 +350,7 @@ const EN_TEMPLATE_VARIANTS = new Set(["site.config.en.yaml", "Makefile.en", "REA
341
350
  function copyTemplate(from, to) {
342
351
  fs.mkdirSync(to, { recursive: true });
343
352
  for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
344
- if (entry.name === ".git" || EN_TEMPLATE_VARIANTS.has(entry.name)) continue;
353
+ if (entry.name === ".git" || entry.name === "AGENTS.md" || entry.name === "AGENTS.en.md" || EN_TEMPLATE_VARIANTS.has(entry.name)) continue;
345
354
  const source = path.join(from, entry.name);
346
355
  const destination = path.join(to, entry.name);
347
356
  if (entry.isDirectory()) copyTemplate(source, destination);
@@ -414,6 +423,7 @@ async function initSite() {
414
423
  fs.copyFileSync(enWorkflow, workflowDest);
415
424
  }
416
425
  }
426
+ installSiteGuide({ engineRoot: gardenPackageRoot, siteRoot: destination, lang });
417
427
  writeEngineDependency(destination);
418
428
  process.stdout.write(lang === "en" ? `Created site: ${destination}\n` : `已创建站点:${destination}\n`);
419
429
  installSkill(destination, { lang });
@@ -466,34 +476,10 @@ async function editorsCommand(argv) {
466
476
  if (status) process.exit(status);
467
477
  }
468
478
 
469
- function packagedSkills() {
470
- const root = path.join(gardenPackageRoot, ".agents", "skills");
471
- if (!fs.existsSync(root)) return [];
472
- return fs.readdirSync(root, { withFileTypes: true })
473
- .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(root, entry.name, "SKILL.md")))
474
- .map((entry) => entry.name);
475
- }
476
-
477
- function installSkill(destinationRoot = siteRoot, { onlyMissing = false, lang = siteCliLang(destinationRoot) } = {}) {
478
- const names = packagedSkills();
479
- if (!names.length) fail("Garden 包里没有 skill。");
480
- const installed = [];
481
- for (const name of names) {
482
- const to = path.join(destinationRoot, ".agents", "skills", name);
483
- if (onlyMissing && fs.existsSync(path.join(to, "SKILL.md"))) continue;
484
- fs.cpSync(path.join(gardenPackageRoot, ".agents", "skills", name), to, { recursive: true });
485
- // English sites get the English skill text; files without a translation keep the packaged copy.
486
- const english = path.join(gardenPackageRoot, "templates", "skills-en", name);
487
- if (lang === "en" && fs.existsSync(english)) fs.cpSync(english, to, { recursive: true, force: true });
488
- installed.push(name);
489
- }
490
- if (installed.length) {
491
- process.stdout.write(
492
- lang === "en"
493
- ? `Installed skills: ${installed.join(", ")}\n`
494
- : `已安装 skill:${installed.join("、")}\n`,
495
- );
496
- }
479
+ function installSkill(destinationRoot = siteRoot, { lang = siteCliLang(destinationRoot) } = {}) {
480
+ // Managed kits update recursively even on automatic sync; local edits are backed up.
481
+ const installed = installManagedSkills({ engineRoot: gardenPackageRoot, siteRoot: destinationRoot, lang });
482
+ process.stdout.write(lang === "en" ? `Installed skills: ${installed.join(", ")}\n` : `已安装 skill:${installed.join("、")}\n`);
497
483
  }
498
484
 
499
485
  function help() {
@@ -504,14 +490,16 @@ function help() {
504
490
  garden sync 只从 Markdown 生成索引
505
491
  garden new 创建文章草稿
506
492
  garden embed 拉取外链摘要
507
- garden links 重新拉取外链图标(build 时会自动做)
508
- garden images 读取新的远程图片尺寸,写入 image-sizes.json(build 时会自动做)
509
- garden files 读取 garden file 里远程文件的大小,写入 file-sizes.json(build 时会自动做)
493
+ garden links 重新拉取外链图标与卡片元数据(dev / build 时也会自动做)
494
+ garden images 读取新的远程图片尺寸,写入 .garden/image-sizes.json(build 时会自动做)
495
+ garden files 读取 Compose <file> 里远程文件的大小,写入 file-sizes.json(build 时会自动做)
510
496
  garden init [目录] 创建只含稿和配置的新站点
511
497
  garden check <文章.md> 检查一篇博文的硬性结构
498
+ garden verify [文章.md|--diff] 全量/单篇/改动文档严格预检(包含草稿;make check 使用)
512
499
  garden ui <子命令> Intelligent UI:scan、check、apply、prompt、schema、catalog
513
500
  garden hooks 安装提交前检查(检查暂存稿件里的 ui 块)
514
501
  garden editors install 安装 Intelligent UI 插件到 VS Code / Cursor 与 Typora(--vscode、--typora、--yes)
502
+ garden agents 仅在缺失时安装站点 AGENTS.md,保留已有指南
515
503
  garden skill 把引擎自带的 skill 装进当前站点
516
504
  garden pages:validate 检查 dist/client
517
505
  garden pages:prepare 写入公开产物仓库
@@ -542,7 +530,7 @@ async function main() {
542
530
  if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/link-assets.mjs");
543
531
  // Remote image sizes: only new images are probed; the rest come from image-sizes.json.
544
532
  if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/image-sizes.mjs");
545
- // Sizes of remote files in garden file blocks; failures fall back to file-sizes.json.
533
+ // Sizes of remote files in Compose file components; failures fall back to file-sizes.json.
546
534
  if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/file-sizes.mjs");
547
535
  await runVinext(["build", ...rest], { copyDist: true });
548
536
  // /favicon.ico from the site's own icon (brand.favicon).
@@ -576,6 +564,9 @@ async function main() {
576
564
  case "embed":
577
565
  runNode("scripts/fetch-embed.mjs", rest);
578
566
  break;
567
+ case "verify":
568
+ runNode("scripts/check-site.mjs", rest);
569
+ break;
579
570
  case "check":
580
571
  runNode("scripts/check-post.mjs", rest);
581
572
  break;
@@ -591,6 +582,14 @@ async function main() {
591
582
  case "init":
592
583
  await initSite();
593
584
  break;
585
+ case "agents": {
586
+ if (samePath(siteRoot, gardenPackageRoot)) fail("This command is for a Garden site, not the engine repository.");
587
+ if (!fs.existsSync(path.join(siteRoot, "site.config.yaml"))) fail("Not a Garden site: missing site.config.yaml");
588
+ const lang = siteCliLang(siteRoot);
589
+ const created = installSiteGuide({ engineRoot: gardenPackageRoot, siteRoot, lang });
590
+ process.stdout.write(lang === "en" ? (created ? "Created AGENTS.md\n" : "Kept existing AGENTS.md\n") : (created ? "已创建 AGENTS.md\n" : "保留已有 AGENTS.md\n"));
591
+ break;
592
+ }
594
593
  case "skill":
595
594
  case "skills":
596
595
  installSkill();