hd-garden 0.8.0 → 0.8.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 (187) hide show
  1. package/.agents/skills/garden-ui/SKILL.md +2 -2
  2. package/.env.en.example +67 -0
  3. package/README.en.md +2 -2
  4. package/README.md +2 -2
  5. package/app/a2/[slug]/page.tsx +4 -3
  6. package/app/a2/page.tsx +4 -3
  7. package/app/about/page.tsx +8 -9
  8. package/app/admin/page.tsx +7 -6
  9. package/app/admin/preview/[...path]/page.tsx +2 -1
  10. package/app/admin/ui/blocks/page.tsx +2 -1
  11. package/app/admin/ui/catalog/page.tsx +2 -1
  12. package/app/admin/ui/evals/page.tsx +2 -1
  13. package/app/admin/ui/page.tsx +2 -1
  14. package/app/blog/[...path]/page.tsx +7 -6
  15. package/app/blog/page.tsx +4 -2
  16. package/app/columns/[column]/[...path]/page.tsx +8 -7
  17. package/app/columns/page.tsx +12 -8
  18. package/app/components/AdminUiShell.tsx +4 -3
  19. package/app/components/ArticleMeasure.tsx +6 -3
  20. package/app/components/ArticleReferences.tsx +4 -1
  21. package/app/components/ArticleTags.tsx +3 -2
  22. package/app/components/BlogArchive.tsx +20 -16
  23. package/app/components/Comments.tsx +16 -10
  24. package/app/components/ContentEmptyState.tsx +29 -28
  25. package/app/components/ExternalEmbed.tsx +15 -14
  26. package/app/components/GardenContentGuide.tsx +10 -2
  27. package/app/components/GardenDocsNav.tsx +4 -2
  28. package/app/components/GiscusComments.tsx +2 -1
  29. package/app/components/GithubLiveOverview.tsx +10 -8
  30. package/app/components/GithubProfile.tsx +31 -21
  31. package/app/components/HandbookLangSwitch.tsx +8 -4
  32. package/app/components/HandbookView.tsx +92 -9
  33. package/app/components/MarkdownArticle.tsx +31 -7
  34. package/app/components/MermaidDiagram.tsx +4 -3
  35. package/app/components/SiteChrome.tsx +3 -3
  36. package/app/components/SiteFooter.tsx +1 -1
  37. package/app/components/SiteHeader.tsx +13 -4
  38. package/app/components/ThoughtCard.tsx +16 -14
  39. package/app/components/UiBlock.tsx +3 -2
  40. package/app/components/Webmentions.tsx +11 -11
  41. package/app/components/WidgetChart.tsx +8 -6
  42. package/app/components/garden/FilePreview.tsx +2 -2
  43. package/app/components/garden/GardenBlock.tsx +7 -0
  44. package/app/components/garden/GardenFile.tsx +2 -2
  45. package/app/components/garden/RefDemo.tsx +62 -0
  46. package/app/components/garden/blocks.tsx +56 -8
  47. package/app/components/garden/patterns.tsx +497 -0
  48. package/app/garden/[...slug]/page.tsx +3 -2
  49. package/app/garden/en/page.tsx +7 -7
  50. package/app/garden/page.tsx +3 -2
  51. package/app/globals.css +49 -1
  52. package/app/lib/a2-meta.ts +26 -3
  53. package/app/lib/content.ts +2 -2
  54. package/app/lib/garden-docs-meta.ts +24 -0
  55. package/app/lib/github-id.mjs +8 -0
  56. package/app/lib/handbook-switch.mjs +12 -0
  57. package/app/lib/i18n.ts +401 -2
  58. package/app/lib/rehype-field-tables.mjs +47 -0
  59. package/app/lib/remark-a2.mjs +18 -8
  60. package/app/lib/remark-link-card.mjs +22 -0
  61. package/app/lib/remark-ref-demos.mjs +42 -0
  62. package/app/lib/tag-filter.ts +9 -2
  63. package/app/not-found.tsx +32 -4
  64. package/app/page.tsx +17 -14
  65. package/app/rss.xml/route.ts +2 -1
  66. package/app/styles/a2.css +338 -0
  67. package/app/styles/shell.css +15 -2
  68. package/app/tags/page.tsx +7 -5
  69. package/app/thoughts/[slug]/page.tsx +2 -1
  70. package/app/thoughts/page/[n]/page.tsx +1 -1
  71. package/app/thoughts/page.tsx +2 -1
  72. package/app/ui-parity/page.tsx +38 -22
  73. package/bin/garden.mjs +129 -19
  74. package/docs/handbook/ai-writing.md +2 -2
  75. package/docs/handbook/assets/reading-layout-en.svg +26 -0
  76. package/docs/handbook/components/card.md +43 -22
  77. package/docs/handbook/components/chart.md +10 -26
  78. package/docs/handbook/components/choice.md +42 -0
  79. package/docs/handbook/components/compare.md +32 -17
  80. package/docs/handbook/components/figure.md +33 -46
  81. package/docs/handbook/components/file.md +10 -15
  82. package/docs/handbook/components/html.md +10 -30
  83. package/docs/handbook/components/interactive.md +9 -35
  84. package/docs/handbook/components/list.md +65 -0
  85. package/docs/handbook/components/relations.md +95 -0
  86. package/docs/handbook/components/stat.md +10 -28
  87. package/docs/handbook/components/timeline.md +13 -28
  88. package/docs/handbook/components.md +210 -19
  89. package/docs/handbook/config.md +5 -3
  90. package/docs/handbook/deploy.md +1 -1
  91. package/docs/handbook/design.md +20 -12
  92. package/docs/handbook/en/ai-writing.md +2 -2
  93. package/docs/handbook/en/components/card.md +42 -21
  94. package/docs/handbook/en/components/chart.md +10 -26
  95. package/docs/handbook/en/components/choice.md +42 -0
  96. package/docs/handbook/en/components/compare.md +30 -15
  97. package/docs/handbook/en/components/figure.md +29 -42
  98. package/docs/handbook/en/components/file.md +9 -14
  99. package/docs/handbook/en/components/html.md +9 -29
  100. package/docs/handbook/en/components/interactive.md +8 -34
  101. package/docs/handbook/en/components/list.md +65 -0
  102. package/docs/handbook/en/components/relations.md +95 -0
  103. package/docs/handbook/en/components/stat.md +9 -27
  104. package/docs/handbook/en/components/timeline.md +9 -24
  105. package/docs/handbook/en/components.md +211 -19
  106. package/docs/handbook/en/config.md +32 -30
  107. package/docs/handbook/en/deploy.md +12 -12
  108. package/docs/handbook/en/design.md +19 -12
  109. package/docs/handbook/en/index.md +2 -2
  110. package/docs/handbook/en/reference/card.md +85 -0
  111. package/docs/handbook/en/reference/chart.md +68 -0
  112. package/docs/handbook/en/reference/choice.md +69 -0
  113. package/docs/handbook/en/reference/compare.md +59 -0
  114. package/docs/handbook/en/reference/figure.md +84 -0
  115. package/docs/handbook/en/reference/file/checklist.md +17 -0
  116. package/docs/handbook/en/reference/file.md +50 -0
  117. package/docs/handbook/en/reference/html.md +62 -0
  118. package/docs/handbook/en/reference/interactive.md +71 -0
  119. package/docs/handbook/en/reference/list.md +72 -0
  120. package/docs/handbook/en/reference/relations.md +113 -0
  121. package/docs/handbook/en/reference/stat.md +60 -0
  122. package/docs/handbook/en/reference/timeline.md +56 -0
  123. package/docs/handbook/en/reference.md +26 -0
  124. package/docs/handbook/en/start.md +2 -2
  125. package/docs/handbook/en/workflow.md +6 -5
  126. package/docs/handbook/en/writing.md +3 -2
  127. package/docs/handbook/index.md +1 -1
  128. package/docs/handbook/reference/card.md +85 -0
  129. package/docs/handbook/reference/chart.md +68 -0
  130. package/docs/handbook/reference/choice.md +69 -0
  131. package/docs/handbook/reference/compare.md +59 -0
  132. package/docs/handbook/reference/figure.md +84 -0
  133. package/docs/handbook/reference/file/checklist.md +17 -0
  134. package/docs/handbook/reference/file.md +50 -0
  135. package/docs/handbook/reference/html.md +62 -0
  136. package/docs/handbook/reference/interactive.md +71 -0
  137. package/docs/handbook/reference/list.md +72 -0
  138. package/docs/handbook/reference/relations.md +113 -0
  139. package/docs/handbook/reference/stat.md +60 -0
  140. package/docs/handbook/reference/timeline.md +56 -0
  141. package/docs/handbook/reference.md +26 -0
  142. package/docs/handbook/start.md +1 -1
  143. package/docs/handbook/workflow.md +6 -5
  144. package/docs/handbook/writing.md +3 -2
  145. package/next.config.ts +6 -0
  146. package/package.json +2 -1
  147. package/public/images/garden/ref-card-book.svg +1 -0
  148. package/public/images/garden/ref-card-lamp.svg +1 -0
  149. package/scripts/build-content.mjs +37 -16
  150. package/scripts/favicon-ico.mjs +9 -6
  151. package/scripts/file-preview.mjs +10 -7
  152. package/scripts/handbook-allowlist.mjs +18 -0
  153. package/scripts/handbook-facts.mjs +305 -11
  154. package/scripts/link-assets.mjs +7 -0
  155. package/scripts/new-post.mjs +18 -9
  156. package/scripts/prepare-pages-output.mjs +30 -18
  157. package/scripts/redirects.mjs +17 -6
  158. package/templates/garden-blocks.md +67 -0
  159. package/templates/garden-snippets.plist +61 -0
  160. package/templates/site/Makefile.en +45 -0
  161. package/templates/site/README.en.md +15 -0
  162. package/templates/site/site.config.en.yaml +55 -0
  163. package/templates/skills-en/garden-interactive-chart/SKILL.md +84 -0
  164. package/templates/skills-en/garden-interactive-chart/references/shell.html +49 -0
  165. package/templates/skills-en/garden-interactive-chart/references/style.md +61 -0
  166. package/templates/skills-en/garden-post-review/SKILL.md +63 -0
  167. package/templates/skills-en/garden-ui/SKILL.md +118 -0
  168. package/templates/workflows/pages.en.yml +195 -0
  169. package/templates/workflows/pages.yml +195 -0
  170. package/ui/garden/catalog.mjs +205 -4
  171. package/ui/garden/check.mjs +6 -0
  172. package/ui/garden/file-block.mjs +8 -7
  173. package/ui/garden/html-lang.mjs +73 -1
  174. package/ui/garden/markdown-links.mjs +7 -4
  175. package/ui/garden/parse.mjs +45 -0
  176. package/ui/garden/relations.mjs +252 -0
  177. package/ui/garden/search-index.mjs +6 -4
  178. package/ui/garden/source-icon.mjs +54 -0
  179. package/ui/parity-fixtures.mjs +77 -0
  180. package/ui/preview/dist/garden-ui.esm.js +41 -4
  181. package/ui/preview/dist/garden-ui.iife.js +41 -4
  182. package/ui/prompt/rules.md +28 -1
  183. package/ui/react/components/chart.tsx +1 -1
  184. package/ui/react/components/display.tsx +1 -1
  185. package/ui/react/lang.tsx +4 -0
  186. package/ui/react/parity.tsx +8 -6
  187. package/worker/index.ts +8 -2
package/bin/garden.mjs CHANGED
@@ -5,6 +5,7 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { createRequire } from "node:module";
7
7
  import { gardenPackageRoot, gardenSiteRoot } from "../scripts/garden-paths.mjs";
8
+ import { langOf, siteCliLang } from "../ui/core/cli-locale.mjs";
8
9
 
9
10
  const siteRoot = gardenSiteRoot();
10
11
  const args = process.argv.slice(2);
@@ -90,7 +91,17 @@ function ensurePublic() {
90
91
  }
91
92
  }
92
93
 
93
- function printGiscusGuide() {
94
+ function printGiscusGuide(lang = siteCliLang(siteRoot)) {
95
+ if (lang === "en") {
96
+ process.stdout.write(`Comments are disabled by default. Configure Giscus before publishing, or GitHub Actions builds will fail:
97
+ 1. Prepare a public GitHub repository and enable Discussions. A public source repo works directly; a private source repo needs a separate public repo.
98
+ 2. Open https://giscus.app, install the Giscus App for that repository, and pick a category.
99
+ 3. Set GISCUS_REPO, GISCUS_REPO_ID, GISCUS_CATEGORY, and GISCUS_CATEGORY_ID in .env.local.
100
+ 4. Add the same four variables under Settings -> Secrets and variables -> Actions -> Variables in your source repository.
101
+ Placeholder values do not count as configured. To publish without comments, set the Actions Variable GISCUS_DISABLED to 1.
102
+ `);
103
+ return;
104
+ }
94
105
  process.stdout.write(`评论默认关闭。发布前要接上自己的 Giscus,留空会让 GitHub Actions 构建失败:
95
106
  1. 准备一个公开 GitHub 仓库,并开启 Discussions。公开源码仓库可以直接用;私有源码要另建一个公开仓库。
96
107
  2. 打开 https://giscus.app/zh-CN ,安装 Giscus App,只授权这个仓库,并选一个分类。
@@ -104,11 +115,12 @@ function ensureEnv() {
104
115
  // CI injects the real values. Copying .env.example there would fill any
105
116
  // unset key with a sample, which used to turn comments on for owner/repository.
106
117
  if (process.env.GITHUB_ACTIONS === "true") return;
107
- const example = path.join(gardenPackageRoot, ".env.example");
118
+ const lang = siteCliLang(siteRoot);
119
+ const example = path.join(gardenPackageRoot, lang === "en" ? ".env.en.example" : ".env.example");
108
120
  const local = path.join(siteRoot, ".env.local");
109
121
  if (!fs.existsSync(local) && fs.existsSync(example)) {
110
122
  fs.copyFileSync(example, local);
111
- printGiscusGuide();
123
+ printGiscusGuide(lang);
112
124
  }
113
125
  }
114
126
 
@@ -156,6 +168,16 @@ function vinextRoot() {
156
168
  }
157
169
  }
158
170
 
171
+ // English sites: the runtime manifest is a copy with the English description, not a link.
172
+ // Remove the link first so the write never goes through it into the engine's own package.json.
173
+ if (siteCliLang(siteRoot) === "en") {
174
+ const target = path.join(runtime, "package.json");
175
+ const manifest = JSON.parse(fs.readFileSync(path.join(gardenPackageRoot, "package.json"), "utf8"));
176
+ manifest.description = "A personal blog engine: write in Markdown, publish with GitHub Pages.";
177
+ fs.rmSync(target, { force: true });
178
+ fs.writeFileSync(target, `${JSON.stringify(manifest, null, 2)}\n`);
179
+ }
180
+
159
181
  return runtime;
160
182
  }
161
183
 
@@ -176,22 +198,27 @@ function copyIfMissing(from, to) {
176
198
 
177
199
  function ensureSiteKit(destinationRoot = siteRoot) {
178
200
  if (samePath(destinationRoot, gardenPackageRoot)) return;
201
+ const lang = siteCliLang(destinationRoot);
179
202
 
180
203
  const added = [];
181
204
  if (
182
205
  copyIfMissing(
183
- path.join(gardenPackageRoot, "templates", "site", "Makefile"),
206
+ path.join(gardenPackageRoot, "templates", "site", lang === "en" ? "Makefile.en" : "Makefile"),
184
207
  path.join(destinationRoot, "Makefile"),
185
208
  )
186
209
  ) {
187
210
  added.push("Makefile");
188
211
  }
189
212
  if (added.length) {
190
- process.stdout.write(`已补齐站点文件:${added.join("、")}\n`);
213
+ process.stdout.write(
214
+ lang === "en"
215
+ ? `Added missing site files: ${added.join(", ")}\n`
216
+ : `已补齐站点文件:${added.join("、")}\n`,
217
+ );
191
218
  }
192
219
 
193
- installSkill(destinationRoot, { onlyMissing: true });
194
- if (process.env.GARDEN_SKIP_HOOKS !== "1") installHook(destinationRoot, { quiet: true });
220
+ installSkill(destinationRoot, { onlyMissing: true, lang });
221
+ if (process.env.GARDEN_SKIP_HOOKS !== "1") installHook(destinationRoot, { quiet: true, lang });
195
222
  }
196
223
 
197
224
  const HOOK_MARKER = "# garden: ui check";
@@ -203,30 +230,46 @@ if [ ! -d node_modules/hd-garden ]; then
203
230
  fi
204
231
  exec npx --no-install garden ui check --staged --quiet
205
232
  `;
233
+ const HOOK_SCRIPT_EN = `#!/bin/sh
234
+ ${HOOK_MARKER}
235
+ if [ ! -d node_modules/hd-garden ]; then
236
+ echo "garden is not installed yet (run npm install first); skipping ui check." >&2
237
+ exit 0
238
+ fi
239
+ exec npx --no-install garden ui check --staged --quiet
240
+ `;
206
241
 
207
242
  /** Installs the pre-commit hook unless the repository already has one. */
208
- function installHook(destinationRoot = siteRoot, { quiet = false, force = false } = {}) {
243
+ function installHook(destinationRoot = siteRoot, { quiet = false, force = false, lang = siteCliLang(destinationRoot) } = {}) {
209
244
  const result = spawnSync("git", ["rev-parse", "--git-path", "hooks/pre-commit"], { cwd: destinationRoot, encoding: "utf8" });
210
245
  if (result.status !== 0) {
211
- if (!quiet) process.stdout.write("这里不是 git 仓库,没有安装提交前检查。\n");
246
+ if (!quiet) process.stdout.write(lang === "en" ? "Not a git repository; skipping pre-commit hook installation.\n" : "这里不是 git 仓库,没有安装提交前检查。\n");
212
247
  return;
213
248
  }
214
249
  const hookPath = path.resolve(destinationRoot, result.stdout.trim());
215
250
  if (fs.existsSync(hookPath)) {
216
251
  const current = fs.readFileSync(hookPath, "utf8");
217
252
  if (current.includes(HOOK_MARKER) && !force) {
218
- if (!quiet) process.stdout.write("提交前检查已经装好了。\n");
253
+ if (!quiet) process.stdout.write(lang === "en" ? "Pre-commit hook is already installed.\n" : "提交前检查已经装好了。\n");
219
254
  return;
220
255
  }
221
256
  if (!current.includes(HOOK_MARKER)) {
222
- process.stdout.write(`已有 pre-commit 钩子,没有覆盖。请把这一行加进 ${path.relative(destinationRoot, hookPath)}:\n npx --no-install garden ui check --staged --quiet\n`);
257
+ process.stdout.write(
258
+ lang === "en"
259
+ ? `Existing pre-commit hook kept. Add this line to ${path.relative(destinationRoot, hookPath)}:\n npx --no-install garden ui check --staged --quiet\n`
260
+ : `已有 pre-commit 钩子,没有覆盖。请把这一行加进 ${path.relative(destinationRoot, hookPath)}:\n npx --no-install garden ui check --staged --quiet\n`,
261
+ );
223
262
  return;
224
263
  }
225
264
  }
226
265
  fs.mkdirSync(path.dirname(hookPath), { recursive: true });
227
- fs.writeFileSync(hookPath, HOOK_SCRIPT, { mode: 0o755 });
266
+ fs.writeFileSync(hookPath, lang === "en" ? HOOK_SCRIPT_EN : HOOK_SCRIPT, { mode: 0o755 });
228
267
  fs.chmodSync(hookPath, 0o755);
229
- process.stdout.write("已安装提交前检查:提交时会检查暂存稿件里的 ui 块。\n");
268
+ process.stdout.write(
269
+ lang === "en"
270
+ ? "Installed pre-commit hook: staged posts will be checked for valid ui blocks before commit.\n"
271
+ : "已安装提交前检查:提交时会检查暂存稿件里的 ui 块。\n",
272
+ );
230
273
  }
231
274
 
232
275
  function prepare() {
@@ -283,10 +326,12 @@ function runVinext(vinextArgs, { copyDist = false } = {}) {
283
326
  if (result.status !== 0) process.exit(result.status || 1);
284
327
  }
285
328
 
329
+ const EN_TEMPLATE_VARIANTS = new Set(["site.config.en.yaml", "Makefile.en", "README.en.md"]);
330
+
286
331
  function copyTemplate(from, to) {
287
332
  fs.mkdirSync(to, { recursive: true });
288
333
  for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
289
- if (entry.name === ".git") continue;
334
+ if (entry.name === ".git" || EN_TEMPLATE_VARIANTS.has(entry.name)) continue;
290
335
  const source = path.join(from, entry.name);
291
336
  const destination = path.join(to, entry.name);
292
337
  if (entry.isDirectory()) copyTemplate(source, destination);
@@ -294,6 +339,34 @@ function copyTemplate(from, to) {
294
339
  }
295
340
  }
296
341
 
342
+ function parseInitArgs(argv) {
343
+ let locale = process.env.GARDEN_LOCALE ?? "";
344
+ const positional = [];
345
+ const hasLocaleFlag = argv.includes("--locale");
346
+ for (let i = 0; i < argv.length; i += 1) {
347
+ const arg = argv[i];
348
+ if (arg === "--locale") {
349
+ locale = argv[i + 1] ?? "";
350
+ i += 1;
351
+ continue;
352
+ }
353
+ if (arg.startsWith("--locale=")) {
354
+ locale = arg.slice("--locale=".length);
355
+ continue;
356
+ }
357
+ if (/^locale\s*[:=]/iu.test(arg)) {
358
+ locale = arg.replace(/^locale\s*[:=]\s*/iu, "");
359
+ continue;
360
+ }
361
+ positional.push(arg);
362
+ }
363
+ void hasLocaleFlag;
364
+ return {
365
+ destination: path.resolve(positional[0] ?? "."),
366
+ lang: langOf(locale),
367
+ };
368
+ }
369
+
297
370
  function writeEngineDependency(destination) {
298
371
  const engine = JSON.parse(fs.readFileSync(path.join(gardenPackageRoot, "package.json"), "utf8"));
299
372
  const sitePkgPath = path.join(destination, "package.json");
@@ -306,14 +379,44 @@ function writeEngineDependency(destination) {
306
379
  }
307
380
 
308
381
  function initSite() {
309
- const destination = path.resolve(rest[0] ?? ".");
382
+ const { destination, lang } = parseInitArgs(rest);
310
383
  const template = path.join(gardenPackageRoot, "templates", "site");
311
384
  if (!fs.existsSync(template)) fail("Garden 包里缺少 templates/site。");
312
385
  if (fs.existsSync(path.join(destination, "site.config.yaml"))) {
313
- fail(`目录已是 Garden 站点:${destination}`);
386
+ fail(lang === "en" ? `Directory is already a Garden site: ${destination}` : `目录已是 Garden 站点:${destination}`);
314
387
  }
315
388
  copyTemplate(template, destination);
389
+ if (lang === "en") {
390
+ for (const [srcName, destName] of [
391
+ ["site.config.en.yaml", "site.config.yaml"],
392
+ ["Makefile.en", "Makefile"],
393
+ ["README.en.md", "README.md"],
394
+ ]) {
395
+ const srcFile = path.join(template, srcName);
396
+ if (fs.existsSync(srcFile)) {
397
+ fs.copyFileSync(srcFile, path.join(destination, destName));
398
+ }
399
+ }
400
+ const enWorkflow = path.join(gardenPackageRoot, "templates", "workflows", "pages.en.yml");
401
+ if (fs.existsSync(enWorkflow)) {
402
+ const workflowDest = path.join(destination, ".github", "workflows", "pages.yml");
403
+ fs.mkdirSync(path.dirname(workflowDest), { recursive: true });
404
+ fs.copyFileSync(enWorkflow, workflowDest);
405
+ }
406
+ }
316
407
  writeEngineDependency(destination);
408
+ if (lang === "en") {
409
+ process.stdout.write(`Created site: ${destination}\n`);
410
+ installSkill(destination, { lang });
411
+ process.stdout.write("Next steps:\n");
412
+ process.stdout.write(` cd ${destination}\n`);
413
+ process.stdout.write(" npm install\n");
414
+ process.stdout.write(" Configure comments in four steps below, then start writing\n");
415
+ process.stdout.write(" make new TITLE=\"Hello, World\" SLUG=hello\n");
416
+ process.stdout.write(" make dev\n");
417
+ printGiscusGuide("en");
418
+ return;
419
+ }
317
420
  process.stdout.write(`已创建站点:${destination}\n`);
318
421
  installSkill(destination);
319
422
  process.stdout.write("下一步:\n");
@@ -322,7 +425,7 @@ function initSite() {
322
425
  process.stdout.write(" 按下面四步接上评论,再写文章\n");
323
426
  process.stdout.write(" make new TITLE=\"你好,世界\" SLUG=hello\n");
324
427
  process.stdout.write(" make dev\n");
325
- printGiscusGuide();
428
+ printGiscusGuide("zh");
326
429
  }
327
430
 
328
431
  function packagedSkills() {
@@ -333,7 +436,7 @@ function packagedSkills() {
333
436
  .map((entry) => entry.name);
334
437
  }
335
438
 
336
- function installSkill(destinationRoot = siteRoot, { onlyMissing = false } = {}) {
439
+ function installSkill(destinationRoot = siteRoot, { onlyMissing = false, lang = siteCliLang(destinationRoot) } = {}) {
337
440
  const names = packagedSkills();
338
441
  if (!names.length) fail("Garden 包里没有 skill。");
339
442
  const installed = [];
@@ -341,10 +444,17 @@ function installSkill(destinationRoot = siteRoot, { onlyMissing = false } = {})
341
444
  const to = path.join(destinationRoot, ".agents", "skills", name);
342
445
  if (onlyMissing && fs.existsSync(path.join(to, "SKILL.md"))) continue;
343
446
  fs.cpSync(path.join(gardenPackageRoot, ".agents", "skills", name), to, { recursive: true });
447
+ // English sites get the English skill text; files without a translation keep the packaged copy.
448
+ const english = path.join(gardenPackageRoot, "templates", "skills-en", name);
449
+ if (lang === "en" && fs.existsSync(english)) fs.cpSync(english, to, { recursive: true, force: true });
344
450
  installed.push(name);
345
451
  }
346
452
  if (installed.length) {
347
- process.stdout.write(`已安装 skill:${installed.join("、")}\n`);
453
+ process.stdout.write(
454
+ lang === "en"
455
+ ? `Installed skills: ${installed.join(", ")}\n`
456
+ : `已安装 skill:${installed.join("、")}\n`,
457
+ );
348
458
  }
349
459
  }
350
460
 
@@ -86,8 +86,8 @@ Agent 按 `garden-ui` skill 会把这个想法块替换成一段紧挨着完整
86
86
 
87
87
  不借助 AI 手写组件时,可以直接使用包内自带的模板与代码片段:
88
88
 
89
- - `make new` 创建的草稿底部自带一段 `<!-- 组件速查 ... -->` 注释,包含想法块和 9 个组件的示例,写完可直接删掉。
90
- - `templates/garden-blocks.md` 汇总了想法块与 9 个组件的适用场景(`when` / `whenNot`)和可复制示例;macOS 与 Typora / Alfred 用户可导入 `templates/garden-snippets.plist`,输入 `;gidea`(或 `;garden想法`)、`;gcard`、`;gstat`、`;gcompare`、`;gtimeline`、`;gchart`、`;gfigure`、`;ghtml`、`;ginteractive`、`;gfile` 即可展开对应围栏。
89
+ - `make new` 创建的草稿底部自带一段 `<!-- 组件速查 ... -->` 注释,包含想法块和 12 个组件的示例,写完可直接删掉。
90
+ - `templates/garden-blocks.md` 汇总了想法块与 12 个组件的适用场景(`when` / `whenNot`)和可复制示例;macOS 与 Typora / Alfred 用户可导入 `templates/garden-snippets.plist`,输入 `;gidea`(或 `;garden想法`)、`;gcard`、`;gstat`、`;gcompare`、`;gtimeline`、`;gchart`、`;gfigure`、`;ghtml`、`;ginteractive`、`;gfile`、`;grel`、`;glist`、`;gchoice` 即可展开对应围栏。
91
91
  - 引擎仓库的 `editors/vscode`、`editors/typora` 与 `editors/obsidian` 目录提供了对应的编辑器片段与插件支持,运行 `npx garden ui catalog` 则可在本地生成 `.garden/ui-catalog.html` 离线组件画廊。
92
92
 
93
93
  ## 7. 可选的独立管理后台
@@ -0,0 +1,26 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="2080" height="560" viewBox="0 0 1040 280" fill="none" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif">
2
+ <!-- Outer 1340px wide-table boundary -->
3
+ <rect x="20" y="20" width="1000" height="240" rx="14" stroke="#6e6b64" stroke-width="1.5" stroke-dasharray="6 5" />
4
+ <text x="44" y="48" fill="#4a4844" font-size="13" font-weight="600">Wide-table zone &#xB7; up to 1340px on article pages (extends up to 300px per side)</text>
5
+ <text x="996" y="48" text-anchor="end" fill="#6e6b64" font-size="12" font-family="ui-monospace, SFMono-Regular, Menlo, monospace">max 1340px</text>
6
+
7
+ <!-- Middle 1040px wide-figure boundary -->
8
+ <rect x="110" y="66" width="820" height="174" rx="12" stroke="#2d2c29" stroke-width="1.5" stroke-dasharray="4 4" />
9
+ <text x="132" y="92" fill="#2d2c29" font-size="13" font-weight="600">Wide-figure zone &#xB7; up to 1040px on article pages (retina screenshots render at half pixel width, expanding past 740px)</text>
10
+ <text x="908" y="92" text-anchor="end" fill="#6e6b64" font-size="12" font-family="ui-monospace, SFMono-Regular, Menlo, monospace">max 1040px</text>
11
+
12
+ <!-- Inner 740px reading column -->
13
+ <rect x="215" y="110" width="610" height="110" rx="10" fill="#f0eee6" fill-opacity="0.72" stroke="#1b1a18" stroke-width="2" />
14
+ <text x="520" y="146" text-anchor="middle" fill="#1b1a18" font-size="16" font-weight="700">Reading column &#xB7; 740px (var(--col))</text>
15
+ <text x="520" y="172" text-anchor="middle" fill="#2d2c29" font-size="13">Paragraphs, lists, blockquotes, code blocks, and formulas stay inside this column</text>
16
+ <text x="520" y="196" text-anchor="middle" fill="#6e6b64" font-size="12" font-family="ui-monospace, SFMono-Regular, Menlo, monospace">Transparent-background figures get var(--image-backing) #faf9f5 warm backing in dark mode</text>
17
+
18
+ <!-- Left & right expansion arrows -->
19
+ <path d="M210 165 H118" stroke="#2d2c29" stroke-width="1.5" />
20
+ <path d="M124 160 L116 165 L124 170" stroke="#2d2c29" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
21
+ <text x="164" y="156" text-anchor="middle" fill="#4a4844" font-size="11" font-family="ui-monospace, SFMono-Regular, Menlo, monospace">&#x2264; 150px</text>
22
+
23
+ <path d="M830 165 H922" stroke="#2d2c29" stroke-width="1.5" />
24
+ <path d="M916 160 L924 165 L916 170" stroke="#2d2c29" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
25
+ <text x="876" y="156" text-anchor="middle" fill="#4a4844" font-size="11" font-family="ui-monospace, SFMono-Regular, Menlo, monospace">&#x2264; 150px</text>
26
+ </svg>
@@ -1,14 +1,12 @@
1
1
  ---
2
2
  title: 知识卡片(card)
3
3
  navTitle: 知识卡片
4
- description: 把论文、项目或模块说明整理成带元数据与编号要点的卡片。
4
+ description: 介绍一篇论文、一个项目或一组值得参考的公开工具。
5
5
  section: components
6
6
  order: 31
7
7
  badge: card
8
8
  ---
9
9
 
10
- 在 `garden` 想法块里写一句「放一张 `card` 介绍 SQLite 的 WAL 文档,带标题、链接和三条要点」,Agent 会按 `garden-ui` skill 生成下面的 `garden card`(手写格式附后)。
11
-
12
10
  ```garden card
13
11
  title: SQLite WAL 写入模式
14
12
  url: https://www.sqlite.org/wal.html
@@ -20,27 +18,50 @@ points:
20
18
  - 后台 Checkpoint 按阈值将日志页分批写回主库文件。
21
19
  ```
22
20
 
21
+ - **什么时候用**:介绍一篇论文、一个仓库或一本书,需要标题、链接和几条说明;或者把几项参考资源放在一起,各附一句可借鉴的结论。
22
+ - **什么时候不用**:正文里一两句话已经说清,或者只需要普通外链卡片时,直接写正文段落。
23
+
24
+ 可以这样对 AI 说:
25
+
23
26
  ````markdown
27
+ ```garden
28
+ 整理一张卡片介绍 SQLite 的 WAL 文档,带链接和三条核心要点。
29
+ ```
30
+ ````
31
+
32
+ ## 多张资源行卡(`cards`)
33
+
34
+ 需要连续列出几个项目、公开工具或参考资料,并在每张卡片下方附上一句启发时,可以用 `cards` 列表:
35
+
24
36
  ```garden card
25
- title: SQLite WAL 写入模式
26
- url: https://www.sqlite.org/wal.html
27
- meta: 文档说明 · sqlite.org
28
- description: 把修改先顺序追加到预写日志,读操作继续访问原数据库页,减少读写互斥等待。
29
- points:
30
- - 写入只追加到 `-wal` 文件末尾,不需要在事务提交时原地覆盖数据页。
31
- - 读事务记录当前末尾偏移量即可看到一致快照,不会阻塞并发写入。
32
- - 后台 Checkpoint 按阈值将日志页分批写回主库文件。
37
+ cards:
38
+ - url: https://openlibrary.org/
39
+ title: Open Library:每本书一个页面
40
+ description: Internet Archive 维护的开放图书目录,可以查版本、借阅电子书。
41
+ image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 132 132'%3E%3Crect width='132' height='132' fill='%23F0EEE6'/%3E%3Crect x='34' y='26' width='64' height='84' rx='6' fill='%23C9B79C'/%3E%3Crect x='34' y='26' width='10' height='84' fill='%238B6A45'/%3E%3Crect x='52' y='44' width='34' height='5' rx='2' fill='%23FAF9F5'/%3E%3Crect x='52' y='54' width='24' height='5' rx='2' fill='%23FAF9F5'/%3E%3C/svg%3E"
42
+ alt: 一本合上的书
43
+ source:
44
+ - https://openlibrary.org/
45
+ takeaway: "**可借鉴:** 选书时直接贴书的页面链接,大家看到的是同一个版本。"
46
+ - url: https://www.gutenberg.org/
47
+ title: Project Gutenberg:公版电子书
48
+ description: 收录已进入公有领域的电子书,可以免费下载。
49
+ image: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 132 132'%3E%3Crect width='132' height='132' fill='%23E8EEF0'/%3E%3Ccircle cx='66' cy='60' r='30' fill='%239DB7A8'/%3E%3Crect x='30' y='98' width='72' height='8' rx='4' fill='%233F6B55'/%3E%3Crect x='44' y='112' width='44' height='6' rx='3' fill='%239DB7A8'/%3E%3C/svg%3E"
50
+ alt: 一盏台灯
51
+ source:
52
+ - https://www.gutenberg.org/
53
+ - https://en.wikipedia.org/wiki/Project_Gutenberg
54
+ takeaway: "**可借鉴:** 偶尔选一本公版书,没买书的人也能马上加入。"
55
+ ```
56
+
57
+ 可以这样对 AI 说:
58
+
59
+ ````markdown
60
+ ```garden
61
+ 把 Open Library 和 Project Gutenberg 做成两张资源卡片,附上来源链接和各自可借鉴的地方。
33
62
  ```
34
63
  ````
35
64
 
36
- - **用**:介绍一篇论文、一个开源项目或某个子系统,需要带出来源链接和 2~4 条关键要点时。
37
- - **别用**:只有一两句普通说明时直接写正文段落或链接卡片(`{card}`),不要套一层 `card`。
38
-
39
- | 字段 | 必填 | 类型 | 说明 |
40
- | :--- | :--- | :--- | :--- |
41
- | `title` | 建议 | 字符串 | 卡片标题 |
42
- | `url` | 否 | 完整链接 | 标题旁显示的外部链接(渲染为 `来源 ↗`) |
43
- | `meta` | 否 | 字符串 | 标题下方的副标题或元信息说明 |
44
- | `description` | 与 `points` 二选一 | 字符串 | 卡片主说明段落(支持行内加粗与代码) |
45
- | `points` | 与 `description` 二选一 | 字符串数组 | 下方带编号的要点列表 |
46
- | `source` | 否 | 字符串或 `{ title, url }` | 右下角出处角标 |
65
+ ## 字段说明
66
+
67
+ 全部字段、默认值、取值限制和报错代码见 [组件参考 · card](/garden/reference/card)。
@@ -1,14 +1,12 @@
1
1
  ---
2
2
  title: 条形图表(chart)
3
3
  navTitle: 条形图表
4
- description: 用 YAML 数据直接生成比例条形图,下方自带可展开的数据表格。
4
+ description: 把一组算好的数字画成比例条形图,下方自带可展开的数据表。
5
5
  section: components
6
6
  order: 35
7
7
  badge: chart
8
8
  ---
9
9
 
10
- 在 `garden` 想法块里写一句「用 `chart` 画条形图比较正文列、宽图区和宽表区的宽度上限」,Agent 会按 `garden-ui` skill 生成下面的 `garden chart`(手写格式附后)。
11
-
12
10
  ```garden chart
13
11
  title: 三种版面区域的宽度上限
14
12
  xLabel: 版面区域
@@ -24,31 +22,17 @@ data:
24
22
  caption: 点击「查看数据表」可展开完整数值表格。
25
23
  ```
26
24
 
25
+ - **什么时候用**:一组已经算好的同单位数字,想让读者一眼看出长短比例。
26
+ - **什么时候不用**:数字会随读者调整参数而变化时用[可调公式与参数](/garden/components/interactive);图表下方已经自带可展开的数据表,同一小节里不需要再重复放一张相同数字的表格。
27
+
28
+ 可以这样对 AI 说:
29
+
27
30
  ````markdown
28
- ```garden chart
29
- title: 三种版面区域的宽度上限
30
- xLabel: 版面区域
31
- yLabel: 最大宽度
32
- unit: px
33
- data:
34
- - x: 普通正文列
35
- y: 740
36
- - x: 宽幅配图区
37
- y: 1040
38
- - x: 多列宽表格
39
- y: 1340
40
- caption: 点击「查看数据表」可展开完整数值表格。
31
+ ```garden
32
+ 画一张条形图比较普通正文列(740px)、宽幅配图区(1040px)和多列宽表格(1340px)的宽度上限。
41
33
  ```
42
34
  ````
43
35
 
44
- - **用**:对比 2~8 组同量纲数值的长短比例时。
45
- - **别用**:已经在同一个小节里放了同组数字的 Markdown 表格时不要再画一张 `chart`(图表底部已自带可展开的数据表)。
36
+ ## 字段说明
46
37
 
47
- | 字段 | 必填 | 类型 | 说明 |
48
- | :--- | :--- | :--- | :--- |
49
- | `title` | 建议 | 字符串 | 图表标题 |
50
- | `data` | 是 | `{ x, y, label? }[]`(至少 2 项) | 数据行列表,`x` 为名称,`y` 为数值 |
51
- | `unit` | 否 | 字符串 | 数值后缀单位(当数据项未单独指定 `label` 时自动拼接) |
52
- | `xLabel` | 否 | 字符串 | 展开数据表第一列的表头名称(默认 `项目`) |
53
- | `yLabel` | 否 | 字符串 | 展开数据表第二列的表头名称(默认 `数值`) |
54
- | `caption` | 否 | 字符串 | 图表底部的图注说明 |
38
+ 全部字段、默认值、取值限制和报错代码见 [组件参考 · chart](/garden/reference/chart)。
@@ -0,0 +1,42 @@
1
+ ---
2
+ title: 选择卡(choice)
3
+ navTitle: 选择卡
4
+ description: 给出两三个具体选项卡片,读者点选后看到对应的回应。
5
+ section: components
6
+ order: 42
7
+ badge: choice
8
+ ---
9
+
10
+ ```garden choice
11
+ title: 下个月在哪里见面?
12
+ items:
13
+ - icon: idea
14
+ value: ¥40 / 人
15
+ text: 街角咖啡馆,含一杯饮料
16
+ label: 更看重氛围
17
+ reply: 选了咖啡馆。我们会提前订好靠窗的长桌。
18
+ - icon: book
19
+ value: 免费
20
+ text: 社区图书馆活动室,需提前预约
21
+ label: 更看重安静
22
+ reply: 选了图书馆。我们会在月初提交预约。
23
+ none:
24
+ label: 都不合适 / 说不清
25
+ reply: 没关系,我们会在群里再列几个场地。
26
+ caption: 示意交互,场地和价格均为虚构。
27
+ ```
28
+
29
+ - **什么时候用**:给读者两三个具体选项让他选一个,每个选项带图标、一个醒目的数字或价格和一句说明。
30
+ - **什么时候不用**:选项会改变后面的公式或图表计算结果时,用[可调公式与参数](/garden/components/interactive)。
31
+
32
+ 可以这样对 AI 说:
33
+
34
+ ````markdown
35
+ ```garden
36
+ 放一组选择卡让读者在街角咖啡馆(40 元含饮料,重氛围)和社区图书馆(免费,重安静)之间选一个,底部加一个「都不合适」按钮。
37
+ ```
38
+ ````
39
+
40
+ ## 字段说明
41
+
42
+ 全部字段、默认值、取值限制和报错代码见 [组件参考 · choice](/garden/reference/choice)。
@@ -1,38 +1,53 @@
1
1
  ---
2
2
  title: 并排对比(compare)
3
3
  navTitle: 并排对比
4
- description: 把两到三种方案左右并列,对比各自的取舍与属性。
4
+ description: 把两三种方案左右并列,或把不好的做法与更好的做法上下对照。
5
5
  section: components
6
6
  order: 33
7
7
  badge: compare
8
8
  ---
9
9
 
10
- 在 `garden` 想法块里写一句「用 `compare` 并排比较用一句话描述代码改动和手写 diff 两种做法」,Agent 会按 `garden-ui` skill 生成下面的 `garden compare`(手写格式附后)。
11
-
12
10
  ```garden compare
13
11
  items:
14
12
  - title: 写一句话说明改了什么
15
- text: 想法块紧跟在完整代码下面,garden-ui 把它换成 diff,页面合成「代码 / 对比上一版」。
13
+ text: 想法块紧跟在完整代码下面,AI 把它换成 diff,页面合成「代码 / 对比上一版」。
16
14
  - title: 手写 diff
17
- text: 完整代码下面紧跟一段 diff;前面没有完整代码时,garden check 报 diff-orphan。
15
+ text: 完整代码下面紧跟一段 diff。
18
16
  ```
19
17
 
18
+ - **什么时候用**:两三件东西并排看差异(并排模式下每格文字不超过约 60 字),或者把一正一反两种做法放在一起对照。
19
+ - **什么时候不用**:每段说明都很长,或者要横向对齐多个参数维度时,直接用列表或表格。
20
+
21
+ 可以这样对 AI 说:
22
+
20
23
  ````markdown
24
+ ```garden
25
+ 把「用一句话说明改了什么」和「手写 diff」这两种做法并排对比一下。
26
+ ```
27
+ ````
28
+
29
+ ## 正反做法对照(`verdict: good | bad`)
30
+
31
+ 如果要对比「不好的做法」和「更好的做法」,条目带上 `verdict: bad` 或 `verdict: good` 后会改为上下堆叠,并在标题旁显示红叉或绿勾:
32
+
21
33
  ```garden compare
22
34
  items:
23
- - title: 写一句话说明改了什么
24
- text: 想法块紧跟在完整代码下面,garden-ui 把它换成 diff,页面合成「代码 / 对比上一版」。
25
- - title: 手写 diff
26
- text: 完整代码下面紧跟一段 diff;前面没有完整代码时,garden check 报 diff-orphan。
35
+ - title: 不好的提问
36
+ verdict: bad
37
+ text: 「你读过这位作者的其他作品吗?能分享一段书评吗?」
38
+ - title: 更好的提问
39
+ verdict: good
40
+ text: 「你平时喜欢在什么时间读书?想先旁听一次吗?」
41
+ ```
42
+
43
+ 可以这样对 AI 说:
44
+
45
+ ````markdown
46
+ ```garden
47
+ 对比一下问新成员时「不好的提问」和「更好的提问」。
27
48
  ```
28
49
  ````
29
50
 
30
- - **用**:并排对比 2~3 种方案、配置方式或改动前后的行为差异时(每格文字控制在 60 字以内)。
31
- - **别用**:对比项超过 3 个或需要横向对齐多个参数维度时,直接用 Markdown 表格。
51
+ ## 字段说明
32
52
 
33
- | 字段 | 必填 | 类型 | 说明 |
34
- | :--- | :--- | :--- | :--- |
35
- | `items` | 是 | 数组(至少 2 项) | 对比项列表;每项也可简写为 `"标题:说明"` 单行字符串 |
36
- | `items[].title` | 建议 | 字符串 | 该对比列的标题 |
37
- | `items[].text` | 建议 | 字符串或字符串数组 | 该对比列的正文说明 |
38
- | `items[].<自定义键>` | 否 | 字符串或数字 | 除 `title` / `text` 外的附加键值会渲染在卡片底部的属性标签行 |
53
+ 全部字段、默认值、取值限制和报错代码见 [组件参考 · compare](/garden/reference/compare)。