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.
- package/.agents/skills/garden-ui/SKILL.md +26 -19
- package/README.en.md +57 -0
- package/README.md +31 -46
- package/app/admin/preview/[...path]/page.tsx +1 -1
- package/app/blog/[...path]/page.tsx +11 -3
- package/app/columns/[column]/[...path]/page.tsx +12 -14
- package/app/components/ArticleMeasure.tsx +39 -4
- package/app/components/ArticlePager.tsx +32 -0
- package/app/components/Comments.tsx +2 -1
- package/app/components/FeedMore.tsx +70 -0
- package/app/components/GardenDocsNav.tsx +189 -0
- package/app/components/HandbookLangRedirect.tsx +30 -0
- package/app/components/HandbookLangSwitch.tsx +49 -0
- package/app/components/HandbookView.tsx +187 -0
- package/app/components/MarkdownArticle.tsx +15 -8
- package/app/components/MarkdownImage.tsx +6 -4
- package/app/components/SiteChrome.tsx +11 -2
- package/app/components/SiteFooter.tsx +12 -3
- package/app/components/SiteHeader.tsx +27 -23
- package/app/components/TableOfContents.tsx +15 -9
- package/app/components/ThoughtFeed.tsx +41 -0
- package/app/components/UiLang.tsx +18 -0
- package/app/components/Webmentions.tsx +3 -2
- package/app/components/garden/FilePreview.tsx +13 -11
- package/app/components/garden/GardenBlock.tsx +3 -1
- package/app/components/garden/GardenFile.tsx +9 -7
- package/app/components/garden/blocks.tsx +34 -24
- package/app/components/garden/chrome.tsx +20 -13
- package/app/garden/[...slug]/page.tsx +28 -0
- package/app/garden/en/[...slug]/page.tsx +28 -0
- package/app/garden/en/page.tsx +16 -0
- package/app/garden/page.tsx +7 -141
- package/app/garden-modules.d.ts +18 -0
- package/app/globals.css +10 -10
- package/app/layout.tsx +2 -4
- package/app/lib/content.ts +27 -11
- package/app/lib/garden-docs-meta.ts +138 -0
- package/app/lib/handbook-path.ts +11 -0
- package/app/lib/heading-number.ts +13 -0
- package/app/lib/i18n.ts +222 -0
- package/app/lib/page-alternates.ts +2 -2
- package/app/not-found.tsx +5 -3
- package/app/site.config.ts +1 -1
- package/app/styles/a2.css +613 -13
- package/app/styles/shell.css +6 -0
- package/app/thoughts/page/[n]/page.tsx +44 -0
- package/app/thoughts/page.tsx +2 -6
- package/bin/garden.mjs +4 -0
- package/docs/handbook/ai-writing.md +99 -0
- package/docs/handbook/assets/reading-layout.svg +26 -0
- package/docs/handbook/components/card.md +46 -0
- package/docs/handbook/components/chart.md +54 -0
- package/docs/handbook/components/compare.md +38 -0
- package/docs/handbook/components/figure.md +72 -0
- package/docs/handbook/components/file/checklist.md +17 -0
- package/docs/handbook/components/file.md +34 -0
- package/docs/handbook/components/html.md +69 -0
- package/docs/handbook/components/interactive.md +67 -0
- package/docs/handbook/components/stat.md +57 -0
- package/docs/handbook/components/timeline.md +52 -0
- package/docs/handbook/components.md +30 -0
- package/docs/handbook/config.md +125 -0
- package/docs/handbook/deploy.md +273 -0
- package/docs/handbook/design.md +81 -0
- package/docs/handbook/en/ai-writing.md +99 -0
- package/docs/handbook/en/components/card.md +46 -0
- package/docs/handbook/en/components/chart.md +54 -0
- package/docs/handbook/en/components/compare.md +38 -0
- package/docs/handbook/en/components/figure.md +72 -0
- package/docs/handbook/en/components/file/checklist.md +17 -0
- package/docs/handbook/en/components/file.md +34 -0
- package/docs/handbook/en/components/html.md +69 -0
- package/docs/handbook/en/components/interactive.md +67 -0
- package/docs/handbook/en/components/stat.md +57 -0
- package/docs/handbook/en/components/timeline.md +52 -0
- package/docs/handbook/en/components.md +30 -0
- package/docs/handbook/en/config.md +125 -0
- package/docs/handbook/en/deploy.md +273 -0
- package/docs/handbook/en/design.md +81 -0
- package/docs/handbook/en/index.md +192 -0
- package/docs/handbook/en/start.md +92 -0
- package/docs/handbook/en/workflow.md +81 -0
- package/docs/handbook/en/writing.md +156 -0
- package/docs/handbook/index.md +187 -0
- package/docs/handbook/start.md +92 -0
- package/docs/handbook/workflow.md +81 -0
- package/docs/handbook/writing.md +156 -0
- package/package.json +2 -1
- package/scripts/build-content.mjs +144 -15
- package/scripts/handbook-allowlist.mjs +68 -0
- package/scripts/handbook-facts.mjs +1138 -0
- package/scripts/html-lang.mjs +36 -0
- package/scripts/static-404.mjs +25 -0
- package/scripts/ui-cli.mjs +14 -8
- package/site.config.yaml +2 -2
- package/templates/garden-blocks.md +1 -1
- package/templates/site/package.json +1 -3
- package/templates/site/site.config.yaml +2 -0
- package/ui/core/apply.mjs +22 -4
- package/ui/core/cli-locale.mjs +67 -0
- package/ui/core/fence.mjs +18 -3
- package/ui/core/fix-prompt.mjs +18 -15
- package/ui/core/garden-fence.mjs +64 -0
- package/ui/core/scan.mjs +19 -3
- package/ui/garden/adjacent.mjs +21 -0
- package/ui/garden/catalog.mjs +1 -1
- package/ui/garden/check.mjs +6 -0
- package/ui/garden/html-lang.mjs +16 -0
- package/ui/garden/paging.mjs +30 -0
- package/ui/preview/dist/garden-ui.css +9 -9
- package/ui/preview/dist/garden-ui.esm.js +169 -169
- package/ui/preview/dist/garden-ui.iife.js +169 -169
- package/ui/react/components/chart.tsx +7 -4
- package/ui/react/components/display.tsx +6 -3
- package/ui/react/components/inputs.tsx +6 -3
- package/ui/react/components/layout.tsx +6 -4
- package/ui/react/components/media.tsx +7 -4
- package/ui/react/components/sandbox.tsx +3 -1
- package/ui/react/index.tsx +11 -7
- package/ui/react/lang.tsx +85 -0
- package/ui/react/runtime.tsx +3 -1
- package/ui/react/ui.css +9 -9
- package/worker/index.ts +8 -1
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: garden-ui
|
|
3
3
|
description: >-
|
|
4
|
-
Compiles the
|
|
5
|
-
garden
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
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
|
|
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
|
-
- **写 `
|
|
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. 写 `
|
|
49
|
-
-
|
|
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
|
-
```
|
|
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
|
-
-
|
|
72
|
-
- 新内容可以是一个 `
|
|
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
|
-
|
|
92
|
+
前一条是提交前钩子跑的检查,要通过;后一条的 `ok` 要为 true。同一块连续三次修不好,停下来,把诊断原样报告给用户。
|
|
86
93
|
|
|
87
94
|
7. 回复一张表:
|
|
88
95
|
|
|
89
96
|
| id | 选择 | 配方 | 假设 | expect |
|
|
90
97
|
| --- | --- | --- | --- | --- |
|
|
91
|
-
| kv-memory |
|
|
98
|
+
| kv-memory | garden interactive | 参数台 | 默认序列取 4K | batch 1 时 512 MiB |
|
|
92
99
|
|
|
93
100
|
"假设"写原话没说、你替作者定下的东西(默认值、范围、补的 caption)。没有就写"无"。
|
|
94
101
|
|
|
95
102
|
## 禁止
|
|
96
103
|
|
|
97
|
-
- 直接编辑稿件文件。稿件只经 `apply` 改动;`
|
|
104
|
+
- 直接编辑稿件文件。稿件只经 `apply` 改动;`garden html` 引用的 HTML 文件可以直接创建。
|
|
98
105
|
- 编造原话里没有的数字。需要数字但原话没给时,用原话里的公式算,或者问作者。
|
|
99
106
|
- 使用远程地址(图片、脚本、数据)。
|
|
100
107
|
- 改动目标块之外的正文。
|
|
@@ -108,4 +115,4 @@ description: >-
|
|
|
108
115
|
- 两种做法的差别(对比)
|
|
109
116
|
- 一组格子、字节或内存布局(格子图)
|
|
110
117
|
|
|
111
|
-
每处给出:所在小节、为什么文字不够、一段可以直接粘贴到该段落后面的 `
|
|
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
|
-
|
|
3
|
+
[English](README.en.md)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
如果在本地使用了 npm 镜像,请把 `registry` 留在 `~/.npmrc`,不要让镜像域名写进 `package-lock.json`(提交前可运行 `node node_modules/hd-garden/scripts/check-lockfile-registry.mjs` 检查)。
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
## 2. 用一句话写组件(人和 Agent 用同一套工具)
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
在正文想放组件的位置写一个不带名字的 `garden` 围栏,只描述想让读者做什么、看到什么:
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
````markdown
|
|
29
|
+
```garden
|
|
30
|
+
让读者拖动每天写的字数和每周写几天,看写完一篇 8000 字的长文要几周。
|
|
25
31
|
```
|
|
32
|
+
````
|
|
26
33
|
|
|
27
|
-
|
|
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
|
|
47
|
-
make
|
|
48
|
-
make dev
|
|
49
|
-
make check
|
|
50
|
-
make
|
|
51
|
-
|
|
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
|
-
|
|
51
|
+
本地运行 `make dev` 后访问 `/garden`(英文版在 `/garden/en`),或直接阅读仓库内的文档:
|
|
65
52
|
|
|
66
|
-
- [
|
|
67
|
-
- [
|
|
68
|
-
- [
|
|
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
|
|
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
|
|
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
|
|
92
|
-
const
|
|
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}`}
|
|
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
|
+
}
|