hd-garden 0.7.0 → 0.8.1

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 (220) hide show
  1. package/.agents/skills/garden-ui/SKILL.md +26 -19
  2. package/.env.en.example +67 -0
  3. package/README.en.md +57 -0
  4. package/README.md +31 -46
  5. package/app/a2/[slug]/page.tsx +4 -3
  6. package/app/a2/page.tsx +4 -3
  7. package/app/about/page.tsx +8 -9
  8. package/app/admin/page.tsx +7 -6
  9. package/app/admin/preview/[...path]/page.tsx +2 -1
  10. package/app/admin/ui/blocks/page.tsx +2 -1
  11. package/app/admin/ui/catalog/page.tsx +2 -1
  12. package/app/admin/ui/evals/page.tsx +2 -1
  13. package/app/admin/ui/page.tsx +2 -1
  14. package/app/blog/[...path]/page.tsx +14 -5
  15. package/app/blog/page.tsx +4 -2
  16. package/app/columns/[column]/[...path]/page.tsx +17 -18
  17. package/app/columns/page.tsx +12 -8
  18. package/app/components/AdminUiShell.tsx +4 -3
  19. package/app/components/ArticlePager.tsx +32 -0
  20. package/app/components/ArticleReferences.tsx +4 -1
  21. package/app/components/ArticleTags.tsx +3 -2
  22. package/app/components/BlogArchive.tsx +20 -16
  23. package/app/components/Comments.tsx +17 -10
  24. package/app/components/ContentEmptyState.tsx +29 -28
  25. package/app/components/ExternalEmbed.tsx +15 -14
  26. package/app/components/FeedMore.tsx +70 -0
  27. package/app/components/GardenContentGuide.tsx +10 -2
  28. package/app/components/GardenDocsNav.tsx +29 -22
  29. package/app/components/GiscusComments.tsx +2 -1
  30. package/app/components/GithubLiveOverview.tsx +10 -8
  31. package/app/components/GithubProfile.tsx +31 -21
  32. package/app/components/HandbookLangRedirect.tsx +30 -0
  33. package/app/components/HandbookLangSwitch.tsx +53 -0
  34. package/app/components/HandbookView.tsx +270 -0
  35. package/app/components/MarkdownArticle.tsx +39 -9
  36. package/app/components/MarkdownImage.tsx +6 -4
  37. package/app/components/SiteChrome.tsx +11 -2
  38. package/app/components/SiteFooter.tsx +6 -4
  39. package/app/components/SiteHeader.tsx +38 -25
  40. package/app/components/TableOfContents.tsx +15 -9
  41. package/app/components/ThoughtCard.tsx +16 -14
  42. package/app/components/ThoughtFeed.tsx +41 -0
  43. package/app/components/UiBlock.tsx +3 -2
  44. package/app/components/UiLang.tsx +18 -0
  45. package/app/components/Webmentions.tsx +13 -12
  46. package/app/components/WidgetChart.tsx +8 -6
  47. package/app/components/garden/FilePreview.tsx +14 -12
  48. package/app/components/garden/GardenBlock.tsx +10 -1
  49. package/app/components/garden/GardenFile.tsx +10 -8
  50. package/app/components/garden/RefDemo.tsx +62 -0
  51. package/app/components/garden/blocks.tsx +90 -32
  52. package/app/components/garden/chrome.tsx +20 -13
  53. package/app/components/garden/patterns.tsx +497 -0
  54. package/app/garden/[...slug]/page.tsx +11 -130
  55. package/app/garden/en/[...slug]/page.tsx +28 -0
  56. package/app/garden/en/page.tsx +16 -0
  57. package/app/garden/page.tsx +8 -74
  58. package/app/garden-modules.d.ts +3 -0
  59. package/app/globals.css +10 -0
  60. package/app/layout.tsx +2 -4
  61. package/app/lib/a2-meta.ts +26 -3
  62. package/app/lib/content.ts +13 -3
  63. package/app/lib/garden-docs-meta.ts +84 -5
  64. package/app/lib/github-id.mjs +8 -0
  65. package/app/lib/handbook-path.ts +11 -0
  66. package/app/lib/handbook-switch.mjs +12 -0
  67. package/app/lib/i18n.ts +621 -0
  68. package/app/lib/page-alternates.ts +2 -2
  69. package/app/lib/rehype-field-tables.mjs +47 -0
  70. package/app/lib/remark-a2.mjs +18 -8
  71. package/app/lib/remark-link-card.mjs +22 -0
  72. package/app/lib/remark-ref-demos.mjs +42 -0
  73. package/app/lib/tag-filter.ts +9 -2
  74. package/app/not-found.tsx +34 -4
  75. package/app/page.tsx +17 -14
  76. package/app/rss.xml/route.ts +2 -1
  77. package/app/site.config.ts +1 -1
  78. package/app/styles/a2.css +439 -32
  79. package/app/styles/shell.css +21 -2
  80. package/app/tags/page.tsx +7 -5
  81. package/app/thoughts/[slug]/page.tsx +2 -1
  82. package/app/thoughts/page/[n]/page.tsx +44 -0
  83. package/app/thoughts/page.tsx +4 -7
  84. package/app/ui-parity/page.tsx +38 -22
  85. package/bin/garden.mjs +133 -19
  86. package/docs/handbook/ai-writing.md +99 -0
  87. package/docs/handbook/assets/reading-layout-en.svg +26 -0
  88. package/docs/handbook/components/card.md +43 -20
  89. package/docs/handbook/components/chart.md +10 -24
  90. package/docs/handbook/components/choice.md +42 -0
  91. package/docs/handbook/components/compare.md +34 -21
  92. package/docs/handbook/components/figure.md +33 -44
  93. package/docs/handbook/components/file.md +10 -13
  94. package/docs/handbook/components/html.md +10 -28
  95. package/docs/handbook/components/interactive.md +9 -33
  96. package/docs/handbook/components/list.md +65 -0
  97. package/docs/handbook/components/relations.md +95 -0
  98. package/docs/handbook/components/stat.md +10 -26
  99. package/docs/handbook/components/timeline.md +13 -26
  100. package/docs/handbook/components.md +210 -19
  101. package/docs/handbook/config.md +7 -5
  102. package/docs/handbook/deploy.md +2 -2
  103. package/docs/handbook/design.md +23 -15
  104. package/docs/handbook/en/ai-writing.md +99 -0
  105. package/docs/handbook/en/components/card.md +67 -0
  106. package/docs/handbook/en/components/chart.md +38 -0
  107. package/docs/handbook/en/components/choice.md +42 -0
  108. package/docs/handbook/en/components/compare.md +53 -0
  109. package/docs/handbook/en/components/figure.md +59 -0
  110. package/docs/handbook/en/components/file/checklist.md +17 -0
  111. package/docs/handbook/en/components/file.md +29 -0
  112. package/docs/handbook/en/components/html.md +49 -0
  113. package/docs/handbook/en/components/interactive.md +41 -0
  114. package/docs/handbook/en/components/list.md +65 -0
  115. package/docs/handbook/en/components/relations.md +95 -0
  116. package/docs/handbook/en/components/stat.md +39 -0
  117. package/docs/handbook/en/components/timeline.md +37 -0
  118. package/docs/handbook/en/components.md +222 -0
  119. package/docs/handbook/en/config.md +127 -0
  120. package/docs/handbook/en/deploy.md +273 -0
  121. package/docs/handbook/en/design.md +88 -0
  122. package/docs/handbook/en/index.md +192 -0
  123. package/docs/handbook/en/reference/card.md +85 -0
  124. package/docs/handbook/en/reference/chart.md +68 -0
  125. package/docs/handbook/en/reference/choice.md +69 -0
  126. package/docs/handbook/en/reference/compare.md +59 -0
  127. package/docs/handbook/en/reference/figure.md +84 -0
  128. package/docs/handbook/en/reference/file/checklist.md +17 -0
  129. package/docs/handbook/en/reference/file.md +50 -0
  130. package/docs/handbook/en/reference/html.md +62 -0
  131. package/docs/handbook/en/reference/interactive.md +71 -0
  132. package/docs/handbook/en/reference/list.md +72 -0
  133. package/docs/handbook/en/reference/relations.md +113 -0
  134. package/docs/handbook/en/reference/stat.md +60 -0
  135. package/docs/handbook/en/reference/timeline.md +56 -0
  136. package/docs/handbook/en/reference.md +26 -0
  137. package/docs/handbook/en/start.md +92 -0
  138. package/docs/handbook/en/workflow.md +82 -0
  139. package/docs/handbook/en/writing.md +157 -0
  140. package/docs/handbook/index.md +99 -4
  141. package/docs/handbook/reference/card.md +85 -0
  142. package/docs/handbook/reference/chart.md +68 -0
  143. package/docs/handbook/reference/choice.md +69 -0
  144. package/docs/handbook/reference/compare.md +59 -0
  145. package/docs/handbook/reference/figure.md +84 -0
  146. package/docs/handbook/reference/file/checklist.md +17 -0
  147. package/docs/handbook/reference/file.md +50 -0
  148. package/docs/handbook/reference/html.md +62 -0
  149. package/docs/handbook/reference/interactive.md +71 -0
  150. package/docs/handbook/reference/list.md +72 -0
  151. package/docs/handbook/reference/relations.md +113 -0
  152. package/docs/handbook/reference/stat.md +60 -0
  153. package/docs/handbook/reference/timeline.md +56 -0
  154. package/docs/handbook/reference.md +26 -0
  155. package/docs/handbook/start.md +1 -1
  156. package/docs/handbook/workflow.md +6 -5
  157. package/docs/handbook/writing.md +21 -12
  158. package/next.config.ts +6 -0
  159. package/package.json +2 -1
  160. package/public/images/garden/ref-card-book.svg +1 -0
  161. package/public/images/garden/ref-card-lamp.svg +1 -0
  162. package/scripts/build-content.mjs +57 -16
  163. package/scripts/favicon-ico.mjs +9 -6
  164. package/scripts/file-preview.mjs +10 -7
  165. package/scripts/handbook-allowlist.mjs +18 -0
  166. package/scripts/handbook-facts.mjs +339 -11
  167. package/scripts/html-lang.mjs +36 -0
  168. package/scripts/link-assets.mjs +7 -0
  169. package/scripts/new-post.mjs +18 -9
  170. package/scripts/prepare-pages-output.mjs +30 -18
  171. package/scripts/redirects.mjs +17 -6
  172. package/scripts/static-404.mjs +25 -0
  173. package/scripts/ui-cli.mjs +14 -8
  174. package/templates/garden-blocks.md +67 -0
  175. package/templates/garden-snippets.plist +61 -0
  176. package/templates/site/Makefile.en +45 -0
  177. package/templates/site/README.en.md +15 -0
  178. package/templates/site/site.config.en.yaml +55 -0
  179. package/templates/site/site.config.yaml +2 -0
  180. package/templates/skills-en/garden-interactive-chart/SKILL.md +84 -0
  181. package/templates/skills-en/garden-interactive-chart/references/shell.html +49 -0
  182. package/templates/skills-en/garden-interactive-chart/references/style.md +61 -0
  183. package/templates/skills-en/garden-post-review/SKILL.md +63 -0
  184. package/templates/skills-en/garden-ui/SKILL.md +118 -0
  185. package/templates/workflows/pages.en.yml +195 -0
  186. package/templates/workflows/pages.yml +195 -0
  187. package/ui/core/apply.mjs +22 -4
  188. package/ui/core/cli-locale.mjs +67 -0
  189. package/ui/core/fence.mjs +18 -3
  190. package/ui/core/fix-prompt.mjs +18 -15
  191. package/ui/core/garden-fence.mjs +64 -0
  192. package/ui/core/scan.mjs +19 -3
  193. package/ui/garden/adjacent.mjs +21 -0
  194. package/ui/garden/catalog.mjs +205 -4
  195. package/ui/garden/check.mjs +12 -0
  196. package/ui/garden/file-block.mjs +8 -7
  197. package/ui/garden/html-lang.mjs +88 -0
  198. package/ui/garden/markdown-links.mjs +7 -4
  199. package/ui/garden/paging.mjs +30 -0
  200. package/ui/garden/parse.mjs +45 -0
  201. package/ui/garden/relations.mjs +252 -0
  202. package/ui/garden/search-index.mjs +6 -4
  203. package/ui/garden/source-icon.mjs +54 -0
  204. package/ui/parity-fixtures.mjs +77 -0
  205. package/ui/preview/dist/garden-ui.css +9 -9
  206. package/ui/preview/dist/garden-ui.esm.js +206 -169
  207. package/ui/preview/dist/garden-ui.iife.js +206 -169
  208. package/ui/prompt/rules.md +28 -1
  209. package/ui/react/components/chart.tsx +8 -5
  210. package/ui/react/components/display.tsx +7 -4
  211. package/ui/react/components/inputs.tsx +6 -3
  212. package/ui/react/components/layout.tsx +6 -4
  213. package/ui/react/components/media.tsx +7 -4
  214. package/ui/react/components/sandbox.tsx +3 -1
  215. package/ui/react/index.tsx +11 -7
  216. package/ui/react/lang.tsx +89 -0
  217. package/ui/react/parity.tsx +8 -6
  218. package/ui/react/runtime.tsx +3 -1
  219. package/ui/react/ui.css +9 -9
  220. package/worker/index.ts +14 -1
@@ -0,0 +1,270 @@
1
+ import Link from "next/link";
2
+ import type { Metadata } from "next";
3
+ import { ArrowLeft, ArrowRight } from "@phosphor-icons/react/ssr";
4
+ import { ArticleMeasure } from "./ArticleMeasure";
5
+ import { ArticleReferences } from "./ArticleReferences";
6
+ import { ArticleTitle } from "./ArticleTitle";
7
+ import { GardenDocsNav } from "./GardenDocsNav";
8
+ import { HandbookLangRedirect } from "./HandbookLangRedirect";
9
+ import { MarkdownArticle } from "./MarkdownArticle";
10
+ import { TableOfContents } from "./TableOfContents";
11
+ import { extractHeadings, handbookDocs, handbookIndexDoc, hasHandbookPage, type GardenDocEntry } from "../lib/content";
12
+ import { pageAlternates } from "../lib/page-alternates";
13
+ import { absoluteUrl, getSiteUrl } from "../lib/site";
14
+ import { siteHref } from "../lib/site-href";
15
+ import {
16
+ COMPONENT_BADGE,
17
+ HANDBOOK_SECTIONS,
18
+ HANDBOOK_UI,
19
+ handbookPath,
20
+ isReferencePath,
21
+ orderedHandbookDocs,
22
+ sectionTitle,
23
+ toHandbookNavItems,
24
+ type HandbookLang,
25
+ } from "../lib/garden-docs-meta";
26
+ import { UI_STRINGS, siteLang } from "../lib/i18n";
27
+ import { siteConfig } from "../site.config";
28
+
29
+ const other = (lang: HandbookLang): HandbookLang => (lang === "en" ? "zh" : "en");
30
+ const hreflang = (lang: HandbookLang) => (lang === "en" ? "en" : "zh-CN");
31
+
32
+ /** canonical + hreflang links; the counterpart is listed only when it exists. */
33
+ export function handbookLanguages(lang: HandbookLang, docPath: string) {
34
+ if (siteLang === "en") return undefined;
35
+ if (!hasHandbookPage(other(lang), docPath)) return undefined;
36
+ const zh = siteHref(handbookPath("zh", docPath));
37
+ const en = siteHref(handbookPath("en", docPath));
38
+ return { "zh-CN": zh, en, "x-default": zh };
39
+ }
40
+
41
+ export const handbookAlternates = (lang: HandbookLang, docPath: string) =>
42
+ pageAlternates(handbookPath(lang, docPath), handbookLanguages(lang, docPath));
43
+
44
+ export async function handbookOverviewMetadata(lang: HandbookLang, indexDoc: GardenDocEntry | null | undefined) {
45
+ const t = HANDBOOK_UI[lang];
46
+ const baseUrl = await getSiteUrl();
47
+ const pathname = siteHref(handbookPath(lang, "index"));
48
+ const description = indexDoc?.description ?? "";
49
+ const fullTitle = `${t.name} · ${siteConfig.name}`;
50
+ const imageAlt =
51
+ lang === "en"
52
+ ? `${siteConfig.name} · ${indexDoc?.title || t.name}`
53
+ : `${siteConfig.name} · ${siteConfig.pages.home.title.replace("\n", "")}`;
54
+ return {
55
+ title: t.name,
56
+ description,
57
+ alternates: pageAlternates(handbookPath(lang), handbookLanguages(lang, "index")),
58
+ openGraph: {
59
+ type: "website",
60
+ url: absoluteUrl(pathname, baseUrl),
61
+ title: fullTitle,
62
+ description,
63
+ locale: lang === "en" ? "en_US" : "zh_CN",
64
+ images: [
65
+ {
66
+ url: "/og.png",
67
+ width: 1731,
68
+ height: 909,
69
+ alt: imageAlt,
70
+ },
71
+ ],
72
+ },
73
+ twitter: {
74
+ card: "summary_large_image",
75
+ title: fullTitle,
76
+ description,
77
+ images: ["/og.png"],
78
+ },
79
+ } satisfies Metadata;
80
+ }
81
+
82
+ export async function handbookDocMetadata(lang: HandbookLang, doc: GardenDocEntry | undefined) {
83
+ if (!doc) {
84
+ const copy = UI_STRINGS[lang];
85
+ const fullTitle = `${copy.notFoundTitle} · ${siteConfig.name}`;
86
+ return {
87
+ title: copy.notFoundTitle,
88
+ description: copy.notFoundDeck,
89
+ openGraph: {
90
+ type: "website",
91
+ title: fullTitle,
92
+ description: copy.notFoundDeck,
93
+ locale: lang === "en" ? "en_US" : "zh_CN",
94
+ images: [
95
+ {
96
+ url: "/og.png",
97
+ width: 1731,
98
+ height: 909,
99
+ alt: fullTitle,
100
+ },
101
+ ],
102
+ },
103
+ twitter: {
104
+ card: "summary_large_image",
105
+ title: fullTitle,
106
+ description: copy.notFoundDeck,
107
+ images: ["/og.png"],
108
+ },
109
+ } satisfies Metadata;
110
+ }
111
+ const t = HANDBOOK_UI[lang];
112
+ const baseUrl = await getSiteUrl();
113
+ const pathname = siteHref(handbookPath(lang, doc.path));
114
+ const docTitle = `${doc.title} · ${t.name}`;
115
+ return {
116
+ title: docTitle,
117
+ description: doc.description,
118
+ alternates: handbookAlternates(lang, doc.path),
119
+ openGraph: {
120
+ type: "article",
121
+ url: absoluteUrl(pathname, baseUrl),
122
+ title: docTitle,
123
+ description: doc.description,
124
+ locale: lang === "en" ? "en_US" : "zh_CN",
125
+ images: [
126
+ {
127
+ url: "/og.png",
128
+ width: 1731,
129
+ height: 909,
130
+ alt: `${siteConfig.name} · ${docTitle}`,
131
+ },
132
+ ],
133
+ },
134
+ twitter: {
135
+ card: "summary_large_image",
136
+ title: docTitle,
137
+ description: doc.description,
138
+ images: ["/og.png"],
139
+ },
140
+ } satisfies Metadata;
141
+ }
142
+
143
+ export function HandbookOverview({ lang }: { lang: HandbookLang }) {
144
+ const t = HANDBOOK_UI[lang];
145
+ const docs = handbookDocs(lang);
146
+ const indexDoc = handbookIndexDoc(lang);
147
+ const navItems = toHandbookNavItems(docs);
148
+ const firstDoc = orderedHandbookDocs(docs)[0] ?? null;
149
+ const content = indexDoc?.content ?? "";
150
+ const headings = extractHeadings(content);
151
+ // The site config title/subtitle is written in the site's own language.
152
+ const configured = lang === siteLang ? siteConfig.pages.garden : { title: "", subtitle: "" };
153
+ const title = configured.title?.trim() || indexDoc?.title || t.name;
154
+ const subtitle = configured.subtitle?.trim() || indexDoc?.description || "";
155
+
156
+ return (
157
+ <main className="column-reading-page garden-doc-page" lang={hreflang(lang)}>
158
+ <HandbookLangRedirect lang={lang} />
159
+ <GardenDocsNav docs={navItems} currentPath="" lang={lang} />
160
+
161
+ <article className="article-column garden-doc-article h-entry">
162
+ <header className="gdoc-article-head">
163
+ <div className="gdoc-eyebrow">
164
+ <span>{t.name}</span>
165
+ <span aria-hidden="true">/</span>
166
+ <span>{t.overview}</span>
167
+ </div>
168
+ <ArticleTitle title={title} />
169
+ {subtitle ? <p className="gdoc-article-deck p-summary">{subtitle}</p> : null}
170
+ </header>
171
+
172
+ <ArticleMeasure />
173
+
174
+ <div className="e-content">
175
+ <MarkdownArticle content={content} lang={lang} fieldTables handbook />
176
+ </div>
177
+
178
+ <ArticleReferences references={indexDoc?.references} lang={lang} />
179
+
180
+ {firstDoc ? (
181
+ <nav className="gdoc-pager" aria-label={t.pager}>
182
+ <Link href={siteHref(handbookPath(lang, firstDoc.path))} className="gdoc-pager-card is-next">
183
+ <span className="gdoc-pager-dir">
184
+ {t.next}
185
+ <ArrowRight size={14} aria-hidden="true" />
186
+ </span>
187
+ <strong>{firstDoc.title}</strong>
188
+ </Link>
189
+ </nav>
190
+ ) : null}
191
+ </article>
192
+
193
+ <TableOfContents headings={headings} label={t.toc} lang={lang} />
194
+ </main>
195
+ );
196
+ }
197
+
198
+ export function HandbookDoc({ lang, doc }: { lang: HandbookLang; doc: GardenDocEntry }) {
199
+ const t = HANDBOOK_UI[lang];
200
+ const docs = handbookDocs(lang);
201
+ const ordered = orderedHandbookDocs(docs);
202
+ const navItems = toHandbookNavItems(docs);
203
+ const index = ordered.findIndex((item) => item.path === doc.path);
204
+ const prev = index > 0 ? ordered[index - 1] : null;
205
+ const next = index >= 0 && index < ordered.length - 1 ? ordered[index + 1] : null;
206
+ const headings = extractHeadings(doc.content);
207
+ const sectionMeta = HANDBOOK_SECTIONS.find((sec) => sec.id === doc.section);
208
+ const componentBadge = COMPONENT_BADGE[doc.path];
209
+
210
+ return (
211
+ <main className={`column-reading-page garden-doc-page${isReferencePath(doc.path) ? " is-reference" : ""}`} lang={hreflang(lang)}>
212
+ <HandbookLangRedirect lang={lang} />
213
+ <GardenDocsNav docs={navItems} currentPath={doc.path} lang={lang} />
214
+
215
+ <article className="article-column garden-doc-article h-entry">
216
+ <header className="gdoc-article-head">
217
+ <div className="gdoc-eyebrow">
218
+ <Link href={siteHref(handbookPath(lang))} className="gdoc-eyebrow-root">
219
+ {t.name}
220
+ </Link>
221
+ <span aria-hidden="true">/</span>
222
+ <span>{sectionTitle(sectionMeta, lang) ?? doc.sectionTitle ?? t.guide}</span>
223
+ {componentBadge ? <code className="gdoc-eyebrow-fence">{`garden ${componentBadge}`}</code> : null}
224
+ </div>
225
+ <ArticleTitle title={doc.title} />
226
+ {doc.description ? <p className="gdoc-article-deck p-summary">{doc.description}</p> : null}
227
+ </header>
228
+
229
+ <ArticleMeasure />
230
+
231
+ <div className="e-content">
232
+ <MarkdownArticle content={doc.content} lang={lang} fieldTables handbook refDemos={isReferencePath(doc.path)} />
233
+ </div>
234
+
235
+ <ArticleReferences references={doc.references} lang={lang} />
236
+
237
+ <nav className="gdoc-pager" aria-label={t.pager}>
238
+ {prev ? (
239
+ <Link href={siteHref(handbookPath(lang, prev.path))} className="gdoc-pager-card is-prev">
240
+ <span className="gdoc-pager-dir">
241
+ <ArrowLeft size={14} aria-hidden="true" />
242
+ {t.prev}
243
+ </span>
244
+ <strong>{prev.title}</strong>
245
+ </Link>
246
+ ) : (
247
+ <Link href={siteHref(handbookPath(lang))} className="gdoc-pager-card is-prev">
248
+ <span className="gdoc-pager-dir">
249
+ <ArrowLeft size={14} aria-hidden="true" />
250
+ {t.backToOverview}
251
+ </span>
252
+ <strong>{t.overviewDocTitle}</strong>
253
+ </Link>
254
+ )}
255
+ {next ? (
256
+ <Link href={siteHref(handbookPath(lang, next.path))} className="gdoc-pager-card is-next">
257
+ <span className="gdoc-pager-dir">
258
+ {t.next}
259
+ <ArrowRight size={14} aria-hidden="true" />
260
+ </span>
261
+ <strong>{next.title}</strong>
262
+ </Link>
263
+ ) : null}
264
+ </nav>
265
+ </article>
266
+
267
+ <TableOfContents headings={headings} label={t.toc} lang={lang} />
268
+ </main>
269
+ );
270
+ }
@@ -1,3 +1,5 @@
1
+ import { UiLangProvider } from "./UiLang";
2
+ import { siteLang, ui, type UiLang } from "../lib/i18n";
1
3
  import { markdownPlugins } from "../lib/markdown-plugins";
2
4
  import { remarkA2 } from "../lib/remark-a2.mjs";
3
5
  import { remarkLinkCard } from "../lib/remark-link-card.mjs";
@@ -22,15 +24,19 @@ import { CodeCopy } from "./garden/chrome";
22
24
  import { CodeDiff } from "./garden/chrome";
23
25
  import { GardenBlock } from "./garden/GardenBlock";
24
26
  import { GardenFile } from "./garden/GardenFile";
27
+ import { RefDemo } from "./garden/RefDemo";
28
+ import { remarkRefDemos } from "../lib/remark-ref-demos.mjs";
25
29
  import { readGardenPayload } from "../lib/remark-garden.mjs";
26
30
  import { imageSizes, linkAssets } from "@garden/generated-content";
27
31
  import { imageDimensions, lookupImageSize } from "../../ui/garden/image-dims.mjs";
28
32
  import { siteConfig, siteLinks } from "@garden/site-config";
29
33
  import { articleLink, sameSiteMatcher } from "../../ui/garden/same-site.mjs";
30
34
  import { columnRedirects, internalLinkResolver, readRedirects } from "../../ui/garden/internal-links.mjs";
31
- import { columnHref, columns, gardenDocHref, gardenDocs, postHref, posts, thoughts } from "../lib/content";
35
+ import { columnHref, columns, gardenDocHref, gardenDocs, gardenDocsEn, postHref, posts, thoughts } from "../lib/content";
32
36
  import { LinkCardRefresh } from "./LinkCardRefresh";
33
37
  import { linkHost } from "../../scripts/link-meta-core.mjs";
38
+ import { FaviconProvider, SourceMark } from "./garden/patterns";
39
+ import { rehypeFieldTables } from "../lib/rehype-field-tables.mjs";
34
40
  import { cardDate } from "../../ui/garden/card-refresh.mjs";
35
41
  import { siteHref } from "../lib/site-href";
36
42
  import { codeLanguageLabel, fenceLanguage } from "../../ui/garden/code-language.mjs";
@@ -40,23 +46,36 @@ const sameSite = sameSiteMatcher(siteLinks?.address ?? "", siteLinks?.basePath ?
40
46
 
41
47
  // Links to other pages of the site render as Link. A link to an old address listed under
42
48
  // redirects is rewritten to the page it moved to; a link that lands on no known page stays a plain anchor.
43
- const internalRoute = internalLinkResolver({
49
+ const siteRoute = internalLinkResolver({
44
50
  redirects: [
45
51
  ...readRedirects(siteConfig),
46
52
  ...columnRedirects({ columns: [...new Set(columns.map((entry) => entry.column).filter(Boolean))].map((slug) => ({ slug })) }),
47
53
  ],
48
54
  routes: [
49
- "/", "/blog", "/columns", "/thoughts", "/tags", "/about", "/garden",
55
+ "/", "/blog", "/columns", "/thoughts", "/tags", "/about", "/garden", "/garden/en",
50
56
  ...posts.map(postHref),
51
57
  ...columns.map(columnHref),
52
58
  ...thoughts.map((thought) => `/thoughts/${thought.slug}`),
53
59
  ...gardenDocs.map(gardenDocHref),
60
+ ...gardenDocsEn.map((doc) => siteHref(`/garden/en/${doc.path}`)),
54
61
  ],
55
62
  basePath: siteLinks?.basePath ?? "",
56
63
  // Same shape as every other address in the site (the slash follows trailingSlash).
57
64
  format: siteHref,
58
65
  });
59
66
 
67
+ // The handbook ships with the framework and its links are written without
68
+ // any base path (/garden/…); Link adds the site's own prefix.
69
+ const handbookRoute = internalLinkResolver({
70
+ redirects: [],
71
+ routes: [
72
+ "/garden", "/garden/en",
73
+ ...gardenDocs.map(gardenDocHref),
74
+ ...gardenDocsEn.map((doc) => siteHref(`/garden/en/${doc.path}`)),
75
+ ],
76
+ format: siteHref,
77
+ });
78
+
60
79
  /** The site's favicon, downloaded at build time; nothing when there is none. */
61
80
  function Favicon({ href }: { href: string }) {
62
81
  const src = linkAssets?.favicons?.[linkHost(href)];
@@ -208,7 +227,9 @@ const headingText = (children: ReactNode) => {
208
227
  return parts.join("");
209
228
  };
210
229
 
211
- export function MarkdownArticle({ content, numbering }: { content: string; numbering?: boolean }) {
230
+ export function MarkdownArticle({ content, numbering, lang, fieldTables, refDemos, handbook }: { content: string; numbering?: boolean; lang?: UiLang; fieldTables?: boolean; refDemos?: boolean; handbook?: boolean }) {
231
+ const internalRoute = handbook ? handbookRoute : siteRoute;
232
+ const t = ui(lang);
212
233
  // Optional layer 2: a Worker that refreshes card text. Without it, cards stay as built.
213
234
  const cardWorker = content.includes("{card}") ? siteConfig.linkPreview?.worker : undefined;
214
235
  const numbers = { h2: 0, h3: 0 };
@@ -248,9 +269,10 @@ export function MarkdownArticle({ content, numbering }: { content: string; numbe
248
269
  "garden-block": (props: { dataPayload?: string; "data-payload"?: string }) => {
249
270
  const payload = props.dataPayload ?? props["data-payload"];
250
271
  const block = readGardenPayload(payload);
251
- if (block?.name === "file" && !block.error) return <GardenFile props={(block.props ?? {}) as Record<string, unknown>} />;
272
+ if (block?.name === "file" && !block.error) return <GardenFile props={(block.props ?? {}) as Record<string, unknown>} lang={lang} />;
252
273
  return <GardenBlock dataPayload={payload} />;
253
274
  },
275
+ "garden-refdemo": (props: { children?: ReactNode; "data-name"?: string }) => <RefDemo lang={lang === "en" ? "en" : "zh"} name={props["data-name"]}>{props.children}</RefDemo>,
254
276
  "garden-codediff": (props: {
255
277
  children?: ReactNode;
256
278
  dataDiff?: string;
@@ -266,9 +288,11 @@ export function MarkdownArticle({ content, numbering }: { content: string; numbe
266
288
  ),
267
289
  };
268
290
  return (
291
+ <UiLangProvider lang={lang}>
292
+ <FaviconProvider favicons={linkAssets?.favicons}>
269
293
  <div className="markdown-body prose">
270
294
  <ReactMarkdown
271
- remarkPlugins={[...markdownPlugins, [remarkLinkCard, { sameSite }], [remarkA2, { imageSize: (src: string) => lookupImageSize(imageSizes, src) }]]}
295
+ remarkPlugins={[...(refDemos ? [remarkRefDemos] : []), ...markdownPlugins, [remarkLinkCard, { sameSite }], [remarkA2, { imageSize: (src: string) => lookupImageSize(imageSizes, src), lang: lang ?? siteLang }]]}
272
296
  rehypePlugins={[
273
297
  rehypeRaw,
274
298
  rehypeKatex,
@@ -276,6 +300,8 @@ export function MarkdownArticle({ content, numbering }: { content: string; numbe
276
300
  // Not rehype-autolink-headings "wrap": a heading with a link in it would nest <a> in <a> (React #418).
277
301
  [rehypeHeadingAnchors, { className: ["h-anchor"] }],
278
302
  rehypeHighlight,
303
+ // Handbook only: field tables become cards on narrow screens.
304
+ ...(fieldTables ? [rehypeFieldTables] : []),
279
305
  ]}
280
306
  components={{
281
307
  p: ({ children, ...props }) => {
@@ -307,10 +333,12 @@ export function MarkdownArticle({ content, numbering }: { content: string; numbe
307
333
  href={href}
308
334
  {...domProps}
309
335
  {...(external
310
- ? { className: "ext", target: "_blank", rel: "noopener noreferrer" }
336
+ ? { ...(domProps.className ? { className: `ext ${domProps.className}` } : { className: "ext" }), target: "_blank", rel: "noopener noreferrer" }
311
337
  : {})}
312
338
  >
313
- {external && href ? <WithFavicon href={href}>{children}</WithFavicon> : children}
339
+ {external && href && /\bg-src-chip\b/u.test(String(domProps.className ?? ""))
340
+ ? <><SourceMark href={href} /><span>{children}</span></>
341
+ : <>{external && href ? <WithFavicon href={href}>{children}</WithFavicon> : children}</>}
314
342
  </a>
315
343
  );
316
344
  },
@@ -320,7 +348,7 @@ export function MarkdownArticle({ content, numbering }: { content: string; numbe
320
348
  return <MarkdownImage {...props} {...dims} style={{ ...props.style, ...dims.style }} />;
321
349
  },
322
350
  table: ({ children, ...props }) => (
323
- <div className="table-wrap" role="region" aria-label="表格,可横向滚动" tabIndex={0}>
351
+ <div className={/\bfield-table\b/u.test(String(props.className ?? "")) ? "table-wrap field-wrap" : "table-wrap"} role="region" aria-label={t.tableScroll} tabIndex={0}>
324
352
  <table {...props}>{children}</table>
325
353
  </div>
326
354
  ),
@@ -356,5 +384,7 @@ export function MarkdownArticle({ content, numbering }: { content: string; numbe
356
384
  </ReactMarkdown>
357
385
  {cardWorker ? <LinkCardRefresh endpoint={cardWorker} /> : null}
358
386
  </div>
387
+ </FaviconProvider>
388
+ </UiLangProvider>
359
389
  );
360
390
  }
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useUi } from "./UiLang";
3
4
  import { useEffect, useState, type ComponentPropsWithoutRef } from "react";
4
5
 
5
6
  // react-markdown also hands over its syntax-tree `node`; it must not reach the DOM.
@@ -8,7 +9,8 @@ type MarkdownImageProps = ComponentPropsWithoutRef<"img"> & { node?: unknown };
8
9
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
9
10
  export function MarkdownImage({ alt = "", src, node, style, ...props }: MarkdownImageProps) {
10
11
  const [open, setOpen] = useState(false);
11
- const description = alt || "查看原图";
12
+ const t = useUi();
13
+ const description = alt || t.viewImage;
12
14
 
13
15
  useEffect(() => {
14
16
  if (!open) return undefined;
@@ -33,7 +35,7 @@ export function MarkdownImage({ alt = "", src, node, style, ...props }: Markdown
33
35
  type="button"
34
36
  className="markdown-image-trigger"
35
37
  onClick={() => setOpen(true)}
36
- aria-label={`放大查看:${description}`}
38
+ aria-label={t.zoomImage(description)}
37
39
  >
38
40
  {/* Native images preserve Typora-authored relative paths. */}
39
41
  {/* eslint-disable-next-line @next/next/no-img-element */}
@@ -56,9 +58,9 @@ export function MarkdownImage({ alt = "", src, node, style, ...props }: Markdown
56
58
  type="button"
57
59
  className="markdown-image-lightbox-close"
58
60
  onClick={() => setOpen(false)}
59
- aria-label="关闭图片预览"
61
+ aria-label={t.closeImage}
60
62
  >
61
- 关闭
63
+ {t.close.trim()}
62
64
  </button>
63
65
  </div>
64
66
  </div>
@@ -1,18 +1,27 @@
1
1
  "use client";
2
2
 
3
3
  import { usePathname } from "next/navigation";
4
+ import { inAppPath, isHandbookPath, routeLang, ui } from "../lib/i18n";
4
5
  import { SiteFooter } from "./SiteFooter";
5
6
  import { SiteHeader } from "./SiteHeader";
6
7
 
7
8
  export function SiteChrome({ children }: { children: React.ReactNode }) {
8
9
  const pathname = usePathname();
9
- if (pathname?.startsWith("/admin")) return <>{children}</>;
10
+ const lang = routeLang(pathname);
11
+ // The skip link follows the route language too (/garden/en/ is English).
12
+ const skip = (
13
+ <a className="skip-link" href="#main-content">
14
+ {ui(lang).skipToContent}
15
+ </a>
16
+ );
17
+ if (inAppPath(pathname).startsWith("/admin")) return <>{skip}{children}</>;
10
18
 
11
19
  return (
12
20
  <>
21
+ {skip}
13
22
  <SiteHeader />
14
23
  <div id="main-content">{children}</div>
15
- <SiteFooter docs={pathname?.startsWith("/garden") ?? false} />
24
+ <SiteFooter docs={isHandbookPath(pathname)} lang={lang} />
16
25
  </>
17
26
  );
18
27
  }
@@ -1,18 +1,20 @@
1
1
  import { GithubLogo, Rss } from "@phosphor-icons/react/ssr";
2
2
  import { githubUrl, siteConfig } from "../site.config";
3
+ import { ui, type UiLang } from "../lib/i18n";
3
4
 
4
5
  /** On the Garden docs pages the footer carries no site-owner line. */
5
- export function SiteFooter({ docs = false }: { docs?: boolean }) {
6
+ export function SiteFooter({ docs = false, lang }: { docs?: boolean; lang?: UiLang }) {
7
+ const t = ui(lang);
6
8
  return (
7
9
  <footer className="sh-footer">
8
10
  <div className="sh-foot">
9
11
  <p>
10
12
  {docs ? (
11
- <span className="footer-framework">基于 <a href="https://github.com/hedon954/garden" target="_blank" rel="noreferrer">Garden</a> 构建</span>
13
+ <span className="footer-framework">{t.builtWith[0]}<a href="https://github.com/hedon954/garden" target="_blank" rel="noreferrer">Garden</a>{t.builtWith[1]}</span>
12
14
  ) : (
13
15
  <>
14
16
  © {new Date().getFullYear()} {siteConfig.author.name} · {siteConfig.footer}
15
- <span className="footer-framework"> · 基于 <a href="https://github.com/hedon954/garden" target="_blank" rel="noreferrer">Garden</a> 构建</span>
17
+ <span className="footer-framework"> · {t.builtWith[0]}<a href="https://github.com/hedon954/garden" target="_blank" rel="noreferrer">Garden</a>{t.builtWith[1]}</span>
16
18
  </>
17
19
  )}
18
20
  </p>
@@ -21,7 +23,7 @@ export function SiteFooter({ docs = false }: { docs?: boolean }) {
21
23
  <GithubLogo size={16} aria-hidden="true" />
22
24
  GitHub
23
25
  </a>
24
- <a href="/rss.xml">
26
+ <a href={`${(process.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/+$/u, "")}/rss.xml`}>
25
27
  <Rss size={16} aria-hidden="true" />
26
28
  RSS
27
29
  </a>