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.
- package/.agents/skills/garden-ui/SKILL.md +26 -19
- package/.env.en.example +67 -0
- package/README.en.md +57 -0
- package/README.md +31 -46
- 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 +14 -5
- package/app/blog/page.tsx +4 -2
- package/app/columns/[column]/[...path]/page.tsx +17 -18
- package/app/columns/page.tsx +12 -8
- package/app/components/AdminUiShell.tsx +4 -3
- package/app/components/ArticlePager.tsx +32 -0
- 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 +17 -10
- package/app/components/ContentEmptyState.tsx +29 -28
- package/app/components/ExternalEmbed.tsx +15 -14
- package/app/components/FeedMore.tsx +70 -0
- package/app/components/GardenContentGuide.tsx +10 -2
- package/app/components/GardenDocsNav.tsx +29 -22
- package/app/components/GiscusComments.tsx +2 -1
- package/app/components/GithubLiveOverview.tsx +10 -8
- package/app/components/GithubProfile.tsx +31 -21
- package/app/components/HandbookLangRedirect.tsx +30 -0
- package/app/components/HandbookLangSwitch.tsx +53 -0
- package/app/components/HandbookView.tsx +270 -0
- package/app/components/MarkdownArticle.tsx +39 -9
- package/app/components/MarkdownImage.tsx +6 -4
- package/app/components/SiteChrome.tsx +11 -2
- package/app/components/SiteFooter.tsx +6 -4
- package/app/components/SiteHeader.tsx +38 -25
- package/app/components/TableOfContents.tsx +15 -9
- package/app/components/ThoughtCard.tsx +16 -14
- package/app/components/ThoughtFeed.tsx +41 -0
- package/app/components/UiBlock.tsx +3 -2
- package/app/components/UiLang.tsx +18 -0
- package/app/components/Webmentions.tsx +13 -12
- package/app/components/WidgetChart.tsx +8 -6
- package/app/components/garden/FilePreview.tsx +14 -12
- package/app/components/garden/GardenBlock.tsx +10 -1
- package/app/components/garden/GardenFile.tsx +10 -8
- package/app/components/garden/RefDemo.tsx +62 -0
- package/app/components/garden/blocks.tsx +90 -32
- package/app/components/garden/chrome.tsx +20 -13
- package/app/components/garden/patterns.tsx +497 -0
- package/app/garden/[...slug]/page.tsx +11 -130
- package/app/garden/en/[...slug]/page.tsx +28 -0
- package/app/garden/en/page.tsx +16 -0
- package/app/garden/page.tsx +8 -74
- package/app/garden-modules.d.ts +3 -0
- package/app/globals.css +10 -0
- package/app/layout.tsx +2 -4
- package/app/lib/a2-meta.ts +26 -3
- package/app/lib/content.ts +13 -3
- package/app/lib/garden-docs-meta.ts +84 -5
- package/app/lib/github-id.mjs +8 -0
- package/app/lib/handbook-path.ts +11 -0
- package/app/lib/handbook-switch.mjs +12 -0
- package/app/lib/i18n.ts +621 -0
- package/app/lib/page-alternates.ts +2 -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 +34 -4
- package/app/page.tsx +17 -14
- package/app/rss.xml/route.ts +2 -1
- package/app/site.config.ts +1 -1
- package/app/styles/a2.css +439 -32
- package/app/styles/shell.css +21 -2
- package/app/tags/page.tsx +7 -5
- package/app/thoughts/[slug]/page.tsx +2 -1
- package/app/thoughts/page/[n]/page.tsx +44 -0
- package/app/thoughts/page.tsx +4 -7
- package/app/ui-parity/page.tsx +38 -22
- package/bin/garden.mjs +133 -19
- package/docs/handbook/ai-writing.md +99 -0
- package/docs/handbook/assets/reading-layout-en.svg +26 -0
- package/docs/handbook/components/card.md +43 -20
- package/docs/handbook/components/chart.md +10 -24
- package/docs/handbook/components/choice.md +42 -0
- package/docs/handbook/components/compare.md +34 -21
- package/docs/handbook/components/figure.md +33 -44
- package/docs/handbook/components/file.md +10 -13
- package/docs/handbook/components/html.md +10 -28
- package/docs/handbook/components/interactive.md +9 -33
- package/docs/handbook/components/list.md +65 -0
- package/docs/handbook/components/relations.md +95 -0
- package/docs/handbook/components/stat.md +10 -26
- package/docs/handbook/components/timeline.md +13 -26
- package/docs/handbook/components.md +210 -19
- package/docs/handbook/config.md +7 -5
- package/docs/handbook/deploy.md +2 -2
- package/docs/handbook/design.md +23 -15
- package/docs/handbook/en/ai-writing.md +99 -0
- package/docs/handbook/en/components/card.md +67 -0
- package/docs/handbook/en/components/chart.md +38 -0
- package/docs/handbook/en/components/choice.md +42 -0
- package/docs/handbook/en/components/compare.md +53 -0
- package/docs/handbook/en/components/figure.md +59 -0
- package/docs/handbook/en/components/file/checklist.md +17 -0
- package/docs/handbook/en/components/file.md +29 -0
- package/docs/handbook/en/components/html.md +49 -0
- package/docs/handbook/en/components/interactive.md +41 -0
- 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 +39 -0
- package/docs/handbook/en/components/timeline.md +37 -0
- package/docs/handbook/en/components.md +222 -0
- package/docs/handbook/en/config.md +127 -0
- package/docs/handbook/en/deploy.md +273 -0
- package/docs/handbook/en/design.md +88 -0
- package/docs/handbook/en/index.md +192 -0
- 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 +92 -0
- package/docs/handbook/en/workflow.md +82 -0
- package/docs/handbook/en/writing.md +157 -0
- package/docs/handbook/index.md +99 -4
- 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 +21 -12
- 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 +57 -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 +339 -11
- package/scripts/html-lang.mjs +36 -0
- 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/scripts/static-404.mjs +25 -0
- package/scripts/ui-cli.mjs +14 -8
- 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/site/site.config.yaml +2 -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/core/apply.mjs +22 -4
- package/ui/core/cli-locale.mjs +67 -0
- package/ui/core/fence.mjs +18 -3
- package/ui/core/fix-prompt.mjs +18 -15
- package/ui/core/garden-fence.mjs +64 -0
- package/ui/core/scan.mjs +19 -3
- package/ui/garden/adjacent.mjs +21 -0
- package/ui/garden/catalog.mjs +205 -4
- package/ui/garden/check.mjs +12 -0
- package/ui/garden/file-block.mjs +8 -7
- package/ui/garden/html-lang.mjs +88 -0
- package/ui/garden/markdown-links.mjs +7 -4
- package/ui/garden/paging.mjs +30 -0
- 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.css +9 -9
- package/ui/preview/dist/garden-ui.esm.js +206 -169
- package/ui/preview/dist/garden-ui.iife.js +206 -169
- package/ui/prompt/rules.md +28 -1
- package/ui/react/components/chart.tsx +8 -5
- package/ui/react/components/display.tsx +7 -4
- package/ui/react/components/inputs.tsx +6 -3
- package/ui/react/components/layout.tsx +6 -4
- package/ui/react/components/media.tsx +7 -4
- package/ui/react/components/sandbox.tsx +3 -1
- package/ui/react/index.tsx +11 -7
- package/ui/react/lang.tsx +89 -0
- package/ui/react/parity.tsx +8 -6
- package/ui/react/runtime.tsx +3 -1
- package/ui/react/ui.css +9 -9
- package/worker/index.ts +14 -1
|
@@ -14,9 +14,11 @@ import type Fuse from "fuse.js";
|
|
|
14
14
|
import { MagnifyingGlass, X } from "@phosphor-icons/react";
|
|
15
15
|
import { brand, searchFull, searchIndexUrl } from "@garden/site-config";
|
|
16
16
|
import { createSearch } from "../../ui/garden/search-match.mjs";
|
|
17
|
-
import { cropMatchedText, highlightParts
|
|
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 { isHandbookPath, routeLang, ui } from "../lib/i18n";
|
|
21
|
+
import { HandbookLangSwitch } from "./HandbookLangSwitch";
|
|
20
22
|
import { createIdbStore, loadFullIndex, saveDataOn, whenIdleAfterLoad } from "../../ui/garden/search-full.mjs";
|
|
21
23
|
import { siteConfig } from "../site.config";
|
|
22
24
|
import { siteHref } from "../lib/site-href";
|
|
@@ -87,7 +89,7 @@ function loadSearchEngine() {
|
|
|
87
89
|
return engineLoad;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
const nav = SITE_NAV.map((item) => ({ ...item, href: siteHref(item.href) }));
|
|
92
|
+
const nav = SITE_NAV.map((item) => ({ ...item, key: item.href, href: siteHref(item.href) }));
|
|
91
93
|
|
|
92
94
|
function rangesFor(
|
|
93
95
|
matches: readonly SearchMatch[] | undefined,
|
|
@@ -140,6 +142,7 @@ function searchPreview(
|
|
|
140
142
|
|
|
141
143
|
export function SiteHeader() {
|
|
142
144
|
const pathname = usePathname() || "/";
|
|
145
|
+
const t = ui(routeLang(pathname));
|
|
143
146
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
144
147
|
const [searchOpen, setSearchOpen] = useState(false);
|
|
145
148
|
const [query, setQuery] = useState("");
|
|
@@ -189,7 +192,12 @@ export function SiteHeader() {
|
|
|
189
192
|
useEffect(() => {
|
|
190
193
|
const system = window.matchMedia("(prefers-color-scheme: dark)");
|
|
191
194
|
const apply = () => {
|
|
192
|
-
|
|
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
|
+
}
|
|
193
201
|
const useDark = resolveDark(stored, system.matches);
|
|
194
202
|
document.documentElement.dataset.theme = useDark ? "dark" : "light";
|
|
195
203
|
setDark(useDark);
|
|
@@ -295,7 +303,11 @@ export function SiteHeader() {
|
|
|
295
303
|
const next = !dark;
|
|
296
304
|
setDark(next);
|
|
297
305
|
document.documentElement.dataset.theme = next ? "dark" : "light";
|
|
298
|
-
|
|
306
|
+
try {
|
|
307
|
+
window.localStorage.setItem("theme", next ? "dark" : "light");
|
|
308
|
+
} catch {
|
|
309
|
+
// Storage blocked: the choice lasts for this page only.
|
|
310
|
+
}
|
|
299
311
|
};
|
|
300
312
|
|
|
301
313
|
const current = (href: string) => isCurrentNav(pathname, href);
|
|
@@ -313,29 +325,30 @@ export function SiteHeader() {
|
|
|
313
325
|
{brand.logo ? <span className="sh-mark" aria-hidden="true" dangerouslySetInnerHTML={{ __html: brand.logo }} /> : null}
|
|
314
326
|
{siteConfig.name}
|
|
315
327
|
</Link>
|
|
316
|
-
<nav className="sh-nav" aria-label=
|
|
328
|
+
<nav className="sh-nav" aria-label={t.siteNav}>
|
|
317
329
|
{nav.map((item) => (
|
|
318
330
|
<Link key={item.href} href={item.href} aria-current={current(item.href) ? "page" : undefined}>
|
|
319
|
-
{item.label}
|
|
331
|
+
{t.navLabels[item.key] ?? item.label}
|
|
320
332
|
</Link>
|
|
321
333
|
))}
|
|
322
334
|
</nav>
|
|
323
335
|
<div className="sh-tools">
|
|
324
|
-
<button ref={searchTriggerRef} className="sh-search" type="button" onClick={openSearch} onPointerEnter={prefetchSearch} onFocus={prefetchSearch} aria-label=
|
|
336
|
+
<button ref={searchTriggerRef} className="sh-search" type="button" onClick={openSearch} onPointerEnter={prefetchSearch} onFocus={prefetchSearch} aria-label={t.searchArticles} aria-expanded={searchOpen} aria-controls="site-search-dialog">
|
|
325
337
|
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
326
338
|
<circle cx="9" cy="9" r="5.8" />
|
|
327
339
|
<path d="M13.3 13.3 17 17" />
|
|
328
340
|
</svg>
|
|
329
|
-
<span className="lbl"
|
|
341
|
+
<span className="lbl">{t.search}</span>
|
|
330
342
|
<kbd><span className="kbd-mod kbd-mac">⌘</span><span className="kbd-mod kbd-other">Ctrl</span>{" "}K</kbd>
|
|
331
343
|
</button>
|
|
332
|
-
<button className="sh-icon sh-search-m" type="button" onClick={openSearch} onPointerDown={prefetchSearch} aria-label=
|
|
344
|
+
<button className="sh-icon sh-search-m" type="button" onClick={openSearch} onPointerDown={prefetchSearch} aria-label={t.searchArticles}>
|
|
333
345
|
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
334
346
|
<circle cx="9" cy="9" r="5.8" />
|
|
335
347
|
<path d="M13.3 13.3 17 17" />
|
|
336
348
|
</svg>
|
|
337
349
|
</button>
|
|
338
|
-
|
|
350
|
+
{isHandbookPath(pathname) ? <HandbookLangSwitch /> : null}
|
|
351
|
+
<button className="sh-icon sh-theme" type="button" onClick={toggleTheme} aria-label={dark ? t.toLight : t.toDark}>
|
|
339
352
|
<svg className="i-moon" viewBox="0 0 20 20" aria-hidden="true">
|
|
340
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" />
|
|
341
354
|
</svg>
|
|
@@ -344,27 +357,27 @@ export function SiteHeader() {
|
|
|
344
357
|
<path d="M10 1.8v2M10 16.2v2M1.8 10h2M16.2 10h2M4.2 4.2l1.4 1.4M14.4 14.4l1.4 1.4M4.2 15.8l1.4-1.4M14.4 5.6l1.4-1.4" />
|
|
345
358
|
</svg>
|
|
346
359
|
</button>
|
|
347
|
-
<button className="sh-icon sh-menu-btn" type="button" aria-expanded={menuOpen} aria-controls="sh-menu" aria-label={menuOpen ?
|
|
360
|
+
<button className="sh-icon sh-menu-btn" type="button" aria-expanded={menuOpen} aria-controls="sh-menu" aria-label={menuOpen ? t.closeMenu : t.openMenu} onClick={() => setMenuOpen((open) => !open)}>
|
|
348
361
|
{menuOpen ? <X size={18} /> : <span aria-hidden="true">☰</span>}
|
|
349
362
|
</button>
|
|
350
363
|
{/* Without JavaScript the button above cannot open anything: these two links take its place
|
|
351
364
|
and open or close the menu through :target on the fixed anchors above (shell.css). */}
|
|
352
|
-
<a className="sh-icon sh-menu-nojs" id="sh-menu-open" href="#sh-menu-on" aria-label=
|
|
365
|
+
<a className="sh-icon sh-menu-nojs" id="sh-menu-open" href="#sh-menu-on" aria-label={t.openMenu}>
|
|
353
366
|
<span aria-hidden="true">☰</span>
|
|
354
367
|
</a>
|
|
355
|
-
<a className="sh-icon sh-menu-nojs-close" href="#sh-menu-off" aria-label=
|
|
368
|
+
<a className="sh-icon sh-menu-nojs-close" href="#sh-menu-off" aria-label={t.closeMenu}>
|
|
356
369
|
<X size={18} />
|
|
357
370
|
</a>
|
|
358
371
|
</div>
|
|
359
372
|
</div>
|
|
360
|
-
<nav className={`sh-menu${menuOpen ? " is-open" : ""}`} id="sh-menu" aria-label=
|
|
373
|
+
<nav className={`sh-menu${menuOpen ? " is-open" : ""}`} id="sh-menu" aria-label={t.siteNavMobile}>
|
|
361
374
|
{nav.map((item) => (
|
|
362
375
|
<Link key={item.href} href={item.href} aria-current={current(item.href) ? "page" : undefined} onClick={() => setMenuOpen(false)}>
|
|
363
|
-
{item.label}
|
|
376
|
+
{t.navLabels[item.key] ?? item.label}
|
|
364
377
|
</Link>
|
|
365
378
|
))}
|
|
366
379
|
<button className="sh-menu-theme" {...themeSwitchProps(dark, toggleTheme)}>
|
|
367
|
-
<span
|
|
380
|
+
<span>{t.darkMode}</span>
|
|
368
381
|
<span className="sh-switch" aria-hidden="true" />
|
|
369
382
|
</button>
|
|
370
383
|
</nav>
|
|
@@ -388,7 +401,7 @@ export function SiteHeader() {
|
|
|
388
401
|
onKeyDown={handleSearchKeyDown}
|
|
389
402
|
>
|
|
390
403
|
<h2 className="sr-only" id="site-search-title">
|
|
391
|
-
|
|
404
|
+
{t.searchTitle}
|
|
392
405
|
</h2>
|
|
393
406
|
<div className="search-input-wrap">
|
|
394
407
|
<MagnifyingGlass size={20} aria-hidden="true" />
|
|
@@ -400,8 +413,8 @@ export function SiteHeader() {
|
|
|
400
413
|
setActiveResult(-1);
|
|
401
414
|
resultRefs.current = [];
|
|
402
415
|
}}
|
|
403
|
-
placeholder=
|
|
404
|
-
aria-label=
|
|
416
|
+
placeholder={t.searchPlaceholder}
|
|
417
|
+
aria-label={t.searchInput}
|
|
405
418
|
role="combobox"
|
|
406
419
|
aria-expanded="true"
|
|
407
420
|
aria-autocomplete="list"
|
|
@@ -410,7 +423,7 @@ export function SiteHeader() {
|
|
|
410
423
|
activeResult >= 0 ? `site-search-result-${activeResult}` : undefined
|
|
411
424
|
}
|
|
412
425
|
/>
|
|
413
|
-
<button onClick={closeSearch} aria-label=
|
|
426
|
+
<button onClick={closeSearch} aria-label={t.closeSearch}>
|
|
414
427
|
<X size={19} />
|
|
415
428
|
</button>
|
|
416
429
|
</div>
|
|
@@ -418,11 +431,11 @@ export function SiteHeader() {
|
|
|
418
431
|
className="search-results"
|
|
419
432
|
id="site-search-results"
|
|
420
433
|
role="listbox"
|
|
421
|
-
aria-label=
|
|
434
|
+
aria-label={t.searchResults}
|
|
422
435
|
>
|
|
423
436
|
{query && engine && resultTotal === 0 ? null : (
|
|
424
437
|
<p className="search-label">
|
|
425
|
-
{query ?
|
|
438
|
+
{query ? t.resultCount(resultTotal) : t.recent}
|
|
426
439
|
</p>
|
|
427
440
|
)}
|
|
428
441
|
{results.map((result, index) => {
|
|
@@ -464,13 +477,13 @@ export function SiteHeader() {
|
|
|
464
477
|
{results.length === 0 && (
|
|
465
478
|
<p className="empty-state">
|
|
466
479
|
{/* The query is a React text child, so it is escaped. */}
|
|
467
|
-
{engine ?
|
|
480
|
+
{engine ? t.noResults(query.trim()) : engineFailed ? t.searchFailed : t.loading}
|
|
468
481
|
</p>
|
|
469
482
|
)}
|
|
470
483
|
</div>
|
|
471
484
|
<footer className="search-footer">
|
|
472
|
-
<span
|
|
473
|
-
<span><kbd>ESC</kbd>
|
|
485
|
+
<span>{t.fuzzy}</span>
|
|
486
|
+
<span><kbd>ESC</kbd> {t.escToClose}</span>
|
|
474
487
|
</footer>
|
|
475
488
|
</section>
|
|
476
489
|
</div>
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from "react";
|
|
12
12
|
import { List, X } from "@phosphor-icons/react";
|
|
13
|
+
import { ui, type UiLang } from "../lib/i18n";
|
|
13
14
|
|
|
14
15
|
type Heading = { depth: number; text: string; id: string; no?: string };
|
|
15
16
|
|
|
@@ -115,11 +116,16 @@ const scrollToHeading = (id: string) => {
|
|
|
115
116
|
|
|
116
117
|
export function TableOfContents({
|
|
117
118
|
headings,
|
|
118
|
-
label
|
|
119
|
+
label,
|
|
120
|
+
lang,
|
|
119
121
|
}: {
|
|
120
122
|
headings: Heading[];
|
|
121
123
|
label?: string;
|
|
124
|
+
/** Defaults to the site language; the bilingual handbook passes its own. */
|
|
125
|
+
lang?: UiLang;
|
|
122
126
|
}) {
|
|
127
|
+
const t = ui(lang);
|
|
128
|
+
label ??= t.toc;
|
|
123
129
|
const [activeId, setActiveId] = useState(headings[0]?.id);
|
|
124
130
|
const [collapsedSections, setCollapsedSections] = useState<Set<string>>(
|
|
125
131
|
() => new Set(),
|
|
@@ -368,7 +374,7 @@ export function TableOfContents({
|
|
|
368
374
|
type="button"
|
|
369
375
|
aria-controls={childListId}
|
|
370
376
|
aria-expanded={!collapsed}
|
|
371
|
-
aria-label={
|
|
377
|
+
aria-label={t.tocToggle(collapsed, node.text)}
|
|
372
378
|
onClick={() => toggleSection(node.id)}
|
|
373
379
|
>
|
|
374
380
|
<span aria-hidden="true">{collapsed ? "+" : "−"}</span>
|
|
@@ -396,25 +402,25 @@ export function TableOfContents({
|
|
|
396
402
|
type="button"
|
|
397
403
|
aria-controls={panelId}
|
|
398
404
|
aria-expanded={mobileOpen}
|
|
399
|
-
aria-label={
|
|
405
|
+
aria-label={`${t.open}${label}${activeHeading ? `${t.currentSection}${activeHeading.text}` : ""}`}
|
|
400
406
|
tabIndex={mobileOpen ? -1 : 0}
|
|
401
407
|
data-mobile-open={mobileOpen}
|
|
402
408
|
onClick={() => setMobileOpen(true)}
|
|
403
409
|
>
|
|
404
410
|
<List size={17} weight="bold" aria-hidden="true" />
|
|
405
|
-
<span
|
|
411
|
+
<span>{t.tocShort}</span>
|
|
406
412
|
</button>
|
|
407
413
|
{/* Without JavaScript the button above does nothing. This link opens the
|
|
408
414
|
drawer through :target instead; CSS shows it only when scripting is
|
|
409
415
|
off, so with JavaScript nothing here is visible. */}
|
|
410
|
-
<a className="toc-nojs-trigger" id={`${panelId}-open`} href={`#${panelId}`} aria-label={
|
|
416
|
+
<a className="toc-nojs-trigger" id={`${panelId}-open`} href={`#${panelId}`} aria-label={`${t.open}${label}`}>
|
|
411
417
|
<List size={17} weight="bold" aria-hidden="true" />
|
|
412
|
-
<span
|
|
418
|
+
<span>{t.tocShort}</span>
|
|
413
419
|
</a>
|
|
414
420
|
<button
|
|
415
421
|
className="toc-mobile-backdrop"
|
|
416
422
|
type="button"
|
|
417
|
-
aria-label={
|
|
423
|
+
aria-label={`${t.close}${label}`}
|
|
418
424
|
tabIndex={mobileOpen ? 0 : -1}
|
|
419
425
|
data-mobile-open={mobileOpen}
|
|
420
426
|
onClick={() => closeMobileToc(true)}
|
|
@@ -431,12 +437,12 @@ export function TableOfContents({
|
|
|
431
437
|
ref={mobileCloseRef}
|
|
432
438
|
className="toc-mobile-close"
|
|
433
439
|
type="button"
|
|
434
|
-
aria-label={
|
|
440
|
+
aria-label={`${t.close}${label}`}
|
|
435
441
|
onClick={() => closeMobileToc(true)}
|
|
436
442
|
>
|
|
437
443
|
<X size={18} aria-hidden="true" />
|
|
438
444
|
</button>
|
|
439
|
-
<a className="toc-nojs-close" href={`#${panelId}-open`} aria-label={
|
|
445
|
+
<a className="toc-nojs-close" href={`#${panelId}-open`} aria-label={`${t.close}${label}`}>
|
|
440
446
|
<X size={18} aria-hidden="true" />
|
|
441
447
|
</a>
|
|
442
448
|
</div>
|
|
@@ -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
|
-
?
|
|
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={
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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
|
)}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { ThoughtCard } from "./ThoughtCard";
|
|
3
|
+
import { FeedMore } from "./FeedMore";
|
|
4
|
+
import { getPublishedThoughts } from "../lib/public-thoughts";
|
|
5
|
+
import { ui } from "../lib/i18n";
|
|
6
|
+
import { siteHref } from "../lib/site-href";
|
|
7
|
+
import { siteConfig } from "../site.config";
|
|
8
|
+
import { pageCount, pageItems, pagePath, pageSizeOf } from "../../ui/garden/paging.mjs";
|
|
9
|
+
|
|
10
|
+
export const thoughtsPageSize = () => pageSizeOf(siteConfig.pages.thoughts.pageSize);
|
|
11
|
+
export const thoughtsPageCount = () => pageCount(getPublishedThoughts().length, thoughtsPageSize());
|
|
12
|
+
export const thoughtsPageHref = (page: number) => siteHref(pagePath("/thoughts", page));
|
|
13
|
+
|
|
14
|
+
/** One page of 随想 and the link (or automatic loading) to the next. */
|
|
15
|
+
export function ThoughtFeed({ page }: { page: number }) {
|
|
16
|
+
const t = ui();
|
|
17
|
+
const all = getPublishedThoughts();
|
|
18
|
+
const total = thoughtsPageCount();
|
|
19
|
+
const items = pageItems(all, page, thoughtsPageSize());
|
|
20
|
+
const next = page < total ? thoughtsPageHref(page + 1) : null;
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<div className="thought-stream" data-feed-page={page}>
|
|
24
|
+
{items.map((thought) => (
|
|
25
|
+
<ThoughtCard key={thought.slug} thought={thought} />
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
{page > 1 ? (
|
|
29
|
+
<p className="feed-prev">
|
|
30
|
+
<Link href={thoughtsPageHref(page - 1)} rel="prev">{t.prevPage}</Link>
|
|
31
|
+
<span>{t.pageOf(page, total)}</span>
|
|
32
|
+
</p>
|
|
33
|
+
) : null}
|
|
34
|
+
<FeedMore
|
|
35
|
+
next={next}
|
|
36
|
+
streamSelector=".thought-stream"
|
|
37
|
+
labels={{ nextPage: t.nextPage, loadingMore: t.loadingMore, loadFailed: t.loadFailed, feedEnd: t.feedEnd }}
|
|
38
|
+
/>
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -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"
|
|
7
|
+
if (!payload) return <p className="external-embed-error">{ui(lang).uiBlockCorrupt}</p>;
|
|
7
8
|
return <UiFigure payload={payload} />;
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
4
|
+
import { siteLang, ui, type UiLang } from "../lib/i18n";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The language of the framework chrome inside a page body (copy buttons, code
|
|
8
|
+
* tabs, file cards...). It is the site language everywhere except the /garden/
|
|
9
|
+
* handbook, whose pages set it from their own route (/garden/en/ is English).
|
|
10
|
+
*/
|
|
11
|
+
const UiLangContext = createContext<UiLang>(siteLang);
|
|
12
|
+
|
|
13
|
+
export function UiLangProvider({ lang, children }: { lang?: UiLang; children: ReactNode }) {
|
|
14
|
+
return <UiLangContext.Provider value={lang ?? siteLang}>{children}</UiLangContext.Provider>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useUiLang = () => useContext(UiLangContext);
|
|
18
|
+
export const useUi = () => ui(useContext(UiLangContext));
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Quotes,
|
|
9
9
|
Repeat,
|
|
10
10
|
} from "@phosphor-icons/react";
|
|
11
|
+
import { siteLang, ui } from "../lib/i18n";
|
|
11
12
|
|
|
12
13
|
type WebmentionEntry = {
|
|
13
14
|
author?: {
|
|
@@ -93,34 +94,34 @@ export function Webmentions({
|
|
|
93
94
|
<div className="webmention-title">
|
|
94
95
|
<LinkSimple size={21} />
|
|
95
96
|
<div>
|
|
96
|
-
<h2 id="webmentions-heading"
|
|
97
|
-
<p
|
|
97
|
+
<h2 id="webmentions-heading">{ui().webmentions}</h2>
|
|
98
|
+
<p>{ui().webmentionsLead}</p>
|
|
98
99
|
</div>
|
|
99
100
|
</div>
|
|
100
101
|
|
|
101
102
|
{!endpoint && (
|
|
102
103
|
<div className="integration-notice">
|
|
103
104
|
<Quotes size={20} weight="fill" />
|
|
104
|
-
<p
|
|
105
|
+
<p>{ui().webmentionIdleNote}</p>
|
|
105
106
|
</div>
|
|
106
107
|
)}
|
|
107
108
|
|
|
108
109
|
{endpoint && loading && (
|
|
109
110
|
<p className="integration-status" role="status">
|
|
110
|
-
|
|
111
|
+
{ui().webmentionLoading}
|
|
111
112
|
</p>
|
|
112
113
|
)}
|
|
113
114
|
|
|
114
115
|
{endpoint && failed && (
|
|
115
116
|
<p className="integration-status" role="status">
|
|
116
|
-
|
|
117
|
+
{ui().webmentionUnavailable}
|
|
117
118
|
</p>
|
|
118
119
|
)}
|
|
119
120
|
|
|
120
121
|
{endpoint && !loading && !failed && entries.length === 0 && (
|
|
121
122
|
<div className="integration-notice">
|
|
122
123
|
<Quotes size={20} weight="fill" />
|
|
123
|
-
<p
|
|
124
|
+
<p>{ui().webmentionEmpty}</p>
|
|
124
125
|
</div>
|
|
125
126
|
)}
|
|
126
127
|
|
|
@@ -129,15 +130,15 @@ export function Webmentions({
|
|
|
129
130
|
<span>
|
|
130
131
|
<Heart size={16} weight="fill" />
|
|
131
132
|
<Repeat size={16} />
|
|
132
|
-
{reactions.length}
|
|
133
|
+
{ui().webmentionReactionCount(reactions.length)}
|
|
133
134
|
</span>
|
|
134
|
-
<div className="webmention-facepile" aria-label=
|
|
135
|
+
<div className="webmention-facepile" aria-label={ui().webmentionRespondersLabel}>
|
|
135
136
|
{reactions.slice(0, 16).map((entry, index) => (
|
|
136
137
|
<a
|
|
137
138
|
href={entry.url}
|
|
138
139
|
target="_blank"
|
|
139
140
|
rel="noopener noreferrer"
|
|
140
|
-
aria-label={entry.author?.name ??
|
|
141
|
+
aria-label={entry.author?.name ?? ui().webmentionViewResponse}
|
|
141
142
|
key={`${entry.url}-${index}`}
|
|
142
143
|
>
|
|
143
144
|
{entry.author?.photo ? (
|
|
@@ -164,10 +165,10 @@ export function Webmentions({
|
|
|
164
165
|
return (
|
|
165
166
|
<article key={`${entry.url}-${index}`}>
|
|
166
167
|
<header>
|
|
167
|
-
<strong>{entry.author?.name ??
|
|
168
|
+
<strong>{entry.author?.name ?? ui().webmentionAuthorFallback}</strong>
|
|
168
169
|
{received && (
|
|
169
170
|
<time dateTime={received}>
|
|
170
|
-
{new Intl.DateTimeFormat("zh-CN", {
|
|
171
|
+
{new Intl.DateTimeFormat(siteLang === "en" ? "en-US" : "zh-CN", {
|
|
171
172
|
year: "numeric",
|
|
172
173
|
month: "2-digit",
|
|
173
174
|
day: "2-digit",
|
|
@@ -181,7 +182,7 @@ export function Webmentions({
|
|
|
181
182
|
target="_blank"
|
|
182
183
|
rel="noopener noreferrer"
|
|
183
184
|
>
|
|
184
|
-
|
|
185
|
+
{ui().webmentionViewOriginal}
|
|
185
186
|
<ArrowSquareOut size={14} />
|
|
186
187
|
</a>
|
|
187
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
|
-
|
|
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" ?
|
|
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>
|