hd-garden 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.agents/skills/garden-ui/SKILL.md +26 -19
  2. package/README.en.md +57 -0
  3. package/README.md +31 -46
  4. package/app/admin/preview/[...path]/page.tsx +1 -1
  5. package/app/blog/[...path]/page.tsx +11 -3
  6. package/app/columns/[column]/[...path]/page.tsx +12 -14
  7. package/app/components/ArticleMeasure.tsx +39 -4
  8. package/app/components/ArticlePager.tsx +32 -0
  9. package/app/components/Comments.tsx +2 -1
  10. package/app/components/FeedMore.tsx +70 -0
  11. package/app/components/GardenDocsNav.tsx +189 -0
  12. package/app/components/HandbookLangRedirect.tsx +30 -0
  13. package/app/components/HandbookLangSwitch.tsx +49 -0
  14. package/app/components/HandbookView.tsx +187 -0
  15. package/app/components/MarkdownArticle.tsx +15 -8
  16. package/app/components/MarkdownImage.tsx +6 -4
  17. package/app/components/SiteChrome.tsx +11 -2
  18. package/app/components/SiteFooter.tsx +12 -3
  19. package/app/components/SiteHeader.tsx +27 -23
  20. package/app/components/TableOfContents.tsx +15 -9
  21. package/app/components/ThoughtFeed.tsx +41 -0
  22. package/app/components/UiLang.tsx +18 -0
  23. package/app/components/Webmentions.tsx +3 -2
  24. package/app/components/garden/FilePreview.tsx +13 -11
  25. package/app/components/garden/GardenBlock.tsx +3 -1
  26. package/app/components/garden/GardenFile.tsx +9 -7
  27. package/app/components/garden/blocks.tsx +34 -24
  28. package/app/components/garden/chrome.tsx +20 -13
  29. package/app/garden/[...slug]/page.tsx +28 -0
  30. package/app/garden/en/[...slug]/page.tsx +28 -0
  31. package/app/garden/en/page.tsx +16 -0
  32. package/app/garden/page.tsx +7 -141
  33. package/app/garden-modules.d.ts +18 -0
  34. package/app/globals.css +10 -10
  35. package/app/layout.tsx +2 -4
  36. package/app/lib/content.ts +27 -11
  37. package/app/lib/garden-docs-meta.ts +138 -0
  38. package/app/lib/handbook-path.ts +11 -0
  39. package/app/lib/heading-number.ts +13 -0
  40. package/app/lib/i18n.ts +222 -0
  41. package/app/lib/page-alternates.ts +2 -2
  42. package/app/not-found.tsx +5 -3
  43. package/app/site.config.ts +1 -1
  44. package/app/styles/a2.css +613 -13
  45. package/app/styles/shell.css +6 -0
  46. package/app/thoughts/page/[n]/page.tsx +44 -0
  47. package/app/thoughts/page.tsx +2 -6
  48. package/bin/garden.mjs +4 -0
  49. package/docs/handbook/ai-writing.md +99 -0
  50. package/docs/handbook/assets/reading-layout.svg +26 -0
  51. package/docs/handbook/components/card.md +46 -0
  52. package/docs/handbook/components/chart.md +54 -0
  53. package/docs/handbook/components/compare.md +38 -0
  54. package/docs/handbook/components/figure.md +72 -0
  55. package/docs/handbook/components/file/checklist.md +17 -0
  56. package/docs/handbook/components/file.md +34 -0
  57. package/docs/handbook/components/html.md +69 -0
  58. package/docs/handbook/components/interactive.md +67 -0
  59. package/docs/handbook/components/stat.md +57 -0
  60. package/docs/handbook/components/timeline.md +52 -0
  61. package/docs/handbook/components.md +30 -0
  62. package/docs/handbook/config.md +125 -0
  63. package/docs/handbook/deploy.md +273 -0
  64. package/docs/handbook/design.md +81 -0
  65. package/docs/handbook/en/ai-writing.md +99 -0
  66. package/docs/handbook/en/components/card.md +46 -0
  67. package/docs/handbook/en/components/chart.md +54 -0
  68. package/docs/handbook/en/components/compare.md +38 -0
  69. package/docs/handbook/en/components/figure.md +72 -0
  70. package/docs/handbook/en/components/file/checklist.md +17 -0
  71. package/docs/handbook/en/components/file.md +34 -0
  72. package/docs/handbook/en/components/html.md +69 -0
  73. package/docs/handbook/en/components/interactive.md +67 -0
  74. package/docs/handbook/en/components/stat.md +57 -0
  75. package/docs/handbook/en/components/timeline.md +52 -0
  76. package/docs/handbook/en/components.md +30 -0
  77. package/docs/handbook/en/config.md +125 -0
  78. package/docs/handbook/en/deploy.md +273 -0
  79. package/docs/handbook/en/design.md +81 -0
  80. package/docs/handbook/en/index.md +192 -0
  81. package/docs/handbook/en/start.md +92 -0
  82. package/docs/handbook/en/workflow.md +81 -0
  83. package/docs/handbook/en/writing.md +156 -0
  84. package/docs/handbook/index.md +187 -0
  85. package/docs/handbook/start.md +92 -0
  86. package/docs/handbook/workflow.md +81 -0
  87. package/docs/handbook/writing.md +156 -0
  88. package/package.json +2 -1
  89. package/scripts/build-content.mjs +144 -15
  90. package/scripts/handbook-allowlist.mjs +68 -0
  91. package/scripts/handbook-facts.mjs +1138 -0
  92. package/scripts/html-lang.mjs +36 -0
  93. package/scripts/static-404.mjs +25 -0
  94. package/scripts/ui-cli.mjs +14 -8
  95. package/site.config.yaml +2 -2
  96. package/templates/garden-blocks.md +1 -1
  97. package/templates/site/package.json +1 -3
  98. package/templates/site/site.config.yaml +2 -0
  99. package/ui/core/apply.mjs +22 -4
  100. package/ui/core/cli-locale.mjs +67 -0
  101. package/ui/core/fence.mjs +18 -3
  102. package/ui/core/fix-prompt.mjs +18 -15
  103. package/ui/core/garden-fence.mjs +64 -0
  104. package/ui/core/scan.mjs +19 -3
  105. package/ui/garden/adjacent.mjs +21 -0
  106. package/ui/garden/catalog.mjs +1 -1
  107. package/ui/garden/check.mjs +6 -0
  108. package/ui/garden/html-lang.mjs +16 -0
  109. package/ui/garden/paging.mjs +30 -0
  110. package/ui/preview/dist/garden-ui.css +9 -9
  111. package/ui/preview/dist/garden-ui.esm.js +169 -169
  112. package/ui/preview/dist/garden-ui.iife.js +169 -169
  113. package/ui/react/components/chart.tsx +7 -4
  114. package/ui/react/components/display.tsx +6 -3
  115. package/ui/react/components/inputs.tsx +6 -3
  116. package/ui/react/components/layout.tsx +6 -4
  117. package/ui/react/components/media.tsx +7 -4
  118. package/ui/react/components/sandbox.tsx +3 -1
  119. package/ui/react/index.tsx +11 -7
  120. package/ui/react/lang.tsx +85 -0
  121. package/ui/react/runtime.tsx +3 -1
  122. package/ui/react/ui.css +9 -9
  123. package/worker/index.ts +8 -1
@@ -1,15 +1,19 @@
1
1
  ---
2
2
  name: garden-ui
3
3
  description: >-
4
- Compiles the intent blocks in one Garden post into ui blocks (or prose, or
5
- garden html), recompiles stale or broken ui blocks, and suggests where a post
6
- could use interaction. Use when the user says 编译这篇 / 编译意图块 / 重编译
7
- <id> / 这篇哪里适合交互, or pastes a garden ui check fix prompt.
4
+ Compiles the `garden` idea blocks in one Garden post into garden components
5
+ (garden interactive, or card / stat / compare / ... , or prose, or garden
6
+ html), fixes broken garden interactive blocks, and suggests where a post could
7
+ use interaction. Older intent / ui blocks are handled the same way. Use when
8
+ the user says 编译这篇 / 编译想法 / 重编译 <id> / 这篇哪里适合交互, or pastes a
9
+ garden ui check fix prompt.
8
10
  ---
9
11
 
10
12
  # 编译 Garden 交互块
11
13
 
12
- 作者在稿里用 `intent` 围栏写一句想法,这个 skill 把它变成 `ui` 块。`ui` 块的格式、组件和设计规则都在 `npx garden ui prompt` 的输出里,以那份为准,不凭记忆写组件。
14
+ 作者在稿里用不带名字的 `garden` 围栏写一句想法,这个 skill 把它变成 `garden interactive` 或别的 `garden` 组件。交互组件和设计规则在 `npx garden ui prompt` 的输出里,9 个 `garden` 组件的用途和示例在 `node_modules/hd-garden/templates/garden-blocks.md`(在引擎仓库里是 `templates/garden-blocks.md`),以这两份为准,不凭记忆写组件。
15
+
16
+ 作者提交时,提交前钩子跑的是同一个 `npx garden ui check`。你用来找块、写回、自查的命令,和拦住作者提交的命令是同一套。
13
17
 
14
18
  所有命令在站点根目录运行。没给稿件路径时只问路径,不扫全库。
15
19
 
@@ -17,7 +21,7 @@ description: >-
17
21
 
18
22
  | 用户说 | 处理哪些块 |
19
23
  | --- | --- |
20
- | 编译这篇:`<路径>`、编译意图块 | 状态是 `uncompiled`、`stale`、`error` 的块 |
24
+ | 编译这篇:`<路径>`、编译想法 | 状态是 `uncompiled`、`stale`、`error` 的块 |
21
25
  | 重编译 `<id>` | 只处理这一块,不管状态 |
22
26
  | 粘贴了 `garden ui check` 的修复提示词 | 提示词里列出的块 |
23
27
  | 这篇哪里适合交互 | 只读建议模式,见文末,不改稿 |
@@ -30,7 +34,7 @@ description: >-
30
34
  npx garden ui scan content/posts/路径.md
31
35
  ```
32
36
 
33
- stdout 是 JSON。每块有 `lang`、`id`、`status`、`startLine`、`hash`(整段围栏的哈希,写回时当 `--base`)、`intentHash`(原话的哈希)、`intent`(原话)、`caption`、`section`(所在小节标题)和 `diagnostics`。不要自己用正则找围栏。
37
+ stdout 是 JSON。`lang` 是 `garden-idea`(想法)、`garden-interactive`,或旧写法的 `intent` / `ui`。每块有 `lang`、`id`、`status`、`startLine`、`hash`(整段围栏的哈希,写回时当 `--base`)、`intentHash`(原话的哈希)、`intent`(原话)、`caption`、`section`(所在小节标题)和 `diagnostics`。不要自己用正则找围栏。
34
38
 
35
39
  2. 读规则:
36
40
 
@@ -42,11 +46,14 @@ description: >-
42
46
 
43
47
  3. 每块读原话和所在小节,三选一:
44
48
  - **写回正文**:一句话、一张表或一张 Mermaid 就能说清时。
45
- - **写 `ui`**:读者需要拖、点、翻才能看懂时。
49
+ - **写 `garden interactive`**:读者需要拖、点、翻才能看懂时。
50
+ - **写一段 `diff`**:想法紧跟在一段完整代码后面,说的是「和上一版比改了什么」时。按原话写出 diff,用一段普通 Markdown 写回(只有这一个 `diff` 围栏)。写回后它紧挨着那段代码,页面会合成「代码 / 对比上一版」切换;diff 里的新行要和上面的完整代码一致。
51
+ - **写别的 `garden` 组件**:一张资源卡、几个指标、两种做法并排、一条时间线、一张静态图表、一张结构图、一份下载文件。
46
52
  - **写 `garden html`**:目录表达不了的画布动画、音频、物理模拟等。按 [garden-interactive-chart](../garden-interactive-chart/SKILL.md) 在同名目录写 HTML,用 `title` 写下判断。
47
53
 
48
- 4. 写 `ui` 时:
49
- - 原话原样放进块头的 `intent`,一个字都不改。`compiled` 不用写,`apply` 会写。
54
+ 4. 写 `garden interactive` 时:
55
+ - 格式:围栏头写 `garden interactive #<id>`;`---` 上面是 `caption`、`intent`、可选的 `expect`;`---` 下面是 `npx garden ui prompt` 里的组件树。
56
+ - 原话原样放进 `intent`,一个字都不改。
50
57
  - `caption` 摘原话里的判断。原话没有判断句,就根据小节内容补一句,并在回复里说明。
51
58
  - 原话里有已知点("batch 1 时是 512 MiB"),写进 `expect`。
52
59
  - 数据写在块里。原话提到同名目录里已有的数据文件时,才用 `Data` 引用。
@@ -56,9 +63,7 @@ description: >-
56
63
 
57
64
  ````bash
58
65
  npx garden ui apply content/posts/路径.md --id <id> --base <hash> <<'GARDEN_UI'
59
- ```ui
60
- ---
61
- id: ...
66
+ ```garden interactive #id
62
67
  caption: ...
63
68
  intent: |
64
69
  ...
@@ -68,8 +73,9 @@ description: >-
68
73
  GARDEN_UI
69
74
  ````
70
75
 
71
- - 没写 id 的 `intent` 块用 `--intent-hash <intentHash>` 定位;原话重复时改用 `--at <startLine>`。
72
- - 新内容可以是一个 `ui` 围栏、一个 `garden html` 围栏,或一段普通 Markdown(写回正文时)。
76
+ - 想法块没有 id,用 `--intent-hash <intentHash>` 定位;原话重复时改用 `--at <startLine>`。
77
+ - 新内容可以是一个 `garden` 组件围栏(interactive、card、stat……),或一段普通 Markdown(写回正文时)。旧稿里的 `ui` 块也可以按原格式写回。
78
+ - 新内容里不能再有没编译的想法。
73
79
  - 成功时 stdout 是 `{"ok":true,...}`。失败时 stderr 是 JSON,按 `code` 处理:
74
80
  - `apply-invalid`:新内容没通过校验,按 `diagnostics` 改了再写。
75
81
  - `apply-base`:块在扫描之后被改过。重新 `scan`,用新的 `hash`。
@@ -79,22 +85,23 @@ description: >-
79
85
  6. 全部写完后:
80
86
 
81
87
  ```bash
88
+ npx garden ui check content/posts/路径.md
82
89
  npx garden check content/posts/路径.md
83
90
  ```
84
91
 
85
- 直到 `ok` 为 true。同一块连续三次修不好,停下来,把诊断原样报告给用户。
92
+ 前一条是提交前钩子跑的检查,要通过;后一条的 `ok` 要为 true。同一块连续三次修不好,停下来,把诊断原样报告给用户。
86
93
 
87
94
  7. 回复一张表:
88
95
 
89
96
  | id | 选择 | 配方 | 假设 | expect |
90
97
  | --- | --- | --- | --- | --- |
91
- | kv-memory | ui | 参数台 | 默认序列取 4K | batch 1 时 512 MiB |
98
+ | kv-memory | garden interactive | 参数台 | 默认序列取 4K | batch 1 时 512 MiB |
92
99
 
93
100
  "假设"写原话没说、你替作者定下的东西(默认值、范围、补的 caption)。没有就写"无"。
94
101
 
95
102
  ## 禁止
96
103
 
97
- - 直接编辑稿件文件。稿件只经 `apply` 改动;`widget` 的 HTML 文件可以直接创建。
104
+ - 直接编辑稿件文件。稿件只经 `apply` 改动;`garden html` 引用的 HTML 文件可以直接创建。
98
105
  - 编造原话里没有的数字。需要数字但原话没给时,用原话里的公式算,或者问作者。
99
106
  - 使用远程地址(图片、脚本、数据)。
100
107
  - 改动目标块之外的正文。
@@ -108,4 +115,4 @@ description: >-
108
115
  - 两种做法的差别(对比)
109
116
  - 一组格子、字节或内存布局(格子图)
110
117
 
111
- 每处给出:所在小节、为什么文字不够、一段可以直接粘贴到该段落后面的 `intent` 草稿。草稿只写读者要做什么、看到什么,以及原文里已有的数字,不写组件名。最多 5 处;没有合适的就说没有。
118
+ 每处给出:所在小节、为什么文字不够、一段可以直接粘贴到该段落后面的 `garden` 想法块草稿。草稿只写读者要做什么、看到什么,以及原文里已有的数字,不写组件名。最多 5 处;没有合适的就说没有。
package/README.en.md ADDED
@@ -0,0 +1,57 @@
1
+ # Garden
2
+
3
+ [中文](README.md)
4
+
5
+ Garden is a Claude-inspired personal blog framework. You write posts, short thoughts, and multi-part columns in Markdown inside your own Git repository, and describe structured or interactive components in a single plain-language sentence.
6
+
7
+ Your site repository keeps only `site.config.yaml` and `content/`. Rendering, static export, and verification come from the `hd-garden` package.
8
+
9
+ ## 1. Create a site
10
+
11
+ Requires **Node.js `>= 22.13.0`**, **Git**, and **GNU Make**:
12
+
13
+ ```bash
14
+ npx hd-garden init my-blog
15
+ cd my-blog
16
+ npm install
17
+ make dev
18
+ ```
19
+
20
+ The published npm package is `hd-garden`; once installed inside a site, you can run `npx garden` (do not run `npx garden init` directly, as `garden` on npm is an unrelated package). Running `make dev` for the first time creates `.env.local`, installs the three AI skills under `.agents/skills/`, installs the pre-commit check in `.git/hooks/pre-commit`, and starts the dev server at `http://localhost:3000` (with the built-in handbook at `http://localhost:3000/garden/en`).
21
+
22
+ ## 2. Describe components in plain language (humans and Agents share the same CLI)
23
+
24
+ Where you want a component or interactive calculation, write an unnamed `garden` fence describing what the reader should adjust and see:
25
+
26
+ ````markdown
27
+ ```garden
28
+ 让读者拖动每天写的字数和每周写几天,看写完一篇 8000 字的长文要几周。
29
+ ```
30
+ ````
31
+
32
+ Ask your Agent to compile the post using the bundled `garden-ui` skill: it runs `npx garden ui scan` to locate uncompiled or broken blocks, reads the component catalog from `npx garden ui prompt`, generates a `garden interactive` block (or `card`, `stat`, `compare`, `timeline`, `chart`, `figure`, `html`, `file`, or a `diff` block right below a code snippet), and writes only that fence back via `npx garden ui apply`. If you run `git commit` before compiling, the pre-commit hook runs `npx --no-install garden ui check --staged --quiet`, stops the commit, and prints a ready-to-paste fix prompt for your AI.
33
+
34
+ ## 3. Common commands
35
+
36
+ ```bash
37
+ make new SLUG=writing-pace TITLE="Writing Pace" # Create a draft post in content/posts/
38
+ make dev # Sync content and start the local server
39
+ CONTENT_INCLUDE_DRAFTS=1 make dev # Preview drafts and future publishAt posts
40
+ make check # Run garden sync and garden build
41
+ make update MESSAGE="Add writing-pace post" # Check, commit, and push to origin
42
+ npx garden check content/posts/writing-pace.md # Validate front matter, components, and assets
43
+ npx garden ui check --staged # Check staged posts for uncompiled ideas or UI errors
44
+ npx garden ui catalog # Generate an offline component gallery at .garden/ui-catalog.html
45
+ ```
46
+
47
+ ## 4. Handbook and reference docs
48
+
49
+ Start `make dev` and open `/garden/en` (Chinese at `/garden`), or read the markdown sources directly:
50
+
51
+ - **[Garden Handbook (English)](docs/handbook/en/index.md)** · **[Garden 使用说明(中文)](docs/handbook/index.md)**: [Quick Start](docs/handbook/en/start.md), [Writing with AI](docs/handbook/en/ai-writing.md), [Site & Column Configuration](docs/handbook/en/config.md), [Deploy to GitHub Pages](docs/handbook/en/deploy.md), [Writing Posts & Thoughts](docs/handbook/en/writing.md), [How Reading Layout Works](docs/handbook/en/design.md), [9 Built-in Components](docs/handbook/en/components.md), and [Checking & Shortcuts](docs/handbook/en/workflow.md).
52
+ - **[Quick Start Guide](docs/quick-start.md)** and **[GitHub Pages Deployment](docs/github-pages.md)**: single public repository or private source + public output repository setup, plus Giscus comments.
53
+ - **[Admin Service](docs/admin-service.md)**: optional `/admin` server (local, Docker, or Cloudflare Workers) for posting thoughts from the web, uploading media to Alibaba Cloud OSS, and using the `/admin/ui` workbench.
54
+
55
+ ## License
56
+
57
+ [MIT](LICENSE). Sites built with Garden should keep the Garden attribution link in the footer.
package/README.md CHANGED
@@ -1,73 +1,58 @@
1
1
  # Garden
2
2
 
3
- 用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。
3
+ [English](README.en.md)
4
4
 
5
- 站点仓库只保存稿件和身份:`content/` 与 `site.config.yaml`。页面、构建和发布工作流来自这个 Garden 包。
5
+ Garden 是一个 Claude 风格的个人博客框架:文章、随想和专栏用 Markdown 写,保存在自己的 Git 仓库里;图表、指标和交互组件用一句自然语言描述就行。
6
6
 
7
- ## 新建站点
7
+ 站点仓库只保存你的配置文件(`site.config.yaml`)与稿件(`content/`),页面渲染、静态构建和检查工具由 `hd-garden` 包提供。
8
+
9
+ ## 1. 新建站点
10
+
11
+ 需要 **Node.js `>= 22.13.0`**、**Git** 与 **GNU Make**:
8
12
 
9
13
  ```bash
10
14
  npx hd-garden init my-blog
11
15
  cd my-blog
12
16
  npm install
17
+ make dev
13
18
  ```
14
19
 
15
- 推荐把 GitHub 仓库命名为 `<你的 GitHub 用户名>.github.io`,并在 **Settings → Pages** 把 Source 设为 **GitHub Actions**。然后修改 `site.config.yaml`,再写第一篇 Markdown。公开包名是 `hd-garden`;装进站点之后也可以用 `garden`。不要运行 `npx garden init`,npm 上的 `garden` 是别人的样式库。
20
+ 公开包名是 `hd-garden`,装进站点后可使用 `npx garden`(不要直接运行未安装的 `npx garden init`,npm 上的 `garden` 是另一个包)。首次运行 `make dev` 会生成 `.env.local`、补齐 `.agents/skills/` 下的三个 AI skill,并在 `.git/hooks/pre-commit` 安装提交前检查,默认预览地址是 `http://localhost:3000`(使用说明在 `http://localhost:3000/garden`)。
16
21
 
17
- 在国内想用 npm 镜像(例如 npmmirror),把 `registry` 写在自己的 `~/.npmrc` 或不提交的项目 `.npmrc` 里,不要让镜像地址进 `package-lock.json`:GitHub Actions 连不上镜像时 `npm ci` 会一直卡住。镜像会把新装包的 `resolved` 写成镜像地址,提交前运行 `node node_modules/hd-garden/scripts/check-lockfile-registry.mjs` 检查(Garden 仓库的 CI 也会检查);报错时把这些地址的域名换回 `https://registry.npmjs.org/`,路径和 integrity 都不用改。
22
+ 如果在本地使用了 npm 镜像,请把 `registry` 留在 `~/.npmrc`,不要让镜像域名写进 `package-lock.json`(提交前可运行 `node node_modules/hd-garden/scripts/check-lockfile-registry.mjs` 检查)。
18
23
 
19
- 也可以继续用 **Use this template**;该仓库同时是可运行的演示站。
24
+ ## 2. 用一句话写组件(人和 Agent 用同一套工具)
20
25
 
21
- 完成后运行:
26
+ 在正文想放组件的位置写一个不带名字的 `garden` 围栏,只描述想让读者做什么、看到什么:
22
27
 
23
- ```bash
24
- make update
28
+ ````markdown
29
+ ```garden
30
+ 让读者拖动每天写的字数和每周写几天,看写完一篇 8000 字的长文要几周。
25
31
  ```
32
+ ````
26
33
 
27
- 它会把本次改动提交并推送到源码仓库;发布工作流完成后,站点自动更新。初次使用请跟着[从零开始](docs/quick-start.md)做一遍。
28
-
29
- ## 日常写作只需要知道这些
30
-
31
- | 你想做什么 | 从哪里开始 |
32
- | --- | --- |
33
- | 写一篇博文 | [新建文章](docs/content-authoring.md#新建一篇博文) |
34
- | 添加封面、图片、代码或公式 | [丰富文章内容](docs/content-authoring.md#丰富文章内容) |
35
- | 保存为草稿或置顶文章 | [草稿与置顶](docs/content-authoring.md#草稿与置顶) |
36
- | 建一个主题专栏 | [创建专栏](docs/content-authoring.md#创建专栏) |
37
- | 发布一条带多媒体的随想 | [发布随想](docs/content-authoring.md#发布随想) |
38
- | 改网站名称、作者和一级页标题 | [站点配置](docs/site-configuration.md) |
39
- | 给 AI 装可交互图表 skill | 第一次 `npx garden dev` 会自动装;更新用 `npx garden skill` |
40
- | 升级 Garden 引擎 | 把 `hd-garden` 改到新版本,再 `npm install` |
41
- | 发布本次改动 | `make update` |
34
+ 让 Agent 按站点里的 `garden-ui` skill 处理这篇稿件:Agent 先用 `npx garden ui scan` 读取待编译块的哈希,再读 `npx garden ui prompt` 的组件规则,生成 `garden interactive`(或 `card`、`stat`、`compare`、`timeline`、`chart`、`figure`、`html`、`file` 等内置组件,或在完整代码下方生成 `diff`),最后用 `npx garden ui apply` 只替换该围栏。如果你在编译前直接 `git commit`,pre-commit 钩子会运行同一条 `npx --no-install garden ui check --staged --quiet` 拦下提交,并打印一段可直接粘贴给 AI 的修复提示词。
42
35
 
43
- ## 常用命令
36
+ ## 3. 常用命令
44
37
 
45
38
  ```bash
46
- make new # 创建文章草稿(无需安装 npm 依赖)
47
- make skill # 安装可交互图表 skill
48
- make dev # 首次准备并本地预览
49
- make check # 检查内容与运行测试
50
- make build # 构建公开站点
51
- make update # 检查、提交并触发自动发布
39
+ make new SLUG=writing-pace TITLE="慢慢写" # 在 content/posts/ 下创建文章草稿
40
+ make dev # 同步内容并启动本地预览
41
+ CONTENT_INCLUDE_DRAFTS=1 make dev # 连同草稿和未来定时文章一起预览
42
+ make check # 运行 garden sync 与 garden build 验证全站
43
+ make update MESSAGE="新增一篇文章" # 检查通过后提交并推送到远端仓库
44
+ npx garden check content/posts/writing-pace.md # 单独检查一篇文章的标题、组件与资源路径
45
+ npx garden ui check --staged # 检查暂存区稿件中的想法块与交互组件
46
+ npx garden ui catalog # 在 .garden/ui-catalog.html 生成离线组件清单
52
47
  ```
53
48
 
54
- 需要自定义提交说明时:
55
-
56
- ```bash
57
- make update MESSAGE="新增一篇文章"
58
- ```
59
-
60
- `make dev` 会在首次运行时自动安装依赖并创建 `.env.local`,同时说明如何接上评论。线上发布要求 Actions Variables 里有 `GISCUS_REPO`、`GISCUS_REPO_ID`、`GISCUS_CATEGORY`、`GISCUS_CATEGORY_ID`;留空会让构建失败。确定不开放评论时设置 `GISCUS_DISABLED=1`。`make update` 会先检查,再提交并推送工作区中的所有改动。不想发布的文件请先移出仓库或加入 `.gitignore`。
61
-
62
- ## 进一步配置
49
+ ## 4. 使用说明与深入文档
63
50
 
64
- 评论接法见[从零开始](docs/quick-start.md)。跑通以后,再按需查看:
51
+ 本地运行 `make dev` 后访问 `/garden`(英文版在 `/garden/en`),或直接阅读仓库内的文档:
65
52
 
66
- - [评论、Webmentions 与内容分发](docs/integrations.md)
67
- - [随想管理后台](docs/admin-service.md)
68
- - [GitHub Pages 与自定义域名](docs/github-pages.md)
69
- - [系统怎么工作](docs/architecture.md)
70
- - [全部文档](docs/README.md)
53
+ - **[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)、[9 个内置组件](docs/handbook/components.md) 与 [语法检查](docs/handbook/workflow.md)。
54
+ - **[从零开始](docs/quick-start.md)** 与 **[GitHub Pages 部署](docs/github-pages.md)**:单公开仓库或「私有源码仓库 + 公开页面仓库」的 Actions 配置与 Giscus 评论接入。
55
+ - **[独立管理后台](docs/admin-service.md)**:可选的 `/admin` 服务(本地、Docker 或 Cloudflare Workers),支持网页发随想、阿里云 OSS 媒体直传与 `/admin/ui` 组件工作台。
71
56
 
72
57
  ## 许可证
73
58
 
@@ -88,7 +88,7 @@ export default async function DraftPreviewPage({ params }: { params: Promise<{ p
88
88
  <h1 className="ui-admin-preview-title">{typeof data.title === "string" ? data.title : post}</h1>
89
89
  {typeof data.description === "string" ? <p className="ui-studio-meta">{data.description}</p> : null}
90
90
  <article className="article-body">
91
- <MarkdownArticle content={proxyAssets(content, post)} />
91
+ <MarkdownArticle content={proxyAssets(content, post)} numbering={data.numbering === false ? false : undefined} />
92
92
  </article>
93
93
  </section>
94
94
  </AdminUiShell>
@@ -19,6 +19,8 @@ import { TableOfContents } from "../../components/TableOfContents";
19
19
  import { Comments } from "../../components/Comments";
20
20
  import { Webmentions } from "../../components/Webmentions";
21
21
  import { ArticleTags } from "../../components/ArticleTags";
22
+ import { ArticlePager } from "../../components/ArticlePager";
23
+ import { adjacentByDate } from "../../../ui/garden/adjacent.mjs";
22
24
  import {
23
25
  absoluteUrl,
24
26
  getSiteUrl,
@@ -26,6 +28,7 @@ import {
26
28
  } from "../../lib/site";
27
29
  import { githubUrl, siteConfig } from "../../site.config";
28
30
  import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../ui/garden/static-params.mjs";
31
+ import { ui } from "../../lib/i18n";
29
32
 
30
33
  export function generateStaticParams() {
31
34
  return staticParams(posts.map((post) => ({ path: post.path.split("/") })), { path: [STATIC_PLACEHOLDER] });
@@ -77,11 +80,12 @@ export default async function PostPage({
77
80
  if (isStaticPlaceholder(path)) return null;
78
81
  notFound();
79
82
  }
80
- const headings = extractHeadings(post.content);
83
+ const headings = extractHeadings(post.content, { numbering: post.numbering });
81
84
  const baseUrl = await getSiteUrl();
82
85
  const pathname = postHref(post);
83
86
  const canonicalUrl = absoluteUrl(pathname, baseUrl);
84
87
  const wordCount = estimateWordCount(post.content);
88
+ const { prev, next } = adjacentByDate(posts, (item: { path: string }) => item.path === post.path);
85
89
 
86
90
  return (
87
91
  <main className="reading-page post-reading-page">
@@ -106,7 +110,7 @@ export default async function PostPage({
106
110
  </time>
107
111
  </li>
108
112
  {post.updated ? <li>更新于 {formatDate(post.updated)}</li> : null}
109
- <li>约 {wordCount.toLocaleString("en-US")} 字</li>
113
+ <li>{ui().wordCount(wordCount)}</li>
110
114
  {originalUrl(post) ? (
111
115
  <li>
112
116
  <a className="orig ext" href={originalUrl(post)} target="_blank" rel="noopener noreferrer">原文 ↗</a>
@@ -125,13 +129,17 @@ export default async function PostPage({
125
129
  <ArticleMeasure />
126
130
  <div className="e-content">
127
131
  {post.description ? <p className="lead quote-plain p-summary">{post.description}</p> : null}
128
- <MarkdownArticle content={post.content} />
132
+ <MarkdownArticle content={post.content} numbering={post.numbering} />
129
133
  </div>
130
134
  <ArticleReferences references={post.references} />
131
135
  <Webmentions
132
136
  target={canonicalUrl}
133
137
  endpoint={webmentionApiEndpoint()}
134
138
  />
139
+ <ArticlePager
140
+ prev={prev ? { href: postHref(prev), title: prev.title } : null}
141
+ next={next ? { href: postHref(next), title: next.title } : null}
142
+ />
135
143
  <Comments slug={post.path} />
136
144
  </div>
137
145
  <TableOfContents headings={headings} />
@@ -1,9 +1,10 @@
1
1
  import Link from "next/link";
2
+ import { ArticlePager } from "../../../components/ArticlePager";
2
3
  import { CoverImage } from "../../../components/CoverImage";
3
4
  import type { Metadata } from "next";
4
5
  import { FEED_ALTERNATES } from "../../../lib/page-alternates";
5
6
  import { notFound } from "next/navigation";
6
- import { ArrowLeft, ArrowRight } from "@phosphor-icons/react/ssr";
7
+ import { ArrowLeft } from "@phosphor-icons/react/ssr";
7
8
  import {
8
9
  columns,
9
10
  extractHeadings,
@@ -28,6 +29,7 @@ import {
28
29
  import { githubUrl, siteConfig } from "../../../site.config";
29
30
  import { siteHref } from "../../../lib/site-href";
30
31
  import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../../ui/garden/static-params.mjs";
32
+ import { ui } from "../../../lib/i18n";
31
33
 
32
34
  export function generateStaticParams() {
33
35
  return staticParams(
@@ -88,8 +90,9 @@ export default async function ColumnEntryPage({
88
90
  const series = columns.filter((item) => item.column === column);
89
91
  const listed = [...series].sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
90
92
  const currentIndex = series.findIndex((item) => item.path === entry.path);
91
- const next = series[currentIndex + 1];
92
- const headings = extractHeadings(entry.content);
93
+ const prev = series[currentIndex - 1] ?? null;
94
+ const next = series[currentIndex + 1] ?? null;
95
+ const headings = extractHeadings(entry.content, { numbering: entry.numbering });
93
96
  const baseUrl = await getSiteUrl();
94
97
  const pathname = columnHref(entry);
95
98
  const canonicalUrl = absoluteUrl(pathname, baseUrl);
@@ -147,23 +150,18 @@ export default async function ColumnEntryPage({
147
150
  <ArticleMeasure />
148
151
  <div className="e-content">
149
152
  {entry.description ? <p className="lead p-summary">{entry.description}</p> : null}
150
- <MarkdownArticle content={entry.content} />
153
+ <MarkdownArticle content={entry.content} numbering={entry.numbering} />
151
154
  </div>
152
155
  <ArticleReferences references={entry.references} />
153
- {next && (
154
- <Link
155
- href={columnHref(next)}
156
- className="next-entry"
157
- >
158
- <span>下一篇</span>
159
- <strong>{next.title}</strong>
160
- <ArrowRight size={20} />
161
- </Link>
162
- )}
163
156
  <Webmentions
164
157
  target={canonicalUrl}
165
158
  endpoint={webmentionApiEndpoint()}
166
159
  />
160
+ <ArticlePager
161
+ label={ui().columnPagerLabel}
162
+ prev={prev ? { href: columnHref(prev), title: prev.title } : null}
163
+ next={next ? { href: columnHref(next), title: next.title } : null}
164
+ />
167
165
  <Comments slug={`${column}:${entry.path}`} />
168
166
  </article>
169
167
 
@@ -45,7 +45,14 @@ export function ArticleMeasure() {
45
45
  return width;
46
46
  };
47
47
 
48
+ const docPage = article.closest<HTMLElement>(".garden-doc-page");
49
+
48
50
  const placeToc = () => {
51
+ if (docPage) {
52
+ root.removeAttribute("data-toc-yield");
53
+ root.removeAttribute("data-toc-hide");
54
+ return;
55
+ }
49
56
  if (!toc) {
50
57
  root.removeAttribute("data-toc-yield");
51
58
  return;
@@ -74,10 +81,38 @@ export function ArticleMeasure() {
74
81
  }
75
82
  };
76
83
 
84
+ const clampMargin = (margin: { left: number; right: number }, maxRoom: number | null) => {
85
+ if (maxRoom === null) return margin;
86
+ const side = Math.max(0, Math.min(margin.left, maxRoom));
87
+ return { left: side, right: side };
88
+ };
89
+
77
90
  const measure = (refresh: boolean) => {
78
91
  // Measure the text column, not the whole article grid (which includes the TOC rail).
79
92
  const articleBox = (article.querySelector(".markdown-body") ?? article).getBoundingClientRect();
80
93
  const viewport = root.clientWidth;
94
+ let docMaxRoom: number | null = null;
95
+ if (docPage) {
96
+ const sidebar = docPage.querySelector<HTMLElement>(".gdoc-sidebar");
97
+ let leftLimit = 24;
98
+ if (sidebar && getComputedStyle(sidebar).position === "sticky") {
99
+ const sbBox = sidebar.getBoundingClientRect();
100
+ if (sbBox.width > 0 && sbBox.right <= articleBox.left + 1) {
101
+ leftLimit = sbBox.right + 16;
102
+ }
103
+ }
104
+ let rightLimit = viewport - 24;
105
+ if (toc && getComputedStyle(toc).display !== "none") {
106
+ const tocBox = toc.getBoundingClientRect();
107
+ if (tocBox.width > 0 && tocBox.left >= articleBox.right - 1) {
108
+ rightLimit = tocBox.left - translatedX(toc) - 16;
109
+ }
110
+ }
111
+ docMaxRoom = Math.max(
112
+ 0,
113
+ Math.floor(Math.min(articleBox.left - leftLimit, rightLimit - articleBox.right)),
114
+ );
115
+ }
81
116
  const widen = (element: HTMLElement, margin: { left: number; right: number }) => {
82
117
  if (margin.left <= 0) return;
83
118
  element.style.setProperty("--wl", `${margin.left}px`);
@@ -89,13 +124,13 @@ export function ArticleMeasure() {
89
124
  const width = tableWidth(wrap, refresh);
90
125
  resetWide(wrap);
91
126
  if (!width) return;
92
- widen(wrap, tableBreakout({
127
+ widen(wrap, clampMargin(tableBreakout({
93
128
  viewport,
94
129
  articleLeft: articleBox.left,
95
130
  articleRight: articleBox.right,
96
131
  tableClient: wrap.clientWidth,
97
132
  tableScroll: width,
98
- }));
133
+ }), docMaxRoom));
99
134
  });
100
135
  // Images: as wide as they display (2x screenshots at half their pixels), up to 1040px in total.
101
136
  article.querySelectorAll<HTMLElement>(".fig").forEach((figure) => {
@@ -115,7 +150,7 @@ export function ArticleMeasure() {
115
150
  if (!pixels) return;
116
151
  // width="" written by the build is the pixel size, not an author's choice.
117
152
  const attrWidth = known ? Number(image.dataset.aw) || 0 : Number(image.getAttribute("width")) || 0;
118
- const breakout = figureBreakout({
153
+ const breakout = clampMargin(figureBreakout({
119
154
  viewport,
120
155
  articleLeft: articleBox.left,
121
156
  articleRight: articleBox.right,
@@ -123,7 +158,7 @@ export function ArticleMeasure() {
123
158
  imageClient: image.clientWidth,
124
159
  naturalWidth: pixels,
125
160
  attrWidth,
126
- });
161
+ }), docMaxRoom);
127
162
  widen(figure, breakout);
128
163
  // Only a widened image, or one with a width attribute, gets an inline width.
129
164
  const width = figureImageWidth({ naturalWidth: pixels, attrWidth, widened: breakout.left > 0 });
@@ -0,0 +1,32 @@
1
+ import Link from "next/link";
2
+ import { ArrowLeft, ArrowRight } from "@phosphor-icons/react/ssr";
3
+ import { ui } from "../lib/i18n";
4
+
5
+ export type PagerTarget = { href: string; title: string } | null;
6
+
7
+ /** 上一篇 / 下一篇, the same cards as the handbook pager. A missing side stays empty. */
8
+ export function ArticlePager({ prev, next, label = ui().pagerLabel }: { prev: PagerTarget; next: PagerTarget; label?: string }) {
9
+ if (!prev && !next) return null;
10
+ return (
11
+ <nav className="gdoc-pager article-pager" aria-label={label}>
12
+ {prev ? (
13
+ <Link href={prev.href} className="gdoc-pager-card is-prev">
14
+ <span className="gdoc-pager-dir">
15
+ <ArrowLeft size={14} aria-hidden="true" />
16
+ {ui().prev}
17
+ </span>
18
+ <strong>{prev.title}</strong>
19
+ </Link>
20
+ ) : null}
21
+ {next ? (
22
+ <Link href={next.href} className="gdoc-pager-card is-next">
23
+ <span className="gdoc-pager-dir">
24
+ {ui().next}
25
+ <ArrowRight size={14} aria-hidden="true" />
26
+ </span>
27
+ <strong>{next.title}</strong>
28
+ </Link>
29
+ ) : null}
30
+ </nav>
31
+ );
32
+ }
@@ -1,6 +1,7 @@
1
1
  import { ChatCircle, GithubLogo } from "@phosphor-icons/react/ssr";
2
2
  import { readGiscusConfig } from "../lib/giscus-config";
3
3
  import { GiscusComments } from "./GiscusComments";
4
+ import { ui } from "../lib/i18n";
4
5
 
5
6
  export function Comments({ slug }: { slug: string }) {
6
7
  const config = readGiscusConfig();
@@ -10,7 +11,7 @@ export function Comments({ slug }: { slug: string }) {
10
11
  <div className="comments-heading">
11
12
  <ChatCircle size={23} />
12
13
  <div>
13
- <h2 id={`comments-heading-${slug}`}>评论</h2>
14
+ <h2 id={`comments-heading-${slug}`}>{ui().comments}</h2>
14
15
  <p>
15
16
  {config
16
17
  ? "评论由 GitHub Discussions 保存和管理。"
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+
5
+ type Labels = { nextPage: string; loadingMore: string; loadFailed: string; feedEnd: string };
6
+
7
+ /**
8
+ * The end of a feed page. Without JavaScript it is a plain 下一页 link to the
9
+ * next pre-rendered page. With JavaScript, when it scrolls near the viewport it
10
+ * fetches that page's HTML, appends its items to the list above and moves on.
11
+ */
12
+ export function FeedMore({ next, streamSelector, labels }: { next: string | null; streamSelector: string; labels: Labels }) {
13
+ // Page links are written without the basePath; the pages fetched later carry it already.
14
+ const base = (process.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/+$/u, "");
15
+ const [href, setHref] = useState(next ? `${base}${next}` : null);
16
+ const [state, setState] = useState<"idle" | "loading" | "error">("idle");
17
+ const navRef = useRef<HTMLElement>(null);
18
+ const busy = useRef(false);
19
+
20
+ const load = useCallback(async () => {
21
+ if (!href || busy.current) return;
22
+ busy.current = true;
23
+ setState("loading");
24
+ try {
25
+ const response = await fetch(href, { headers: { Accept: "text/html" } });
26
+ if (!response.ok) throw new Error(String(response.status));
27
+ const page = new DOMParser().parseFromString(await response.text(), "text/html");
28
+ const incoming = page.querySelector(streamSelector);
29
+ const stream = navRef.current?.parentElement?.querySelector(streamSelector);
30
+ if (!incoming || !stream) throw new Error("feed markup");
31
+ stream.append(...Array.from(incoming.children).map((node) => document.importNode(node, true)));
32
+ setHref(page.querySelector<HTMLAnchorElement>("[data-feed-next]")?.getAttribute("href") ?? null);
33
+ setState("idle");
34
+ } catch {
35
+ setState("error");
36
+ } finally {
37
+ busy.current = false;
38
+ }
39
+ }, [href, streamSelector]);
40
+
41
+ useEffect(() => {
42
+ const node = navRef.current;
43
+ if (!node || !href || state !== "idle" || typeof IntersectionObserver === "undefined") return;
44
+ const observer = new IntersectionObserver((entries) => {
45
+ if (entries.some((entry) => entry.isIntersecting)) void load();
46
+ }, { rootMargin: "600px 0px" });
47
+ observer.observe(node);
48
+ return () => observer.disconnect();
49
+ }, [href, state, load]);
50
+
51
+ return (
52
+ <nav ref={navRef} className="feed-more" aria-live="polite">
53
+ {href ? (
54
+ <a
55
+ href={href}
56
+ rel="next"
57
+ data-feed-next=""
58
+ onClick={(event) => {
59
+ event.preventDefault();
60
+ void load();
61
+ }}
62
+ >
63
+ {state === "loading" ? labels.loadingMore : state === "error" ? labels.loadFailed : labels.nextPage}
64
+ </a>
65
+ ) : (
66
+ <span className="feed-end">{labels.feedEnd}</span>
67
+ )}
68
+ </nav>
69
+ );
70
+ }