hd-garden 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/.agents/skills/garden-ui/SKILL.md +2 -2
  2. package/.env.en.example +67 -0
  3. package/README.en.md +2 -2
  4. package/README.md +2 -2
  5. package/app/a2/[slug]/page.tsx +4 -3
  6. package/app/a2/page.tsx +4 -3
  7. package/app/about/page.tsx +8 -9
  8. package/app/admin/page.tsx +7 -6
  9. package/app/admin/preview/[...path]/page.tsx +2 -1
  10. package/app/admin/ui/blocks/page.tsx +2 -1
  11. package/app/admin/ui/catalog/page.tsx +2 -1
  12. package/app/admin/ui/evals/page.tsx +2 -1
  13. package/app/admin/ui/page.tsx +2 -1
  14. package/app/blog/[...path]/page.tsx +7 -6
  15. package/app/blog/page.tsx +4 -2
  16. package/app/columns/[column]/[...path]/page.tsx +8 -7
  17. package/app/columns/page.tsx +12 -8
  18. package/app/components/AdminUiShell.tsx +4 -3
  19. package/app/components/ArticleMeasure.tsx +6 -3
  20. package/app/components/ArticleReferences.tsx +4 -1
  21. package/app/components/ArticleTags.tsx +3 -2
  22. package/app/components/BlogArchive.tsx +20 -16
  23. package/app/components/Comments.tsx +16 -10
  24. package/app/components/ContentEmptyState.tsx +29 -28
  25. package/app/components/ExternalEmbed.tsx +15 -14
  26. package/app/components/GardenContentGuide.tsx +10 -2
  27. package/app/components/GardenDocsNav.tsx +4 -2
  28. package/app/components/GiscusComments.tsx +2 -1
  29. package/app/components/GithubLiveOverview.tsx +10 -8
  30. package/app/components/GithubProfile.tsx +31 -21
  31. package/app/components/HandbookLangSwitch.tsx +8 -4
  32. package/app/components/HandbookView.tsx +92 -9
  33. package/app/components/MarkdownArticle.tsx +31 -7
  34. package/app/components/MermaidDiagram.tsx +4 -3
  35. package/app/components/SiteChrome.tsx +3 -3
  36. package/app/components/SiteFooter.tsx +1 -1
  37. package/app/components/SiteHeader.tsx +13 -4
  38. package/app/components/ThoughtCard.tsx +16 -14
  39. package/app/components/UiBlock.tsx +3 -2
  40. package/app/components/Webmentions.tsx +11 -11
  41. package/app/components/WidgetChart.tsx +8 -6
  42. package/app/components/garden/FilePreview.tsx +2 -2
  43. package/app/components/garden/GardenBlock.tsx +7 -0
  44. package/app/components/garden/GardenFile.tsx +2 -2
  45. package/app/components/garden/RefDemo.tsx +62 -0
  46. package/app/components/garden/blocks.tsx +56 -8
  47. package/app/components/garden/patterns.tsx +497 -0
  48. package/app/garden/[...slug]/page.tsx +3 -2
  49. package/app/garden/en/page.tsx +7 -7
  50. package/app/garden/page.tsx +3 -2
  51. package/app/globals.css +49 -1
  52. package/app/lib/a2-meta.ts +26 -3
  53. package/app/lib/content.ts +2 -2
  54. package/app/lib/garden-docs-meta.ts +24 -0
  55. package/app/lib/github-id.mjs +8 -0
  56. package/app/lib/handbook-switch.mjs +12 -0
  57. package/app/lib/i18n.ts +401 -2
  58. package/app/lib/rehype-field-tables.mjs +47 -0
  59. package/app/lib/remark-a2.mjs +18 -8
  60. package/app/lib/remark-link-card.mjs +22 -0
  61. package/app/lib/remark-ref-demos.mjs +42 -0
  62. package/app/lib/tag-filter.ts +9 -2
  63. package/app/not-found.tsx +32 -4
  64. package/app/page.tsx +17 -14
  65. package/app/rss.xml/route.ts +2 -1
  66. package/app/styles/a2.css +338 -0
  67. package/app/styles/shell.css +15 -2
  68. package/app/tags/page.tsx +7 -5
  69. package/app/thoughts/[slug]/page.tsx +2 -1
  70. package/app/thoughts/page/[n]/page.tsx +1 -1
  71. package/app/thoughts/page.tsx +2 -1
  72. package/app/ui-parity/page.tsx +38 -22
  73. package/bin/garden.mjs +129 -19
  74. package/docs/handbook/ai-writing.md +2 -2
  75. package/docs/handbook/assets/reading-layout-en.svg +26 -0
  76. package/docs/handbook/components/card.md +43 -22
  77. package/docs/handbook/components/chart.md +10 -26
  78. package/docs/handbook/components/choice.md +42 -0
  79. package/docs/handbook/components/compare.md +32 -17
  80. package/docs/handbook/components/figure.md +33 -46
  81. package/docs/handbook/components/file.md +10 -15
  82. package/docs/handbook/components/html.md +10 -30
  83. package/docs/handbook/components/interactive.md +9 -35
  84. package/docs/handbook/components/list.md +65 -0
  85. package/docs/handbook/components/relations.md +95 -0
  86. package/docs/handbook/components/stat.md +10 -28
  87. package/docs/handbook/components/timeline.md +13 -28
  88. package/docs/handbook/components.md +210 -19
  89. package/docs/handbook/config.md +5 -3
  90. package/docs/handbook/deploy.md +1 -1
  91. package/docs/handbook/design.md +20 -12
  92. package/docs/handbook/en/ai-writing.md +2 -2
  93. package/docs/handbook/en/components/card.md +42 -21
  94. package/docs/handbook/en/components/chart.md +10 -26
  95. package/docs/handbook/en/components/choice.md +42 -0
  96. package/docs/handbook/en/components/compare.md +30 -15
  97. package/docs/handbook/en/components/figure.md +29 -42
  98. package/docs/handbook/en/components/file.md +9 -14
  99. package/docs/handbook/en/components/html.md +9 -29
  100. package/docs/handbook/en/components/interactive.md +8 -34
  101. package/docs/handbook/en/components/list.md +65 -0
  102. package/docs/handbook/en/components/relations.md +95 -0
  103. package/docs/handbook/en/components/stat.md +9 -27
  104. package/docs/handbook/en/components/timeline.md +9 -24
  105. package/docs/handbook/en/components.md +211 -19
  106. package/docs/handbook/en/config.md +32 -30
  107. package/docs/handbook/en/deploy.md +12 -12
  108. package/docs/handbook/en/design.md +19 -12
  109. package/docs/handbook/en/index.md +2 -2
  110. package/docs/handbook/en/reference/card.md +85 -0
  111. package/docs/handbook/en/reference/chart.md +68 -0
  112. package/docs/handbook/en/reference/choice.md +69 -0
  113. package/docs/handbook/en/reference/compare.md +59 -0
  114. package/docs/handbook/en/reference/figure.md +84 -0
  115. package/docs/handbook/en/reference/file/checklist.md +17 -0
  116. package/docs/handbook/en/reference/file.md +50 -0
  117. package/docs/handbook/en/reference/html.md +62 -0
  118. package/docs/handbook/en/reference/interactive.md +71 -0
  119. package/docs/handbook/en/reference/list.md +72 -0
  120. package/docs/handbook/en/reference/relations.md +113 -0
  121. package/docs/handbook/en/reference/stat.md +60 -0
  122. package/docs/handbook/en/reference/timeline.md +56 -0
  123. package/docs/handbook/en/reference.md +26 -0
  124. package/docs/handbook/en/start.md +2 -2
  125. package/docs/handbook/en/workflow.md +6 -5
  126. package/docs/handbook/en/writing.md +3 -2
  127. package/docs/handbook/index.md +1 -1
  128. package/docs/handbook/reference/card.md +85 -0
  129. package/docs/handbook/reference/chart.md +68 -0
  130. package/docs/handbook/reference/choice.md +69 -0
  131. package/docs/handbook/reference/compare.md +59 -0
  132. package/docs/handbook/reference/figure.md +84 -0
  133. package/docs/handbook/reference/file/checklist.md +17 -0
  134. package/docs/handbook/reference/file.md +50 -0
  135. package/docs/handbook/reference/html.md +62 -0
  136. package/docs/handbook/reference/interactive.md +71 -0
  137. package/docs/handbook/reference/list.md +72 -0
  138. package/docs/handbook/reference/relations.md +113 -0
  139. package/docs/handbook/reference/stat.md +60 -0
  140. package/docs/handbook/reference/timeline.md +56 -0
  141. package/docs/handbook/reference.md +26 -0
  142. package/docs/handbook/start.md +1 -1
  143. package/docs/handbook/workflow.md +6 -5
  144. package/docs/handbook/writing.md +3 -2
  145. package/next.config.ts +6 -0
  146. package/package.json +2 -1
  147. package/public/images/garden/ref-card-book.svg +1 -0
  148. package/public/images/garden/ref-card-lamp.svg +1 -0
  149. package/scripts/build-content.mjs +37 -16
  150. package/scripts/favicon-ico.mjs +9 -6
  151. package/scripts/file-preview.mjs +10 -7
  152. package/scripts/handbook-allowlist.mjs +18 -0
  153. package/scripts/handbook-facts.mjs +305 -11
  154. package/scripts/link-assets.mjs +7 -0
  155. package/scripts/new-post.mjs +18 -9
  156. package/scripts/prepare-pages-output.mjs +30 -18
  157. package/scripts/redirects.mjs +17 -6
  158. package/templates/garden-blocks.md +67 -0
  159. package/templates/garden-snippets.plist +61 -0
  160. package/templates/site/Makefile.en +45 -0
  161. package/templates/site/README.en.md +15 -0
  162. package/templates/site/site.config.en.yaml +55 -0
  163. package/templates/skills-en/garden-interactive-chart/SKILL.md +84 -0
  164. package/templates/skills-en/garden-interactive-chart/references/shell.html +49 -0
  165. package/templates/skills-en/garden-interactive-chart/references/style.md +61 -0
  166. package/templates/skills-en/garden-post-review/SKILL.md +63 -0
  167. package/templates/skills-en/garden-ui/SKILL.md +118 -0
  168. package/templates/workflows/pages.en.yml +195 -0
  169. package/templates/workflows/pages.yml +195 -0
  170. package/ui/garden/catalog.mjs +205 -4
  171. package/ui/garden/check.mjs +6 -0
  172. package/ui/garden/file-block.mjs +8 -7
  173. package/ui/garden/html-lang.mjs +73 -1
  174. package/ui/garden/markdown-links.mjs +7 -4
  175. package/ui/garden/parse.mjs +45 -0
  176. package/ui/garden/relations.mjs +252 -0
  177. package/ui/garden/search-index.mjs +6 -4
  178. package/ui/garden/source-icon.mjs +54 -0
  179. package/ui/parity-fixtures.mjs +77 -0
  180. package/ui/preview/dist/garden-ui.esm.js +41 -4
  181. package/ui/preview/dist/garden-ui.iife.js +41 -4
  182. package/ui/prompt/rules.md +28 -1
  183. package/ui/react/components/chart.tsx +1 -1
  184. package/ui/react/components/display.tsx +1 -1
  185. package/ui/react/lang.tsx +4 -0
  186. package/ui/react/parity.tsx +8 -6
  187. package/worker/index.ts +8 -2
@@ -17,11 +17,13 @@ import {
17
17
  HANDBOOK_SECTIONS,
18
18
  HANDBOOK_UI,
19
19
  handbookPath,
20
+ isReferencePath,
20
21
  orderedHandbookDocs,
21
22
  sectionTitle,
22
23
  toHandbookNavItems,
23
24
  type HandbookLang,
24
25
  } from "../lib/garden-docs-meta";
26
+ import { UI_STRINGS, siteLang } from "../lib/i18n";
25
27
  import { siteConfig } from "../site.config";
26
28
 
27
29
  const other = (lang: HandbookLang): HandbookLang => (lang === "en" ? "zh" : "en");
@@ -29,6 +31,7 @@ const hreflang = (lang: HandbookLang) => (lang === "en" ? "en" : "zh-CN");
29
31
 
30
32
  /** canonical + hreflang links; the counterpart is listed only when it exists. */
31
33
  export function handbookLanguages(lang: HandbookLang, docPath: string) {
34
+ if (siteLang === "en") return undefined;
32
35
  if (!hasHandbookPage(other(lang), docPath)) return undefined;
33
36
  const zh = siteHref(handbookPath("zh", docPath));
34
37
  const en = siteHref(handbookPath("en", docPath));
@@ -38,21 +41,101 @@ export function handbookLanguages(lang: HandbookLang, docPath: string) {
38
41
  export const handbookAlternates = (lang: HandbookLang, docPath: string) =>
39
42
  pageAlternates(handbookPath(lang, docPath), handbookLanguages(lang, docPath));
40
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
+
41
82
  export async function handbookDocMetadata(lang: HandbookLang, doc: GardenDocEntry | undefined) {
42
- if (!doc) return {};
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
+ }
43
111
  const t = HANDBOOK_UI[lang];
44
112
  const baseUrl = await getSiteUrl();
45
113
  const pathname = siteHref(handbookPath(lang, doc.path));
114
+ const docTitle = `${doc.title} · ${t.name}`;
46
115
  return {
47
- title: `${doc.title} · ${t.name}`,
116
+ title: docTitle,
48
117
  description: doc.description,
49
118
  alternates: handbookAlternates(lang, doc.path),
50
119
  openGraph: {
51
120
  type: "article",
52
121
  url: absoluteUrl(pathname, baseUrl),
53
- title: `${doc.title} · ${t.name}`,
122
+ title: docTitle,
54
123
  description: doc.description,
55
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"],
56
139
  },
57
140
  } satisfies Metadata;
58
141
  }
@@ -66,7 +149,7 @@ export function HandbookOverview({ lang }: { lang: HandbookLang }) {
66
149
  const content = indexDoc?.content ?? "";
67
150
  const headings = extractHeadings(content);
68
151
  // The site config title/subtitle is written in the site's own language.
69
- const configured = lang === "zh" ? siteConfig.pages.garden : { title: "", subtitle: "" };
152
+ const configured = lang === siteLang ? siteConfig.pages.garden : { title: "", subtitle: "" };
70
153
  const title = configured.title?.trim() || indexDoc?.title || t.name;
71
154
  const subtitle = configured.subtitle?.trim() || indexDoc?.description || "";
72
155
 
@@ -89,10 +172,10 @@ export function HandbookOverview({ lang }: { lang: HandbookLang }) {
89
172
  <ArticleMeasure />
90
173
 
91
174
  <div className="e-content">
92
- <MarkdownArticle content={content} lang={lang} />
175
+ <MarkdownArticle content={content} lang={lang} fieldTables handbook />
93
176
  </div>
94
177
 
95
- <ArticleReferences references={indexDoc?.references} />
178
+ <ArticleReferences references={indexDoc?.references} lang={lang} />
96
179
 
97
180
  {firstDoc ? (
98
181
  <nav className="gdoc-pager" aria-label={t.pager}>
@@ -125,7 +208,7 @@ export function HandbookDoc({ lang, doc }: { lang: HandbookLang; doc: GardenDocE
125
208
  const componentBadge = COMPONENT_BADGE[doc.path];
126
209
 
127
210
  return (
128
- <main className="column-reading-page garden-doc-page" lang={hreflang(lang)}>
211
+ <main className={`column-reading-page garden-doc-page${isReferencePath(doc.path) ? " is-reference" : ""}`} lang={hreflang(lang)}>
129
212
  <HandbookLangRedirect lang={lang} />
130
213
  <GardenDocsNav docs={navItems} currentPath={doc.path} lang={lang} />
131
214
 
@@ -146,10 +229,10 @@ export function HandbookDoc({ lang, doc }: { lang: HandbookLang; doc: GardenDocE
146
229
  <ArticleMeasure />
147
230
 
148
231
  <div className="e-content">
149
- <MarkdownArticle content={doc.content} lang={lang} />
232
+ <MarkdownArticle content={doc.content} lang={lang} fieldTables handbook refDemos={isReferencePath(doc.path)} />
150
233
  </div>
151
234
 
152
- <ArticleReferences references={doc.references} />
235
+ <ArticleReferences references={doc.references} lang={lang} />
153
236
 
154
237
  <nav className="gdoc-pager" aria-label={t.pager}>
155
238
  {prev ? (
@@ -1,5 +1,5 @@
1
1
  import { UiLangProvider } from "./UiLang";
2
- import { ui, type UiLang } from "../lib/i18n";
2
+ import { siteLang, ui, type UiLang } from "../lib/i18n";
3
3
  import { markdownPlugins } from "../lib/markdown-plugins";
4
4
  import { remarkA2 } from "../lib/remark-a2.mjs";
5
5
  import { remarkLinkCard } from "../lib/remark-link-card.mjs";
@@ -24,6 +24,8 @@ import { CodeCopy } from "./garden/chrome";
24
24
  import { CodeDiff } from "./garden/chrome";
25
25
  import { GardenBlock } from "./garden/GardenBlock";
26
26
  import { GardenFile } from "./garden/GardenFile";
27
+ import { RefDemo } from "./garden/RefDemo";
28
+ import { remarkRefDemos } from "../lib/remark-ref-demos.mjs";
27
29
  import { readGardenPayload } from "../lib/remark-garden.mjs";
28
30
  import { imageSizes, linkAssets } from "@garden/generated-content";
29
31
  import { imageDimensions, lookupImageSize } from "../../ui/garden/image-dims.mjs";
@@ -33,6 +35,8 @@ import { columnRedirects, internalLinkResolver, readRedirects } from "../../ui/g
33
35
  import { columnHref, columns, gardenDocHref, gardenDocs, gardenDocsEn, postHref, posts, thoughts } from "../lib/content";
34
36
  import { LinkCardRefresh } from "./LinkCardRefresh";
35
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";
36
40
  import { cardDate } from "../../ui/garden/card-refresh.mjs";
37
41
  import { siteHref } from "../lib/site-href";
38
42
  import { codeLanguageLabel, fenceLanguage } from "../../ui/garden/code-language.mjs";
@@ -42,7 +46,7 @@ const sameSite = sameSiteMatcher(siteLinks?.address ?? "", siteLinks?.basePath ?
42
46
 
43
47
  // Links to other pages of the site render as Link. A link to an old address listed under
44
48
  // redirects is rewritten to the page it moved to; a link that lands on no known page stays a plain anchor.
45
- const internalRoute = internalLinkResolver({
49
+ const siteRoute = internalLinkResolver({
46
50
  redirects: [
47
51
  ...readRedirects(siteConfig),
48
52
  ...columnRedirects({ columns: [...new Set(columns.map((entry) => entry.column).filter(Boolean))].map((slug) => ({ slug })) }),
@@ -60,6 +64,18 @@ const internalRoute = internalLinkResolver({
60
64
  format: siteHref,
61
65
  });
62
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
+
63
79
  /** The site's favicon, downloaded at build time; nothing when there is none. */
64
80
  function Favicon({ href }: { href: string }) {
65
81
  const src = linkAssets?.favicons?.[linkHost(href)];
@@ -211,7 +227,8 @@ const headingText = (children: ReactNode) => {
211
227
  return parts.join("");
212
228
  };
213
229
 
214
- export function MarkdownArticle({ content, numbering, lang }: { content: string; numbering?: boolean; lang?: UiLang }) {
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;
215
232
  const t = ui(lang);
216
233
  // Optional layer 2: a Worker that refreshes card text. Without it, cards stay as built.
217
234
  const cardWorker = content.includes("{card}") ? siteConfig.linkPreview?.worker : undefined;
@@ -255,6 +272,7 @@ export function MarkdownArticle({ content, numbering, lang }: { content: string;
255
272
  if (block?.name === "file" && !block.error) return <GardenFile props={(block.props ?? {}) as Record<string, unknown>} lang={lang} />;
256
273
  return <GardenBlock dataPayload={payload} />;
257
274
  },
275
+ "garden-refdemo": (props: { children?: ReactNode; "data-name"?: string }) => <RefDemo lang={lang === "en" ? "en" : "zh"} name={props["data-name"]}>{props.children}</RefDemo>,
258
276
  "garden-codediff": (props: {
259
277
  children?: ReactNode;
260
278
  dataDiff?: string;
@@ -271,9 +289,10 @@ export function MarkdownArticle({ content, numbering, lang }: { content: string;
271
289
  };
272
290
  return (
273
291
  <UiLangProvider lang={lang}>
292
+ <FaviconProvider favicons={linkAssets?.favicons}>
274
293
  <div className="markdown-body prose">
275
294
  <ReactMarkdown
276
- 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 }]]}
277
296
  rehypePlugins={[
278
297
  rehypeRaw,
279
298
  rehypeKatex,
@@ -281,6 +300,8 @@ export function MarkdownArticle({ content, numbering, lang }: { content: string;
281
300
  // Not rehype-autolink-headings "wrap": a heading with a link in it would nest <a> in <a> (React #418).
282
301
  [rehypeHeadingAnchors, { className: ["h-anchor"] }],
283
302
  rehypeHighlight,
303
+ // Handbook only: field tables become cards on narrow screens.
304
+ ...(fieldTables ? [rehypeFieldTables] : []),
284
305
  ]}
285
306
  components={{
286
307
  p: ({ children, ...props }) => {
@@ -312,10 +333,12 @@ export function MarkdownArticle({ content, numbering, lang }: { content: string;
312
333
  href={href}
313
334
  {...domProps}
314
335
  {...(external
315
- ? { className: "ext", target: "_blank", rel: "noopener noreferrer" }
336
+ ? { ...(domProps.className ? { className: `ext ${domProps.className}` } : { className: "ext" }), target: "_blank", rel: "noopener noreferrer" }
316
337
  : {})}
317
338
  >
318
- {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}</>}
319
342
  </a>
320
343
  );
321
344
  },
@@ -325,7 +348,7 @@ export function MarkdownArticle({ content, numbering, lang }: { content: string;
325
348
  return <MarkdownImage {...props} {...dims} style={{ ...props.style, ...dims.style }} />;
326
349
  },
327
350
  table: ({ children, ...props }) => (
328
- <div className="table-wrap" role="region" aria-label={t.tableScroll} 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}>
329
352
  <table {...props}>{children}</table>
330
353
  </div>
331
354
  ),
@@ -361,6 +384,7 @@ export function MarkdownArticle({ content, numbering, lang }: { content: string;
361
384
  </ReactMarkdown>
362
385
  {cardWorker ? <LinkCardRefresh endpoint={cardWorker} /> : null}
363
386
  </div>
387
+ </FaviconProvider>
364
388
  </UiLangProvider>
365
389
  );
366
390
  }
@@ -25,10 +25,11 @@ function renderMermaid(source: string, id: string) {
25
25
  themeVariables: {
26
26
  darkMode: dark,
27
27
  background: token("--paper"),
28
- primaryColor: token("--card"),
29
- primaryBorderColor: token("--rule-strong"),
28
+ primaryColor: token("--paper"),
29
+ primaryBorderColor: token("--ink"),
30
30
  primaryTextColor: token("--ink"),
31
- lineColor: token("--muted"),
31
+ textColor: token("--ink"),
32
+ lineColor: token("--ink"),
32
33
  secondaryColor: token("--well"),
33
34
  tertiaryColor: token("--well"),
34
35
  fontSize: "14px",
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { usePathname } from "next/navigation";
4
- import { routeLang, ui } from "../lib/i18n";
4
+ import { inAppPath, isHandbookPath, routeLang, ui } from "../lib/i18n";
5
5
  import { SiteFooter } from "./SiteFooter";
6
6
  import { SiteHeader } from "./SiteHeader";
7
7
 
@@ -14,14 +14,14 @@ export function SiteChrome({ children }: { children: React.ReactNode }) {
14
14
  {ui(lang).skipToContent}
15
15
  </a>
16
16
  );
17
- if (pathname?.startsWith("/admin")) return <>{skip}{children}</>;
17
+ if (inAppPath(pathname).startsWith("/admin")) return <>{skip}{children}</>;
18
18
 
19
19
  return (
20
20
  <>
21
21
  {skip}
22
22
  <SiteHeader />
23
23
  <div id="main-content">{children}</div>
24
- <SiteFooter docs={pathname?.startsWith("/garden") ?? false} lang={lang} />
24
+ <SiteFooter docs={isHandbookPath(pathname)} lang={lang} />
25
25
  </>
26
26
  );
27
27
  }
@@ -23,7 +23,7 @@ export function SiteFooter({ docs = false, lang }: { docs?: boolean; lang?: UiLa
23
23
  <GithubLogo size={16} aria-hidden="true" />
24
24
  GitHub
25
25
  </a>
26
- <a href="/rss.xml">
26
+ <a href={`${(process.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/+$/u, "")}/rss.xml`}>
27
27
  <Rss size={16} aria-hidden="true" />
28
28
  RSS
29
29
  </a>
@@ -17,7 +17,7 @@ import { createSearch } from "../../ui/garden/search-match.mjs";
17
17
  import { cropMatchedText, highlightParts } from "../../ui/garden/search-preview.mjs";
18
18
  import { resolveDark, themeSwitchProps } from "../../ui/garden/theme.mjs";
19
19
  import { SITE_NAV, isCurrentNav } from "../../ui/garden/site-nav.mjs";
20
- import { routeLang, ui } from "../lib/i18n";
20
+ import { isHandbookPath, routeLang, ui } from "../lib/i18n";
21
21
  import { HandbookLangSwitch } from "./HandbookLangSwitch";
22
22
  import { createIdbStore, loadFullIndex, saveDataOn, whenIdleAfterLoad } from "../../ui/garden/search-full.mjs";
23
23
  import { siteConfig } from "../site.config";
@@ -192,7 +192,12 @@ export function SiteHeader() {
192
192
  useEffect(() => {
193
193
  const system = window.matchMedia("(prefers-color-scheme: dark)");
194
194
  const apply = () => {
195
- const stored = window.localStorage.getItem("theme");
195
+ let stored: string | null = null;
196
+ try {
197
+ stored = window.localStorage.getItem("theme");
198
+ } catch {
199
+ // Storage blocked (cookies off, opaque origin): follow the system.
200
+ }
196
201
  const useDark = resolveDark(stored, system.matches);
197
202
  document.documentElement.dataset.theme = useDark ? "dark" : "light";
198
203
  setDark(useDark);
@@ -298,7 +303,11 @@ export function SiteHeader() {
298
303
  const next = !dark;
299
304
  setDark(next);
300
305
  document.documentElement.dataset.theme = next ? "dark" : "light";
301
- window.localStorage.setItem("theme", next ? "dark" : "light");
306
+ try {
307
+ window.localStorage.setItem("theme", next ? "dark" : "light");
308
+ } catch {
309
+ // Storage blocked: the choice lasts for this page only.
310
+ }
302
311
  };
303
312
 
304
313
  const current = (href: string) => isCurrentNav(pathname, href);
@@ -338,7 +347,7 @@ export function SiteHeader() {
338
347
  <path d="M13.3 13.3 17 17" />
339
348
  </svg>
340
349
  </button>
341
- {pathname?.startsWith("/garden") ? <HandbookLangSwitch /> : null}
350
+ {isHandbookPath(pathname) ? <HandbookLangSwitch /> : null}
342
351
  <button className="sh-icon sh-theme" type="button" onClick={toggleTheme} aria-label={dark ? t.toLight : t.toDark}>
343
352
  <svg className="i-moon" viewBox="0 0 20 20" aria-hidden="true">
344
353
  <path d="M16.5 12.2A6.8 6.8 0 0 1 7.8 3.5a6.8 6.8 0 1 0 8.7 8.7Z" />
@@ -15,6 +15,7 @@ import {
15
15
  import { githubUrl, siteConfig } from "../site.config";
16
16
  import { MarkdownArticle } from "./MarkdownArticle";
17
17
  import { siteHref } from "../lib/site-href";
18
+ import { siteLang, ui } from "../lib/i18n";
18
19
 
19
20
  const icons = {
20
21
  image: ImageSquare,
@@ -66,11 +67,12 @@ export function ThoughtCard({
66
67
  const isMixed = media.length > 1;
67
68
  const Icon = isMixed ? SquaresFour : icons[thought.mediaType ?? "text"];
68
69
  const mediaGroups = groupMedia(media);
70
+ const t = ui();
69
71
 
70
72
  return (
71
73
  <article id={thought.slug} className="thought-card h-entry">
72
74
  <Link className="u-url sr-only" href={siteHref(`/thoughts/${thought.slug}`)}>
73
- {thought.title} 永久链接
75
+ {siteLang === "en" ? t.permalinkLabel(thought.title) : <>{thought.title} 永久链接</>}
74
76
  </Link>
75
77
  <a
76
78
  className="p-author h-card sr-only"
@@ -83,16 +85,16 @@ export function ThoughtCard({
83
85
  <span className="thought-kind">
84
86
  <Icon size={16} />
85
87
  {isMixed
86
- ? `混合记录 · ${media.length} 个附件`
88
+ ? t.thoughtMixedLabel(media.length)
87
89
  : thought.mediaType === "image"
88
- ? "一张照片"
90
+ ? t.thoughtImageLabel
89
91
  : thought.mediaType === "audio"
90
- ? "一段声音"
92
+ ? t.thoughtAudioLabel
91
93
  : thought.mediaType === "video"
92
- ? "一段影像"
94
+ ? t.thoughtVideoLabel
93
95
  : thought.mediaType === "link"
94
- ? "一个链接"
95
- : "随想"}
96
+ ? t.thoughtLinkLabel
97
+ : t.thoughtTextLabel}
96
98
  </span>
97
99
  <time className="dt-published" dateTime={thought.date}>
98
100
  {formatDate(thought.date, true)}
@@ -112,7 +114,7 @@ export function ThoughtCard({
112
114
  <div
113
115
  className={`thought-gallery thought-gallery-${Math.min(group.items.length, 3)}`}
114
116
  key={`images-${groupIndex}`}
115
- aria-label={`${group.items.length} 张照片`}
117
+ aria-label={t.thoughtGalleryLabel(group.items.length)}
116
118
  >
117
119
  {group.items.map((item, index) => (
118
120
  // eslint-disable-next-line @next/next/no-img-element
@@ -120,7 +122,7 @@ export function ThoughtCard({
120
122
  key={`${item.src}-${index}`}
121
123
  className="thought-image u-photo"
122
124
  src={item.src}
123
- alt={item.alt ?? `${thought.title} · 照片 ${index + 1}`}
125
+ alt={item.alt ?? t.thoughtPhotoAlt(thought.title, index + 1)}
124
126
  loading="lazy"
125
127
  />
126
128
  ))}
@@ -132,14 +134,14 @@ export function ThoughtCard({
132
134
  <div className="audio-embed" key={`${group.item.src}-${groupIndex}`}>
133
135
  <MusicNotes size={28} />
134
136
  <div>
135
- <strong>{group.item.title ?? `当前播放 · ${thought.title}`}</strong>
137
+ <strong>{group.item.title ?? t.thoughtNowPlaying(thought.title)}</strong>
136
138
  <audio
137
139
  className="u-audio"
138
140
  controls
139
141
  preload="none"
140
142
  src={group.item.src}
141
143
  >
142
- 你的浏览器不支持音频播放。
144
+ {t.audioFallback}
143
145
  </audio>
144
146
  </div>
145
147
  </div>
@@ -194,14 +196,14 @@ export function ThoughtCard({
194
196
  <div className="audio-embed">
195
197
  <MusicNotes size={28} />
196
198
  <div>
197
- <strong>当前播放 · {thought.title}</strong>
199
+ <strong>{t.thoughtNowPlaying(thought.title)}</strong>
198
200
  <audio
199
201
  className="u-audio"
200
202
  controls
201
203
  preload="none"
202
204
  src={thought.media}
203
205
  >
204
- 你的浏览器不支持音频播放。
206
+ {t.audioFallback}
205
207
  </audio>
206
208
  </div>
207
209
  </div>
@@ -234,7 +236,7 @@ export function ThoughtCard({
234
236
 
235
237
  {showPermalink && (
236
238
  <Link className="thought-permalink" href={siteHref(`/thoughts/${thought.slug}`)}>
237
- 永久链接
239
+ {t.thoughtPermalinkShort}
238
240
  <ArrowUpRight size={15} />
239
241
  </Link>
240
242
  )}
@@ -1,9 +1,10 @@
1
1
  import { readPayload } from "../../ui/core/compile.mjs";
2
2
  import { IntentCard, UiErrorCard, UiFigure, type Diagnostic, type UiPayload } from "../../ui/react";
3
+ import { ui, type UiLang } from "../lib/i18n";
3
4
 
4
- export function UiBlock({ source }: { source: string }) {
5
+ export function UiBlock({ source, lang }: { source: string; lang?: UiLang }) {
5
6
  const payload = readPayload(source) as UiPayload | null;
6
- if (!payload) return <p className="external-embed-error">这块交互的数据损坏了,请重新运行 garden sync。</p>;
7
+ if (!payload) return <p className="external-embed-error">{ui(lang).uiBlockCorrupt}</p>;
7
8
  return <UiFigure payload={payload} />;
8
9
  }
9
10
 
@@ -8,7 +8,7 @@ import {
8
8
  Quotes,
9
9
  Repeat,
10
10
  } from "@phosphor-icons/react";
11
- import { ui } from "../lib/i18n";
11
+ import { siteLang, ui } from "../lib/i18n";
12
12
 
13
13
  type WebmentionEntry = {
14
14
  author?: {
@@ -102,26 +102,26 @@ export function Webmentions({
102
102
  {!endpoint && (
103
103
  <div className="integration-notice">
104
104
  <Quotes size={20} weight="fill" />
105
- <p>接收与展示代码已经就绪,注册正式域名后即可开始收集回应。</p>
105
+ <p>{ui().webmentionIdleNote}</p>
106
106
  </div>
107
107
  )}
108
108
 
109
109
  {endpoint && loading && (
110
110
  <p className="integration-status" role="status">
111
- 正在读取开放网络回应…
111
+ {ui().webmentionLoading}
112
112
  </p>
113
113
  )}
114
114
 
115
115
  {endpoint && failed && (
116
116
  <p className="integration-status" role="status">
117
- 暂时无法读取回应,稍后会自动恢复。
117
+ {ui().webmentionUnavailable}
118
118
  </p>
119
119
  )}
120
120
 
121
121
  {endpoint && !loading && !failed && entries.length === 0 && (
122
122
  <div className="integration-notice">
123
123
  <Quotes size={20} weight="fill" />
124
- <p>还没有站外回应。任何链接到这篇文章的网站都可以向它发送 Webmention。</p>
124
+ <p>{ui().webmentionEmpty}</p>
125
125
  </div>
126
126
  )}
127
127
 
@@ -130,15 +130,15 @@ export function Webmentions({
130
130
  <span>
131
131
  <Heart size={16} weight="fill" />
132
132
  <Repeat size={16} />
133
- {reactions.length} 个喜欢、转发或收藏
133
+ {ui().webmentionReactionCount(reactions.length)}
134
134
  </span>
135
- <div className="webmention-facepile" aria-label="回应者">
135
+ <div className="webmention-facepile" aria-label={ui().webmentionRespondersLabel}>
136
136
  {reactions.slice(0, 16).map((entry, index) => (
137
137
  <a
138
138
  href={entry.url}
139
139
  target="_blank"
140
140
  rel="noopener noreferrer"
141
- aria-label={entry.author?.name ?? "查看站外回应"}
141
+ aria-label={entry.author?.name ?? ui().webmentionViewResponse}
142
142
  key={`${entry.url}-${index}`}
143
143
  >
144
144
  {entry.author?.photo ? (
@@ -165,10 +165,10 @@ export function Webmentions({
165
165
  return (
166
166
  <article key={`${entry.url}-${index}`}>
167
167
  <header>
168
- <strong>{entry.author?.name ?? "开放网络访客"}</strong>
168
+ <strong>{entry.author?.name ?? ui().webmentionAuthorFallback}</strong>
169
169
  {received && (
170
170
  <time dateTime={received}>
171
- {new Intl.DateTimeFormat("zh-CN", {
171
+ {new Intl.DateTimeFormat(siteLang === "en" ? "en-US" : "zh-CN", {
172
172
  year: "numeric",
173
173
  month: "2-digit",
174
174
  day: "2-digit",
@@ -182,7 +182,7 @@ export function Webmentions({
182
182
  target="_blank"
183
183
  rel="noopener noreferrer"
184
184
  >
185
- 查看原文
185
+ {ui().webmentionViewOriginal}
186
186
  <ArrowSquareOut size={14} />
187
187
  </a>
188
188
  </article>
@@ -3,6 +3,7 @@
3
3
  import { ArrowUpRight, Browser, CaretDown, FilePdf } from "@phosphor-icons/react";
4
4
  import { useEffect, useRef, useState } from "react";
5
5
  import { parse } from "yaml";
6
+ import { ui, type UiLang } from "../lib/i18n";
6
7
 
7
8
  type WidgetKind = "chart" | "html" | "pdf";
8
9
 
@@ -79,7 +80,8 @@ function DocumentThumb({ kind }: { kind: "html" | "pdf" }) {
79
80
  );
80
81
  }
81
82
 
82
- export function WidgetChart({ source }: { source: string }) {
83
+ export function WidgetChart({ source, lang }: { source: string; lang?: UiLang }) {
84
+ const s = ui(lang);
83
85
  const data = readWidgetData(source);
84
86
  const src = data?.src;
85
87
  const kind = data?.kind;
@@ -132,7 +134,7 @@ export function WidgetChart({ source }: { source: string }) {
132
134
  if (!data) {
133
135
  return (
134
136
  <p className="external-embed-error">
135
- 无法解析这个嵌入块。请在与 Markdown 同名的目录里放一份 .html 或 .pdf,并在围栏中写 src 与 caption。
137
+ {s.widgetParseError}
136
138
  </p>
137
139
  );
138
140
  }
@@ -156,7 +158,7 @@ export function WidgetChart({ source }: { source: string }) {
156
158
  <figcaption>
157
159
  <span>{data.caption}</span>
158
160
  <a href={data.src} target="_blank" rel="noopener noreferrer">
159
- 打开页面
161
+ {s.widgetOpenPage}
160
162
  </a>
161
163
  </figcaption>
162
164
  <noscript>
@@ -167,8 +169,8 @@ export function WidgetChart({ source }: { source: string }) {
167
169
  }
168
170
 
169
171
  const Icon = data.kind === "pdf" ? FilePdf : Browser;
170
- const kindLabel = data.kind === "pdf" ? "PDF" : "页面";
171
- const openLabel = data.kind === "pdf" ? "打开 PDF" : "打开页面";
172
+ const kindLabel = data.kind === "pdf" ? "PDF" : s.widgetPageKind;
173
+ const openLabel = data.kind === "pdf" ? s.widgetOpenPdf : s.widgetOpenPage;
172
174
 
173
175
  return (
174
176
  <figure
@@ -197,7 +199,7 @@ export function WidgetChart({ source }: { source: string }) {
197
199
  <strong>{data.caption}</strong>
198
200
  </span>
199
201
  <span className="garden-doc-action">
200
- {expanded ? "收起" : "展开"}
202
+ {expanded ? s.widgetCollapse : s.widgetExpand}
201
203
  <CaretDown size={14} className="garden-doc-caret" />
202
204
  </span>
203
205
  </button>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useCallback, useEffect, useId, useRef, useState, type FocusEvent, type MouseEvent } from "react";
4
4
  import { createPortal } from "react-dom";
5
- import { ui, type UiLang } from "../../lib/i18n";
5
+ import { siteLang, ui, type UiLang } from "../../lib/i18n";
6
6
  import { previewBody, previewFrameProps } from "../../../ui/garden/file-block.mjs";
7
7
  import { ActionGlyph, FileGlyph } from "./file-icons";
8
8
 
@@ -119,7 +119,7 @@ export function FilePreview({ file, compact, lang }: { file: PreviewFile; compac
119
119
  if (file.preview === "md" && loaded.state !== "ready" && loaded.state !== "loading") load();
120
120
  };
121
121
 
122
- const frame = previewFrameProps(file) as { src: string; title: string; referrerPolicy: "no-referrer"; sandbox?: string } | null;
122
+ const frame = previewFrameProps(file, lang ?? siteLang) as { src: string; title: string; referrerPolicy: "no-referrer"; sandbox?: string } | null;
123
123
  const drawer = open
124
124
  ? createPortal(
125
125
  <div className="gf-layer">