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.
- package/.agents/skills/garden-ui/SKILL.md +2 -2
- package/.env.en.example +67 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/app/a2/[slug]/page.tsx +4 -3
- package/app/a2/page.tsx +4 -3
- package/app/about/page.tsx +8 -9
- package/app/admin/page.tsx +7 -6
- package/app/admin/preview/[...path]/page.tsx +2 -1
- package/app/admin/ui/blocks/page.tsx +2 -1
- package/app/admin/ui/catalog/page.tsx +2 -1
- package/app/admin/ui/evals/page.tsx +2 -1
- package/app/admin/ui/page.tsx +2 -1
- package/app/blog/[...path]/page.tsx +7 -6
- package/app/blog/page.tsx +4 -2
- package/app/columns/[column]/[...path]/page.tsx +8 -7
- package/app/columns/page.tsx +12 -8
- package/app/components/AdminUiShell.tsx +4 -3
- package/app/components/ArticleMeasure.tsx +6 -3
- package/app/components/ArticleReferences.tsx +4 -1
- package/app/components/ArticleTags.tsx +3 -2
- package/app/components/BlogArchive.tsx +20 -16
- package/app/components/Comments.tsx +16 -10
- package/app/components/ContentEmptyState.tsx +29 -28
- package/app/components/ExternalEmbed.tsx +15 -14
- package/app/components/GardenContentGuide.tsx +10 -2
- package/app/components/GardenDocsNav.tsx +4 -2
- package/app/components/GiscusComments.tsx +2 -1
- package/app/components/GithubLiveOverview.tsx +10 -8
- package/app/components/GithubProfile.tsx +31 -21
- package/app/components/HandbookLangSwitch.tsx +8 -4
- package/app/components/HandbookView.tsx +92 -9
- package/app/components/MarkdownArticle.tsx +31 -7
- package/app/components/MermaidDiagram.tsx +4 -3
- package/app/components/SiteChrome.tsx +3 -3
- package/app/components/SiteFooter.tsx +1 -1
- package/app/components/SiteHeader.tsx +13 -4
- package/app/components/ThoughtCard.tsx +16 -14
- package/app/components/UiBlock.tsx +3 -2
- package/app/components/Webmentions.tsx +11 -11
- package/app/components/WidgetChart.tsx +8 -6
- package/app/components/garden/FilePreview.tsx +2 -2
- package/app/components/garden/GardenBlock.tsx +7 -0
- package/app/components/garden/GardenFile.tsx +2 -2
- package/app/components/garden/RefDemo.tsx +62 -0
- package/app/components/garden/blocks.tsx +56 -8
- package/app/components/garden/patterns.tsx +497 -0
- package/app/garden/[...slug]/page.tsx +3 -2
- package/app/garden/en/page.tsx +7 -7
- package/app/garden/page.tsx +3 -2
- package/app/globals.css +49 -1
- package/app/lib/a2-meta.ts +26 -3
- package/app/lib/content.ts +2 -2
- package/app/lib/garden-docs-meta.ts +24 -0
- package/app/lib/github-id.mjs +8 -0
- package/app/lib/handbook-switch.mjs +12 -0
- package/app/lib/i18n.ts +401 -2
- package/app/lib/rehype-field-tables.mjs +47 -0
- package/app/lib/remark-a2.mjs +18 -8
- package/app/lib/remark-link-card.mjs +22 -0
- package/app/lib/remark-ref-demos.mjs +42 -0
- package/app/lib/tag-filter.ts +9 -2
- package/app/not-found.tsx +32 -4
- package/app/page.tsx +17 -14
- package/app/rss.xml/route.ts +2 -1
- package/app/styles/a2.css +338 -0
- package/app/styles/shell.css +15 -2
- package/app/tags/page.tsx +7 -5
- package/app/thoughts/[slug]/page.tsx +2 -1
- package/app/thoughts/page/[n]/page.tsx +1 -1
- package/app/thoughts/page.tsx +2 -1
- package/app/ui-parity/page.tsx +38 -22
- package/bin/garden.mjs +129 -19
- package/docs/handbook/ai-writing.md +2 -2
- package/docs/handbook/assets/reading-layout-en.svg +26 -0
- package/docs/handbook/components/card.md +43 -22
- package/docs/handbook/components/chart.md +10 -26
- package/docs/handbook/components/choice.md +42 -0
- package/docs/handbook/components/compare.md +32 -17
- package/docs/handbook/components/figure.md +33 -46
- package/docs/handbook/components/file.md +10 -15
- package/docs/handbook/components/html.md +10 -30
- package/docs/handbook/components/interactive.md +9 -35
- package/docs/handbook/components/list.md +65 -0
- package/docs/handbook/components/relations.md +95 -0
- package/docs/handbook/components/stat.md +10 -28
- package/docs/handbook/components/timeline.md +13 -28
- package/docs/handbook/components.md +210 -19
- package/docs/handbook/config.md +5 -3
- package/docs/handbook/deploy.md +1 -1
- package/docs/handbook/design.md +20 -12
- package/docs/handbook/en/ai-writing.md +2 -2
- package/docs/handbook/en/components/card.md +42 -21
- package/docs/handbook/en/components/chart.md +10 -26
- package/docs/handbook/en/components/choice.md +42 -0
- package/docs/handbook/en/components/compare.md +30 -15
- package/docs/handbook/en/components/figure.md +29 -42
- package/docs/handbook/en/components/file.md +9 -14
- package/docs/handbook/en/components/html.md +9 -29
- package/docs/handbook/en/components/interactive.md +8 -34
- package/docs/handbook/en/components/list.md +65 -0
- package/docs/handbook/en/components/relations.md +95 -0
- package/docs/handbook/en/components/stat.md +9 -27
- package/docs/handbook/en/components/timeline.md +9 -24
- package/docs/handbook/en/components.md +211 -19
- package/docs/handbook/en/config.md +32 -30
- package/docs/handbook/en/deploy.md +12 -12
- package/docs/handbook/en/design.md +19 -12
- package/docs/handbook/en/index.md +2 -2
- package/docs/handbook/en/reference/card.md +85 -0
- package/docs/handbook/en/reference/chart.md +68 -0
- package/docs/handbook/en/reference/choice.md +69 -0
- package/docs/handbook/en/reference/compare.md +59 -0
- package/docs/handbook/en/reference/figure.md +84 -0
- package/docs/handbook/en/reference/file/checklist.md +17 -0
- package/docs/handbook/en/reference/file.md +50 -0
- package/docs/handbook/en/reference/html.md +62 -0
- package/docs/handbook/en/reference/interactive.md +71 -0
- package/docs/handbook/en/reference/list.md +72 -0
- package/docs/handbook/en/reference/relations.md +113 -0
- package/docs/handbook/en/reference/stat.md +60 -0
- package/docs/handbook/en/reference/timeline.md +56 -0
- package/docs/handbook/en/reference.md +26 -0
- package/docs/handbook/en/start.md +2 -2
- package/docs/handbook/en/workflow.md +6 -5
- package/docs/handbook/en/writing.md +3 -2
- package/docs/handbook/index.md +1 -1
- package/docs/handbook/reference/card.md +85 -0
- package/docs/handbook/reference/chart.md +68 -0
- package/docs/handbook/reference/choice.md +69 -0
- package/docs/handbook/reference/compare.md +59 -0
- package/docs/handbook/reference/figure.md +84 -0
- package/docs/handbook/reference/file/checklist.md +17 -0
- package/docs/handbook/reference/file.md +50 -0
- package/docs/handbook/reference/html.md +62 -0
- package/docs/handbook/reference/interactive.md +71 -0
- package/docs/handbook/reference/list.md +72 -0
- package/docs/handbook/reference/relations.md +113 -0
- package/docs/handbook/reference/stat.md +60 -0
- package/docs/handbook/reference/timeline.md +56 -0
- package/docs/handbook/reference.md +26 -0
- package/docs/handbook/start.md +1 -1
- package/docs/handbook/workflow.md +6 -5
- package/docs/handbook/writing.md +3 -2
- package/next.config.ts +6 -0
- package/package.json +2 -1
- package/public/images/garden/ref-card-book.svg +1 -0
- package/public/images/garden/ref-card-lamp.svg +1 -0
- package/scripts/build-content.mjs +37 -16
- package/scripts/favicon-ico.mjs +9 -6
- package/scripts/file-preview.mjs +10 -7
- package/scripts/handbook-allowlist.mjs +18 -0
- package/scripts/handbook-facts.mjs +305 -11
- package/scripts/link-assets.mjs +7 -0
- package/scripts/new-post.mjs +18 -9
- package/scripts/prepare-pages-output.mjs +30 -18
- package/scripts/redirects.mjs +17 -6
- package/templates/garden-blocks.md +67 -0
- package/templates/garden-snippets.plist +61 -0
- package/templates/site/Makefile.en +45 -0
- package/templates/site/README.en.md +15 -0
- package/templates/site/site.config.en.yaml +55 -0
- package/templates/skills-en/garden-interactive-chart/SKILL.md +84 -0
- package/templates/skills-en/garden-interactive-chart/references/shell.html +49 -0
- package/templates/skills-en/garden-interactive-chart/references/style.md +61 -0
- package/templates/skills-en/garden-post-review/SKILL.md +63 -0
- package/templates/skills-en/garden-ui/SKILL.md +118 -0
- package/templates/workflows/pages.en.yml +195 -0
- package/templates/workflows/pages.yml +195 -0
- package/ui/garden/catalog.mjs +205 -4
- package/ui/garden/check.mjs +6 -0
- package/ui/garden/file-block.mjs +8 -7
- package/ui/garden/html-lang.mjs +73 -1
- package/ui/garden/markdown-links.mjs +7 -4
- package/ui/garden/parse.mjs +45 -0
- package/ui/garden/relations.mjs +252 -0
- package/ui/garden/search-index.mjs +6 -4
- package/ui/garden/source-icon.mjs +54 -0
- package/ui/parity-fixtures.mjs +77 -0
- package/ui/preview/dist/garden-ui.esm.js +41 -4
- package/ui/preview/dist/garden-ui.iife.js +41 -4
- package/ui/prompt/rules.md +28 -1
- package/ui/react/components/chart.tsx +1 -1
- package/ui/react/components/display.tsx +1 -1
- package/ui/react/lang.tsx +4 -0
- package/ui/react/parity.tsx +8 -6
- package/worker/index.ts +8 -2
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
|
|
12
12
|
# 编译 Garden 交互块
|
|
13
13
|
|
|
14
|
-
作者在稿里用不带名字的 `garden` 围栏写一句想法,这个 skill 把它变成 `garden interactive` 或别的 `garden` 组件。交互组件和设计规则在 `npx garden ui prompt` 的输出里,
|
|
14
|
+
作者在稿里用不带名字的 `garden` 围栏写一句想法,这个 skill 把它变成 `garden interactive` 或别的 `garden` 组件。交互组件和设计规则在 `npx garden ui prompt` 的输出里,12 个 `garden` 组件的用途和示例在 `node_modules/hd-garden/templates/garden-blocks.md`(在引擎仓库里是 `templates/garden-blocks.md`),以这两份为准,不凭记忆写组件。
|
|
15
15
|
|
|
16
16
|
作者提交时,提交前钩子跑的是同一个 `npx garden ui check`。你用来找块、写回、自查的命令,和拦住作者提交的命令是同一套。
|
|
17
17
|
|
|
@@ -48,7 +48,7 @@ description: >-
|
|
|
48
48
|
- **写回正文**:一句话、一张表或一张 Mermaid 就能说清时。
|
|
49
49
|
- **写 `garden interactive`**:读者需要拖、点、翻才能看懂时。
|
|
50
50
|
- **写一段 `diff`**:想法紧跟在一段完整代码后面,说的是「和上一版比改了什么」时。按原话写出 diff,用一段普通 Markdown 写回(只有这一个 `diff` 围栏)。写回后它紧挨着那段代码,页面会合成「代码 / 对比上一版」切换;diff 里的新行要和上面的完整代码一致。
|
|
51
|
-
- **写别的 `garden`
|
|
51
|
+
- **写别的 `garden` 组件**:一张或多张资源卡(`card` / `cards`)、几个指标(`stat`)、两种做法并排或好坏对照(`compare` / `verdict: good|bad`)、一条时间线(`timeline`)、一张静态图表(`chart`)、一张分区结构图(`figure`)、一份下载文件(`file`)、3~6 个要素的关系图(`relations`)、紧凑图标列表或分步列表(`list`)、两三个选项的选择卡(`choice`)。字段与校验约束见 `npx garden ui prompt` 与 `templates/garden-blocks.md`。
|
|
52
52
|
- **写 `garden html`**:目录表达不了的画布动画、音频、物理模拟等。按 [garden-interactive-chart](../garden-interactive-chart/SKILL.md) 在同名目录写 HTML,用 `title` 写下判断。
|
|
53
53
|
|
|
54
54
|
4. 写 `garden interactive` 时:
|
package/.env.en.example
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Public blog canonical URL without a trailing slash. Used by GitHub Pages builds and the standalone admin studio.
|
|
2
|
+
SITE_URL=https://your-name.github.io
|
|
3
|
+
NEXT_PUBLIC_SITE_URL=https://your-name.github.io
|
|
4
|
+
|
|
5
|
+
# Standalone admin studio URL; static exports redirect /admin here.
|
|
6
|
+
ADMIN_URL=https://admin.example.com
|
|
7
|
+
|
|
8
|
+
# Optional: explicit GitHub OAuth callback URL when behind a reverse proxy or multiple domains.
|
|
9
|
+
# GITHUB_OAUTH_REDIRECT_URI=https://admin.example.com/api/auth/github/callback
|
|
10
|
+
|
|
11
|
+
# Comments are disabled by default. Open https://giscus.app, install the app, uncomment below, and fill in real values.
|
|
12
|
+
# The repository must be public (owner/name) with Discussions enabled.
|
|
13
|
+
# Also add the same four variables under GitHub Actions Variables for production builds.
|
|
14
|
+
# To publish without comments, set the Actions Variable GISCUS_DISABLED to 1.
|
|
15
|
+
# GISCUS_REPO=
|
|
16
|
+
# GISCUS_REPO_ID=
|
|
17
|
+
# GISCUS_CATEGORY=
|
|
18
|
+
# GISCUS_CATEGORY_ID=
|
|
19
|
+
|
|
20
|
+
# Webmention.io: set your domain (without https://) after registering a custom domain.
|
|
21
|
+
WEBMENTION_IO_DOMAIN=example.com
|
|
22
|
+
|
|
23
|
+
# Optional: override the default Webmention.io endpoint when self-hosting.
|
|
24
|
+
# WEBMENTION_ENDPOINT=https://example.com/webmention
|
|
25
|
+
|
|
26
|
+
# Optional: display exact commit counts, pinned repositories, and contribution graph on /about locally. Server-only.
|
|
27
|
+
# GITHUB_TOKEN=github_pat_xxx
|
|
28
|
+
|
|
29
|
+
# Optional: cookie-free analytics, such as Plausible.
|
|
30
|
+
# NEXT_PUBLIC_ANALYTICS_DOMAIN=example.com
|
|
31
|
+
# NEXT_PUBLIC_ANALYTICS_SCRIPT=https://plausible.io/js/script.js
|
|
32
|
+
|
|
33
|
+
# Optional: include drafts and future-dated posts during local builds.
|
|
34
|
+
# CONTENT_INCLUDE_DRAFTS=1
|
|
35
|
+
|
|
36
|
+
# Admin studio: GitHub OAuth App, allowed logins, and fine-grained PAT for writing to the content repository.
|
|
37
|
+
# Callback URL: https://admin.example.com/api/auth/github/callback
|
|
38
|
+
# CONTENT_REPOSITORY=owner/repository
|
|
39
|
+
# CONTENT_BRANCH=main
|
|
40
|
+
# CONTENT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxx
|
|
41
|
+
# GITHUB_OAUTH_CLIENT_ID=Ov23li_xxxxxxxxxxxx
|
|
42
|
+
# GITHUB_OAUTH_CLIENT_SECRET=xxxxxxxxxxxxxxxx
|
|
43
|
+
# GITHUB_SESSION_SECRET=use-a-random-string-at-least-32-characters-long
|
|
44
|
+
# ADMIN_GITHUB_LOGINS=your-github-login
|
|
45
|
+
|
|
46
|
+
# Admin media: Alibaba Cloud OSS. AccessKeys stay on the admin server and are never sent to the browser.
|
|
47
|
+
# OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
48
|
+
# OSS_BUCKET=your-public-media-bucket
|
|
49
|
+
# OSS_ACCESS_KEY_ID=LTAIxxxxxxxx
|
|
50
|
+
# OSS_ACCESS_KEY_SECRET=xxxxxxxx
|
|
51
|
+
# OSS_PREFIX=blog-media
|
|
52
|
+
# OSS_PUBLIC_BASE_URL=https://media.example.com
|
|
53
|
+
# OSS_MAX_UPLOAD_MB=200
|
|
54
|
+
|
|
55
|
+
# Admin UI studio: OpenAI-compatible endpoint. API keys stay on the admin server only.
|
|
56
|
+
# UI_MODEL_BASE_URL=https://api.openai.com/v1
|
|
57
|
+
# UI_MODEL_API_KEY=sk-xxxxxxxx
|
|
58
|
+
# UI_MODEL_NAME=gpt-5.6
|
|
59
|
+
# UI_DRAFT_RATE_LIMIT=60
|
|
60
|
+
# Optional: GitHub Enterprise or local mock API base URL.
|
|
61
|
+
# GITHUB_API_URL=https://api.github.com
|
|
62
|
+
|
|
63
|
+
# Content distribution: X uses a user access token with write permissions; other platforms use your webhook bridges.
|
|
64
|
+
# X_USER_ACCESS_TOKEN=xxxxxxxx
|
|
65
|
+
# CSDN_SYNC_WEBHOOK=https://publisher.example.com/csdn
|
|
66
|
+
# ZHIHU_SYNC_WEBHOOK=https://publisher.example.com/zhihu
|
|
67
|
+
# JUEJIN_SYNC_WEBHOOK=https://publisher.example.com/juejin
|
package/README.en.md
CHANGED
|
@@ -29,7 +29,7 @@ Where you want a component or interactive calculation, write an unnamed `garden`
|
|
|
29
29
|
```
|
|
30
30
|
````
|
|
31
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.
|
|
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`, `relations`, `list`, `choice`, or a `diff` block right below a code snippet), and writes only that fence back via `npx garden ui apply`. If you run `git commit` before compiling, the pre-commit hook runs `npx --no-install garden ui check --staged --quiet`, stops the commit, and prints a ready-to-paste fix prompt for your AI.
|
|
33
33
|
|
|
34
34
|
## 3. Common commands
|
|
35
35
|
|
|
@@ -48,7 +48,7 @@ npx garden ui catalog # Generate an offline component
|
|
|
48
48
|
|
|
49
49
|
Start `make dev` and open `/garden/en` (Chinese at `/garden`), or read the markdown sources directly:
|
|
50
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), [
|
|
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), [12 Built-in Components](docs/handbook/en/components.md), and [Checking & Shortcuts](docs/handbook/en/workflow.md).
|
|
52
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
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
54
|
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ make dev
|
|
|
31
31
|
```
|
|
32
32
|
````
|
|
33
33
|
|
|
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 的修复提示词。
|
|
34
|
+
让 Agent 按站点里的 `garden-ui` skill 处理这篇稿件:Agent 先用 `npx garden ui scan` 读取待编译块的哈希,再读 `npx garden ui prompt` 的组件规则,生成 `garden interactive`(或 `card`、`stat`、`compare`、`timeline`、`chart`、`figure`、`html`、`file`、`relations`、`list`、`choice` 等内置组件,或在完整代码下方生成 `diff`),最后用 `npx garden ui apply` 只替换该围栏。如果你在编译前直接 `git commit`,pre-commit 钩子会运行同一条 `npx --no-install garden ui check --staged --quiet` 拦下提交,并打印一段可直接粘贴给 AI 的修复提示词。
|
|
35
35
|
|
|
36
36
|
## 3. 常用命令
|
|
37
37
|
|
|
@@ -50,7 +50,7 @@ npx garden ui catalog # 在 .garden/ui-catalog.html 生
|
|
|
50
50
|
|
|
51
51
|
本地运行 `make dev` 后访问 `/garden`(英文版在 `/garden/en`),或直接阅读仓库内的文档:
|
|
52
52
|
|
|
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)、[
|
|
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)、[12 个内置组件](docs/handbook/components.md) 与 [语法检查](docs/handbook/workflow.md)。
|
|
54
54
|
- **[从零开始](docs/quick-start.md)** 与 **[GitHub Pages 部署](docs/github-pages.md)**:单公开仓库或「私有源码仓库 + 公开页面仓库」的 Actions 配置与 Giscus 评论接入。
|
|
55
55
|
- **[独立管理后台](docs/admin-service.md)**:可选的 `/admin` 服务(本地、Docker 或 Cloudflare Workers),支持网页发随想、阿里云 OSS 媒体直传与 `/admin/ui` 组件工作台。
|
|
56
56
|
|
package/app/a2/[slug]/page.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { TableOfContents } from "../../components/TableOfContents";
|
|
|
7
7
|
import { a2WordCounts } from "@garden/generated-content";
|
|
8
8
|
import { demoDateLabel, demoDateTime } from "../../lib/a2-meta";
|
|
9
9
|
import { extractHeadings } from "../../lib/content";
|
|
10
|
+
import { siteLang, ui } from "../../lib/i18n";
|
|
10
11
|
import { a2Slugs, readA2Article } from "../../lib/a2-preview";
|
|
11
12
|
import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../ui/garden/static-params.mjs";
|
|
12
13
|
|
|
@@ -37,9 +38,9 @@ export default async function A2ArticlePage({ params }: { params: Promise<{ slug
|
|
|
37
38
|
<ArticleTitle title={article.title.replace(/(互动版)$/u, "")} />
|
|
38
39
|
<ul className="meta article-meta">
|
|
39
40
|
<li>Hedon</li>
|
|
40
|
-
{article.date ? <li><time dateTime={demoDateTime(article.date)}>{demoDateLabel(article.date)}</time></li> : null}
|
|
41
|
-
<li
|
|
42
|
-
{article.original ? <li><a className="orig ext" href={article.original} target="_blank" rel="noopener noreferrer"
|
|
41
|
+
{article.date ? <li><time dateTime={demoDateTime(article.date)}>{demoDateLabel(article.date, siteLang)}</time></li> : null}
|
|
42
|
+
<li>{siteLang === "en" ? ui().wordCount(wordCount) : <>约 {wordCount.toLocaleString("en-US")} 字</>}</li>
|
|
43
|
+
{article.original ? <li><a className="orig ext" href={article.original} target="_blank" rel="noopener noreferrer">{siteLang === "en" ? "Original ↗" : "原文 ↗"}</a></li> : null}
|
|
43
44
|
</ul>
|
|
44
45
|
{article.cover ? (
|
|
45
46
|
<figure className="article-hero article-cover cover-card cover-img">
|
package/app/a2/page.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
import { siteHref } from "../lib/site-href";
|
|
3
3
|
import { a2Slugs, readA2Article } from "../lib/a2-preview";
|
|
4
|
+
import { ui } from "../lib/i18n";
|
|
4
5
|
|
|
5
6
|
export const metadata = {
|
|
6
|
-
title:
|
|
7
|
+
title: ui().a2IndexTitle,
|
|
7
8
|
robots: { index: false, follow: false },
|
|
8
9
|
};
|
|
9
10
|
|
|
@@ -12,8 +13,8 @@ export default function A2Index() {
|
|
|
12
13
|
return (
|
|
13
14
|
<main className="page-shell index-page">
|
|
14
15
|
<header className="sh-head">
|
|
15
|
-
<h1>
|
|
16
|
-
<p className="sh-deck"
|
|
16
|
+
<h1>{ui().a2IndexTitle}</h1>
|
|
17
|
+
<p className="sh-deck">{ui().a2IndexDeck}</p>
|
|
17
18
|
</header>
|
|
18
19
|
<ul className="sh-rows">
|
|
19
20
|
{articles.map((article) => (
|
package/app/about/page.tsx
CHANGED
|
@@ -5,16 +5,15 @@ import { PageIntro } from "../components/PageIntro";
|
|
|
5
5
|
import { siteConfig } from "../site.config";
|
|
6
6
|
import { siteHref } from "../lib/site-href";
|
|
7
7
|
import { pageAlternates } from "../lib/page-alternates";
|
|
8
|
+
import { ui } from "../lib/i18n";
|
|
8
9
|
|
|
10
|
+
const t = ui();
|
|
9
11
|
const about = siteConfig.pages.about;
|
|
10
|
-
const heading = about.heading ??
|
|
11
|
-
const paragraphs = about.paragraphs ??
|
|
12
|
-
"一名工程师。平时写代码,也把想清楚的问题记下来。",
|
|
13
|
-
"这里放文章、随想和专栏。",
|
|
14
|
-
];
|
|
12
|
+
const heading = about.heading ?? t.aboutFallbackHeading;
|
|
13
|
+
const paragraphs = about.paragraphs ?? t.aboutFallbackParagraphs;
|
|
15
14
|
const quote = about.quote ?? "";
|
|
16
|
-
const focus = about.focus ??
|
|
17
|
-
const cta = about.cta ?? { href: "/blog", label:
|
|
15
|
+
const focus = about.focus ?? t.aboutFallbackFocus;
|
|
16
|
+
const cta = about.cta ?? { href: "/blog", label: t.aboutFallbackCta };
|
|
18
17
|
|
|
19
18
|
export const metadata = {
|
|
20
19
|
title: { absolute: siteConfig.name },
|
|
@@ -26,7 +25,7 @@ export default function AboutPage() {
|
|
|
26
25
|
return (
|
|
27
26
|
<main className="page-shell about-page">
|
|
28
27
|
<PageIntro
|
|
29
|
-
eyebrow=
|
|
28
|
+
eyebrow={t.aboutEyebrow}
|
|
30
29
|
title={about.title}
|
|
31
30
|
subtitle={about.subtitle}
|
|
32
31
|
/>
|
|
@@ -40,7 +39,7 @@ export default function AboutPage() {
|
|
|
40
39
|
{quote ? <blockquote>{quote}</blockquote> : null}
|
|
41
40
|
</section>
|
|
42
41
|
<aside className="about-facts">
|
|
43
|
-
<span
|
|
42
|
+
<span>{t.aboutFocusHeading}</span>
|
|
44
43
|
<ul>
|
|
45
44
|
{focus.map((item) => (
|
|
46
45
|
<li key={item}>{item}</li>
|
package/app/admin/page.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { columns, posts, thoughts } from "../lib/content";
|
|
|
6
6
|
import { isContentRepositoryConfigured, listRepositoryThoughts } from "../lib/github-content";
|
|
7
7
|
import { getSiteUrl } from "../lib/site";
|
|
8
8
|
import { siteConfig } from "../site.config";
|
|
9
|
+
import { ui } from "../lib/i18n";
|
|
9
10
|
import { modelConfig, remainingDrafts } from "../lib/ui-model";
|
|
10
11
|
import { CATALOG_VERSION } from "../../ui/core/catalog.mjs";
|
|
11
12
|
import packageJson from "../../package.json";
|
|
@@ -13,7 +14,7 @@ import packageJson from "../../package.json";
|
|
|
13
14
|
export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
|
|
14
15
|
|
|
15
16
|
export const metadata = {
|
|
16
|
-
title:
|
|
17
|
+
title: ui().adminTitleMain,
|
|
17
18
|
robots: { index: false, follow: false },
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -27,17 +28,17 @@ export default async function AdminPage({
|
|
|
27
28
|
return (
|
|
28
29
|
<main className="admin-access-page">
|
|
29
30
|
<div className="admin-access-card">
|
|
30
|
-
<h1
|
|
31
|
-
<p
|
|
31
|
+
<h1>{ui().staticAdminHeading}</h1>
|
|
32
|
+
<p>{ui().staticAdminRootBody}</p>
|
|
32
33
|
<div className="admin-access-actions">
|
|
33
34
|
{adminUrl ? (
|
|
34
35
|
<a className="admin-github-login" href={adminUrl}>
|
|
35
|
-
|
|
36
|
+
{`${ui().staticAdminOpen} `}<ArrowSquareOut size={15} />
|
|
36
37
|
</a>
|
|
37
38
|
) : (
|
|
38
|
-
<p
|
|
39
|
+
<p>{ui().staticAdminNoUrl}</p>
|
|
39
40
|
)}
|
|
40
|
-
<Link href="/"
|
|
41
|
+
<Link href="/">{ui().staticAdminBack}</Link>
|
|
41
42
|
</div>
|
|
42
43
|
</div>
|
|
43
44
|
</main>
|
|
@@ -7,12 +7,13 @@ import { AdminUiShell, isStaticExport, requireAdminPage, StaticAdminNotice } fro
|
|
|
7
7
|
import { MarkdownArticle } from "../../../components/MarkdownArticle";
|
|
8
8
|
import { repositoryContentPath } from "../../../lib/repository-path.mjs";
|
|
9
9
|
import { readMarkdown, readText } from "../../../lib/ui-content";
|
|
10
|
+
import { ui } from "../../../lib/i18n";
|
|
10
11
|
import { formatInlineLink, replaceInlineLinks } from "../../../../ui/garden/markdown-destination.mjs";
|
|
11
12
|
import { STATIC_PLACEHOLDER, staticParams } from "../../../../ui/garden/static-params.mjs";
|
|
12
13
|
|
|
13
14
|
export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
|
|
14
15
|
export const dynamicParams = false;
|
|
15
|
-
export const metadata = { title:
|
|
16
|
+
export const metadata = { title: ui().adminTitlePreview, robots: { index: false, follow: false } };
|
|
16
17
|
|
|
17
18
|
/** Draft preview is served by the admin worker. The public static export has no paths here. */
|
|
18
19
|
export function generateStaticParams() {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { AdminBlocks } from "../../../components/AdminUiPanels";
|
|
2
2
|
import { AdminUiShell, isStaticExport, requireAdminPage, StaticAdminNotice } from "../../../components/AdminUiShell";
|
|
3
|
+
import { ui } from "../../../lib/i18n";
|
|
3
4
|
import { getSiteUrl } from "../../../lib/site";
|
|
4
5
|
|
|
5
6
|
export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
|
|
6
|
-
export const metadata = { title:
|
|
7
|
+
export const metadata = { title: ui().adminTitleBlocks, robots: { index: false, follow: false } };
|
|
7
8
|
|
|
8
9
|
export default async function UiBlocksPage() {
|
|
9
10
|
if (isStaticExport()) return <StaticAdminNotice />;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { AdminCatalog } from "../../../components/AdminUiPanels";
|
|
2
2
|
import { AdminUiShell, isStaticExport, requireAdminPage, StaticAdminNotice } from "../../../components/AdminUiShell";
|
|
3
|
+
import { ui } from "../../../lib/i18n";
|
|
3
4
|
import { recipeSources } from "../../../lib/ui-model";
|
|
4
5
|
|
|
5
6
|
export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
|
|
6
|
-
export const metadata = { title:
|
|
7
|
+
export const metadata = { title: ui().adminTitleCatalog, robots: { index: false, follow: false } };
|
|
7
8
|
|
|
8
9
|
export default async function UiCatalogPage() {
|
|
9
10
|
if (isStaticExport()) return <StaticAdminNotice />;
|
|
@@ -2,10 +2,11 @@ import { parse as parseYaml } from "yaml";
|
|
|
2
2
|
import casesSource from "../../../../ui/evals/cases.yaml?raw";
|
|
3
3
|
import { AdminEvals } from "../../../components/AdminUiPanels";
|
|
4
4
|
import { AdminUiShell, isStaticExport, requireAdminPage, StaticAdminNotice } from "../../../components/AdminUiShell";
|
|
5
|
+
import { ui } from "../../../lib/i18n";
|
|
5
6
|
import { modelConfig } from "../../../lib/ui-model";
|
|
6
7
|
|
|
7
8
|
export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
|
|
8
|
-
export const metadata = { title:
|
|
9
|
+
export const metadata = { title: ui().adminTitleEvals, robots: { index: false, follow: false } };
|
|
9
10
|
|
|
10
11
|
export default async function UiEvalsPage() {
|
|
11
12
|
if (isStaticExport()) return <StaticAdminNotice />;
|
package/app/admin/ui/page.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { AdminUiShell, isStaticExport, requireAdminPage, StaticAdminNotice } from "../../components/AdminUiShell";
|
|
2
2
|
import { UiStudio } from "../../components/UiStudio";
|
|
3
|
+
import { ui } from "../../lib/i18n";
|
|
3
4
|
import { modelConfig, recipeSources, remainingDrafts } from "../../lib/ui-model";
|
|
4
5
|
|
|
5
6
|
export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
|
|
6
|
-
export const metadata = { title:
|
|
7
|
+
export const metadata = { title: ui().adminTitleUi, robots: { index: false, follow: false } };
|
|
7
8
|
|
|
8
9
|
export default async function UiStudioPage() {
|
|
9
10
|
if (isStaticExport()) return <StaticAdminNotice />;
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from "../../lib/site";
|
|
29
29
|
import { githubUrl, siteConfig } from "../../site.config";
|
|
30
30
|
import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../ui/garden/static-params.mjs";
|
|
31
|
-
import { ui } from "../../lib/i18n";
|
|
31
|
+
import { siteLang, ui } from "../../lib/i18n";
|
|
32
32
|
|
|
33
33
|
export function generateStaticParams() {
|
|
34
34
|
return staticParams(posts.map((post) => ({ path: post.path.split("/") })), { path: [STATIC_PLACEHOLDER] });
|
|
@@ -86,12 +86,13 @@ export default async function PostPage({
|
|
|
86
86
|
const canonicalUrl = absoluteUrl(pathname, baseUrl);
|
|
87
87
|
const wordCount = estimateWordCount(post.content);
|
|
88
88
|
const { prev, next } = adjacentByDate(posts, (item: { path: string }) => item.path === post.path);
|
|
89
|
+
const t = ui();
|
|
89
90
|
|
|
90
91
|
return (
|
|
91
92
|
<main className="reading-page post-reading-page">
|
|
92
93
|
<article className="post-article h-entry">
|
|
93
94
|
<Link className="u-url sr-only" href={pathname}>
|
|
94
|
-
{post.title}
|
|
95
|
+
{siteLang === "en" ? t.permalinkLabel(post.title) : <>{post.title} 永久链接</>}
|
|
95
96
|
</Link>
|
|
96
97
|
<a
|
|
97
98
|
className="p-author h-card sr-only"
|
|
@@ -109,18 +110,18 @@ export default async function PostPage({
|
|
|
109
110
|
{formatDate(post.date)}
|
|
110
111
|
</time>
|
|
111
112
|
</li>
|
|
112
|
-
{post.updated ? <li
|
|
113
|
-
<li>{
|
|
113
|
+
{post.updated ? <li>{siteLang === "en" ? t.updatedOn(formatDate(post.updated)) : <>更新于 {formatDate(post.updated)}</>}</li> : null}
|
|
114
|
+
<li>{t.wordCount(wordCount)}</li>
|
|
114
115
|
{originalUrl(post) ? (
|
|
115
116
|
<li>
|
|
116
|
-
<a className="orig ext" href={originalUrl(post)} target="_blank" rel="noopener noreferrer"
|
|
117
|
+
<a className="orig ext" href={originalUrl(post)} target="_blank" rel="noopener noreferrer">{t.originalLink}</a>
|
|
117
118
|
</li>
|
|
118
119
|
) : null}
|
|
119
120
|
</ul>
|
|
120
121
|
<ArticleTags tags={post.tags} />
|
|
121
122
|
{post.cover && (
|
|
122
123
|
<figure className="article-hero article-cover cover-card cover-img">
|
|
123
|
-
<CoverImage className="u-photo" src={post.cover} image={post.coverImage} alt={
|
|
124
|
+
<CoverImage className="u-photo" src={post.cover} image={post.coverImage} alt={t.coverLabel(post.title)} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
|
|
124
125
|
</figure>
|
|
125
126
|
)}
|
|
126
127
|
</header>
|
package/app/blog/page.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { BlogArchive, BlogArchiveFallback } from "../components/BlogArchive";
|
|
|
4
4
|
import { posts } from "../lib/content";
|
|
5
5
|
import { archivePosts, archiveSeries } from "../lib/archive";
|
|
6
6
|
import { pageAlternates } from "../lib/page-alternates";
|
|
7
|
+
import { siteLang, ui } from "../lib/i18n";
|
|
7
8
|
|
|
8
9
|
export const metadata = {
|
|
9
10
|
title: siteDocumentTitle,
|
|
@@ -12,13 +13,14 @@ export const metadata = {
|
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export default function BlogIndex() {
|
|
16
|
+
const t = ui();
|
|
15
17
|
const list = archivePosts();
|
|
16
18
|
const series = archiveSeries();
|
|
17
19
|
return (
|
|
18
20
|
<main className="sh-main" id="content">
|
|
19
21
|
<div className="sh-head">
|
|
20
|
-
<h1 id="sh-title"
|
|
21
|
-
<p className="sh-deck"
|
|
22
|
+
<h1 id="sh-title">{t.blogHeading}</h1>
|
|
23
|
+
<p className="sh-deck">{siteLang === "en" ? t.blogDeck(posts.length) : <>阶段性的完整沉淀,关于工程、架构与技术思考。共 {posts.length} 篇,按年份归档。</>}</p>
|
|
22
24
|
</div>
|
|
23
25
|
<Suspense fallback={<BlogArchiveFallback posts={list} series={series} />}>
|
|
24
26
|
<BlogArchive posts={list} series={series} />
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
import { githubUrl, siteConfig } from "../../../site.config";
|
|
30
30
|
import { siteHref } from "../../../lib/site-href";
|
|
31
31
|
import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../../ui/garden/static-params.mjs";
|
|
32
|
-
import { ui } from "../../../lib/i18n";
|
|
32
|
+
import { columnStatusLabel, siteLang, ui } from "../../../lib/i18n";
|
|
33
33
|
|
|
34
34
|
export function generateStaticParams() {
|
|
35
35
|
return staticParams(
|
|
@@ -96,15 +96,16 @@ export default async function ColumnEntryPage({
|
|
|
96
96
|
const baseUrl = await getSiteUrl();
|
|
97
97
|
const pathname = columnHref(entry);
|
|
98
98
|
const canonicalUrl = absoluteUrl(pathname, baseUrl);
|
|
99
|
+
const t = ui();
|
|
99
100
|
|
|
100
101
|
return (
|
|
101
102
|
<main className="column-reading-page">
|
|
102
103
|
<aside className="series-nav">
|
|
103
104
|
<Link href={siteHref("/columns")} className="back-link">
|
|
104
105
|
<ArrowLeft size={15} />
|
|
105
|
-
|
|
106
|
+
{t.allColumnsBack}
|
|
106
107
|
</Link>
|
|
107
|
-
<p
|
|
108
|
+
<p>{t.seriesEyebrow}</p>
|
|
108
109
|
<h2>{entry.columnTitle}</h2>
|
|
109
110
|
<ol>
|
|
110
111
|
{listed.map((item) => (
|
|
@@ -113,12 +114,12 @@ export default async function ColumnEntryPage({
|
|
|
113
114
|
</li>
|
|
114
115
|
))}
|
|
115
116
|
</ol>
|
|
116
|
-
<small>{series.length}
|
|
117
|
+
<small>{ui().columnSideCount(series.length, columnStatusLabel(entry.columnStatus))}</small>
|
|
117
118
|
</aside>
|
|
118
119
|
|
|
119
120
|
<article className="article-column series-article h-entry">
|
|
120
121
|
<Link className="u-url sr-only" href={pathname}>
|
|
121
|
-
{entry.title}
|
|
122
|
+
{siteLang === "en" ? t.permalinkLabel(entry.title) : <>{entry.title} 永久链接</>}
|
|
122
123
|
</Link>
|
|
123
124
|
<a
|
|
124
125
|
className="p-author h-card sr-only"
|
|
@@ -137,13 +138,13 @@ export default async function ColumnEntryPage({
|
|
|
137
138
|
</time>
|
|
138
139
|
</li>
|
|
139
140
|
{originalUrl(entry) ? (
|
|
140
|
-
<li><a className="orig ext" href={originalUrl(entry)} target="_blank" rel="noopener noreferrer"
|
|
141
|
+
<li><a className="orig ext" href={originalUrl(entry)} target="_blank" rel="noopener noreferrer">{t.originalLink}</a></li>
|
|
141
142
|
) : null}
|
|
142
143
|
</ul>
|
|
143
144
|
<ArticleTags tags={entry.tags} />
|
|
144
145
|
{entry.cover && (
|
|
145
146
|
<figure className="article-hero article-cover cover-img">
|
|
146
|
-
<CoverImage className="u-photo" src={entry.cover} image={entry.coverImage} alt={`${entry.title} 封面`} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
|
|
147
|
+
<CoverImage className="u-photo" src={entry.cover} image={entry.coverImage} alt={siteLang === "en" ? t.coverLabel(entry.title) : `${entry.title} 封面`} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
|
|
147
148
|
</figure>
|
|
148
149
|
)}
|
|
149
150
|
</header>
|
package/app/columns/page.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { columnHref, columns } from "../lib/content";
|
|
|
4
4
|
import { ContentEmptyState } from "../components/ContentEmptyState";
|
|
5
5
|
import { siteConfig, siteDocumentTitle } from "../site.config";
|
|
6
6
|
import { pageAlternates } from "../lib/page-alternates";
|
|
7
|
+
import { columnStatusLabel, ui } from "../lib/i18n";
|
|
7
8
|
|
|
8
9
|
export const metadata = {
|
|
9
10
|
title: siteDocumentTitle,
|
|
@@ -23,6 +24,7 @@ function day(value: string) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export default function ColumnsPage() {
|
|
27
|
+
const t = ui();
|
|
26
28
|
const grouped = Array.from(
|
|
27
29
|
columns.reduce((result, entry) => {
|
|
28
30
|
const existing = result.get(entry.column ?? "");
|
|
@@ -39,19 +41,21 @@ export default function ColumnsPage() {
|
|
|
39
41
|
return (
|
|
40
42
|
<main className="sh-main" id="content">
|
|
41
43
|
<div className="sh-head">
|
|
42
|
-
<h1>{siteConfig.pages.columns.title ||
|
|
44
|
+
<h1>{siteConfig.pages.columns.title || t.homeColumns}</h1>
|
|
43
45
|
{siteConfig.pages.columns.subtitle ? <p className="sh-deck">{siteConfig.pages.columns.subtitle}</p> : null}
|
|
44
46
|
</div>
|
|
45
47
|
{grouped.length ? grouped.map(({ slug, first, entries }) => (
|
|
46
|
-
<section className="sh-series" aria-labelledby={slug} key={slug}>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
<section className={first.columnCover ? "sh-series" : "sh-series no-cover"} aria-labelledby={slug} key={slug}>
|
|
49
|
+
{first.columnCover ? (
|
|
50
|
+
<div className="cover" style={{ background: "var(--well)" }}>
|
|
51
|
+
<img className="column-feature-cover" src={first.columnCover} alt={first.columnCoverAlt ?? t.columnCoverAlt(first.columnTitle)} />
|
|
52
|
+
</div>
|
|
53
|
+
) : null}
|
|
50
54
|
<div className="body">
|
|
51
55
|
<h2 id={slug}>{first.columnTitle}</h2>
|
|
52
|
-
<p className="count"
|
|
53
|
-
<p className="desc">{first.columnDescription ?? first.description ??
|
|
54
|
-
<ol aria-label={
|
|
56
|
+
<p className="count">{t.columnReadingList(entries.length, columnStatusLabel(first.columnStatus))}</p>
|
|
57
|
+
<p className="desc">{first.columnDescription ?? first.description ?? t.columnFallbackDesc}</p>
|
|
58
|
+
<ol aria-label={t.columnNewestFirst(first.columnTitle)}>
|
|
55
59
|
{entries.map((entry) => (
|
|
56
60
|
<li key={entry.path}>
|
|
57
61
|
<Link href={columnHref(entry)}>{entry.title}</Link>
|
|
@@ -2,6 +2,7 @@ import Link from "next/link";
|
|
|
2
2
|
import { redirect } from "next/navigation";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import { getAdminUser } from "../lib/admin-auth";
|
|
5
|
+
import { ui } from "../lib/i18n";
|
|
5
6
|
|
|
6
7
|
const SECTIONS = [
|
|
7
8
|
{ href: "/admin", label: "总览" },
|
|
@@ -23,9 +24,9 @@ export function StaticAdminNotice() {
|
|
|
23
24
|
return (
|
|
24
25
|
<main className="admin-access-page">
|
|
25
26
|
<div className="admin-access-card">
|
|
26
|
-
<h1
|
|
27
|
-
<p
|
|
28
|
-
<Link href="/"
|
|
27
|
+
<h1>{ui().staticAdminHeading}</h1>
|
|
28
|
+
<p>{ui().staticAdminBody}</p>
|
|
29
|
+
<Link href="/">{ui().staticAdminBack}</Link>
|
|
29
30
|
</div>
|
|
30
31
|
</main>
|
|
31
32
|
);
|
|
@@ -92,8 +92,11 @@ export function ArticleMeasure() {
|
|
|
92
92
|
const articleBox = (article.querySelector(".markdown-body") ?? article).getBoundingClientRect();
|
|
93
93
|
const viewport = root.clientWidth;
|
|
94
94
|
let docMaxRoom: number | null = null;
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// A left rail (the doc sidebar, or a column's article list) bounds the
|
|
96
|
+
// breakout on both sides, so wide content never runs under it.
|
|
97
|
+
const columnNav = docPage ? null : document.querySelector<HTMLElement>(".series-nav");
|
|
98
|
+
if (docPage || columnNav) {
|
|
99
|
+
const sidebar = docPage ? docPage.querySelector<HTMLElement>(".gdoc-sidebar") : columnNav;
|
|
97
100
|
let leftLimit = 24;
|
|
98
101
|
if (sidebar && getComputedStyle(sidebar).position === "sticky") {
|
|
99
102
|
const sbBox = sidebar.getBoundingClientRect();
|
|
@@ -102,7 +105,7 @@ export function ArticleMeasure() {
|
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
let rightLimit = viewport - 24;
|
|
105
|
-
if (toc && getComputedStyle(toc).display !== "none") {
|
|
108
|
+
if (docPage && toc && getComputedStyle(toc).display !== "none") {
|
|
106
109
|
const tocBox = toc.getBoundingClientRect();
|
|
107
110
|
if (tocBox.width > 0 && tocBox.left >= articleBox.right - 1) {
|
|
108
111
|
rightLimit = tocBox.left - translatedX(toc) - 16;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { referenceColumns, referenceDomain } from "../../ui/garden/references.mjs";
|
|
2
|
+
import { ui, type UiLang } from "../lib/i18n";
|
|
2
3
|
|
|
3
4
|
type ReferenceItem = {
|
|
4
5
|
title: string;
|
|
@@ -8,8 +9,10 @@ type ReferenceItem = {
|
|
|
8
9
|
|
|
9
10
|
export function ArticleReferences({
|
|
10
11
|
references,
|
|
12
|
+
lang,
|
|
11
13
|
}: {
|
|
12
14
|
references?: ReferenceItem[];
|
|
15
|
+
lang?: UiLang;
|
|
13
16
|
}) {
|
|
14
17
|
if (!references?.length) return null;
|
|
15
18
|
const split = referenceColumns(references.length) === 2;
|
|
@@ -19,7 +22,7 @@ export function ArticleReferences({
|
|
|
19
22
|
className={split ? "article-references is-split" : "article-references"}
|
|
20
23
|
aria-labelledby="article-references-heading"
|
|
21
24
|
>
|
|
22
|
-
<h2 id="article-references-heading"
|
|
25
|
+
<h2 id="article-references-heading">{ui(lang).referencesHeading}</h2>
|
|
23
26
|
<ol>
|
|
24
27
|
{references.map((item, index) => {
|
|
25
28
|
const domain = item.domain ?? referenceDomain(item.url);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
import { articleTagHref } from "../lib/tag-filter";
|
|
3
3
|
import { siteHref } from "../lib/site-href";
|
|
4
|
+
import { ui, type UiLang } from "../lib/i18n";
|
|
4
5
|
|
|
5
6
|
/** Tags under an article title. Link, so following one is a client navigation like the rest of the site. */
|
|
6
|
-
export function ArticleTags({ tags }: { tags?: string[] }) {
|
|
7
|
+
export function ArticleTags({ tags, lang }: { tags?: string[]; lang?: UiLang }) {
|
|
7
8
|
const list = (tags ?? []).filter(Boolean);
|
|
8
9
|
if (!list.length) return null;
|
|
9
10
|
return (
|
|
10
|
-
<ul className="article-tagrow" aria-label=
|
|
11
|
+
<ul className="article-tagrow" aria-label={ui(lang).articleTagsLabel}>
|
|
11
12
|
{list.map((tag) => (
|
|
12
13
|
<li key={tag}>
|
|
13
14
|
<Link className="tag-chip p-category" href={siteHref(articleTagHref(tag))}>
|