hd-garden 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/garden-ui/SKILL.md +26 -19
- package/README.en.md +57 -0
- package/README.md +31 -46
- package/app/admin/preview/[...path]/page.tsx +1 -1
- package/app/blog/[...path]/page.tsx +11 -3
- package/app/columns/[column]/[...path]/page.tsx +12 -14
- package/app/components/ArticleMeasure.tsx +39 -4
- package/app/components/ArticlePager.tsx +32 -0
- package/app/components/Comments.tsx +2 -1
- package/app/components/FeedMore.tsx +70 -0
- package/app/components/GardenDocsNav.tsx +189 -0
- package/app/components/HandbookLangRedirect.tsx +30 -0
- package/app/components/HandbookLangSwitch.tsx +49 -0
- package/app/components/HandbookView.tsx +187 -0
- package/app/components/MarkdownArticle.tsx +15 -8
- package/app/components/MarkdownImage.tsx +6 -4
- package/app/components/SiteChrome.tsx +11 -2
- package/app/components/SiteFooter.tsx +12 -3
- package/app/components/SiteHeader.tsx +27 -23
- package/app/components/TableOfContents.tsx +15 -9
- package/app/components/ThoughtFeed.tsx +41 -0
- package/app/components/UiLang.tsx +18 -0
- package/app/components/Webmentions.tsx +3 -2
- package/app/components/garden/FilePreview.tsx +13 -11
- package/app/components/garden/GardenBlock.tsx +3 -1
- package/app/components/garden/GardenFile.tsx +9 -7
- package/app/components/garden/blocks.tsx +34 -24
- package/app/components/garden/chrome.tsx +20 -13
- package/app/garden/[...slug]/page.tsx +28 -0
- package/app/garden/en/[...slug]/page.tsx +28 -0
- package/app/garden/en/page.tsx +16 -0
- package/app/garden/page.tsx +7 -141
- package/app/garden-modules.d.ts +18 -0
- package/app/globals.css +10 -10
- package/app/layout.tsx +2 -4
- package/app/lib/content.ts +27 -11
- package/app/lib/garden-docs-meta.ts +138 -0
- package/app/lib/handbook-path.ts +11 -0
- package/app/lib/heading-number.ts +13 -0
- package/app/lib/i18n.ts +222 -0
- package/app/lib/page-alternates.ts +2 -2
- package/app/not-found.tsx +5 -3
- package/app/site.config.ts +1 -1
- package/app/styles/a2.css +613 -13
- package/app/styles/shell.css +6 -0
- package/app/thoughts/page/[n]/page.tsx +44 -0
- package/app/thoughts/page.tsx +2 -6
- package/bin/garden.mjs +4 -0
- package/docs/handbook/ai-writing.md +99 -0
- package/docs/handbook/assets/reading-layout.svg +26 -0
- package/docs/handbook/components/card.md +46 -0
- package/docs/handbook/components/chart.md +54 -0
- package/docs/handbook/components/compare.md +38 -0
- package/docs/handbook/components/figure.md +72 -0
- package/docs/handbook/components/file/checklist.md +17 -0
- package/docs/handbook/components/file.md +34 -0
- package/docs/handbook/components/html.md +69 -0
- package/docs/handbook/components/interactive.md +67 -0
- package/docs/handbook/components/stat.md +57 -0
- package/docs/handbook/components/timeline.md +52 -0
- package/docs/handbook/components.md +30 -0
- package/docs/handbook/config.md +125 -0
- package/docs/handbook/deploy.md +273 -0
- package/docs/handbook/design.md +81 -0
- package/docs/handbook/en/ai-writing.md +99 -0
- package/docs/handbook/en/components/card.md +46 -0
- package/docs/handbook/en/components/chart.md +54 -0
- package/docs/handbook/en/components/compare.md +38 -0
- package/docs/handbook/en/components/figure.md +72 -0
- package/docs/handbook/en/components/file/checklist.md +17 -0
- package/docs/handbook/en/components/file.md +34 -0
- package/docs/handbook/en/components/html.md +69 -0
- package/docs/handbook/en/components/interactive.md +67 -0
- package/docs/handbook/en/components/stat.md +57 -0
- package/docs/handbook/en/components/timeline.md +52 -0
- package/docs/handbook/en/components.md +30 -0
- package/docs/handbook/en/config.md +125 -0
- package/docs/handbook/en/deploy.md +273 -0
- package/docs/handbook/en/design.md +81 -0
- package/docs/handbook/en/index.md +192 -0
- package/docs/handbook/en/start.md +92 -0
- package/docs/handbook/en/workflow.md +81 -0
- package/docs/handbook/en/writing.md +156 -0
- package/docs/handbook/index.md +187 -0
- package/docs/handbook/start.md +92 -0
- package/docs/handbook/workflow.md +81 -0
- package/docs/handbook/writing.md +156 -0
- package/package.json +2 -1
- package/scripts/build-content.mjs +144 -15
- package/scripts/handbook-allowlist.mjs +68 -0
- package/scripts/handbook-facts.mjs +1138 -0
- package/scripts/html-lang.mjs +36 -0
- package/scripts/static-404.mjs +25 -0
- package/scripts/ui-cli.mjs +14 -8
- package/site.config.yaml +2 -2
- package/templates/garden-blocks.md +1 -1
- package/templates/site/package.json +1 -3
- package/templates/site/site.config.yaml +2 -0
- package/ui/core/apply.mjs +22 -4
- package/ui/core/cli-locale.mjs +67 -0
- package/ui/core/fence.mjs +18 -3
- package/ui/core/fix-prompt.mjs +18 -15
- package/ui/core/garden-fence.mjs +64 -0
- package/ui/core/scan.mjs +19 -3
- package/ui/garden/adjacent.mjs +21 -0
- package/ui/garden/catalog.mjs +1 -1
- package/ui/garden/check.mjs +6 -0
- package/ui/garden/html-lang.mjs +16 -0
- package/ui/garden/paging.mjs +30 -0
- package/ui/preview/dist/garden-ui.css +9 -9
- package/ui/preview/dist/garden-ui.esm.js +169 -169
- package/ui/preview/dist/garden-ui.iife.js +169 -169
- package/ui/react/components/chart.tsx +7 -4
- package/ui/react/components/display.tsx +6 -3
- package/ui/react/components/inputs.tsx +6 -3
- package/ui/react/components/layout.tsx +6 -4
- package/ui/react/components/media.tsx +7 -4
- package/ui/react/components/sandbox.tsx +3 -1
- package/ui/react/index.tsx +11 -7
- package/ui/react/lang.tsx +85 -0
- package/ui/react/runtime.tsx +3 -1
- package/ui/react/ui.css +9 -9
- package/worker/index.ts +8 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { BookOpenText, CaretRight, List, X } from "@phosphor-icons/react";
|
|
6
|
+
import {
|
|
7
|
+
COMPONENT_BADGE,
|
|
8
|
+
groupHandbookDocs,
|
|
9
|
+
HANDBOOK_SECTIONS,
|
|
10
|
+
HANDBOOK_UI,
|
|
11
|
+
NAV_SHORT_TITLE,
|
|
12
|
+
handbookPath,
|
|
13
|
+
sectionTitle,
|
|
14
|
+
type GardenDocNavItem,
|
|
15
|
+
type HandbookLang,
|
|
16
|
+
} from "../lib/garden-docs-meta";
|
|
17
|
+
import { siteHref } from "../lib/site-href";
|
|
18
|
+
|
|
19
|
+
export function GardenDocsNav({
|
|
20
|
+
docs,
|
|
21
|
+
currentPath = "",
|
|
22
|
+
lang = "zh",
|
|
23
|
+
}: {
|
|
24
|
+
docs: GardenDocNavItem[];
|
|
25
|
+
currentPath?: string;
|
|
26
|
+
lang?: HandbookLang;
|
|
27
|
+
}) {
|
|
28
|
+
const t = HANDBOOK_UI[lang];
|
|
29
|
+
const groups = groupHandbookDocs(docs);
|
|
30
|
+
const currentDoc = docs.find((doc) => doc.path === currentPath);
|
|
31
|
+
const currentSection = groups.find((group) =>
|
|
32
|
+
group.items.some((item) => item.path === currentPath),
|
|
33
|
+
);
|
|
34
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
35
|
+
const drawerId = useId();
|
|
36
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
37
|
+
const closeRef = useRef<HTMLButtonElement>(null);
|
|
38
|
+
const restoreFocusRef = useRef(false);
|
|
39
|
+
|
|
40
|
+
const closeMobile = useCallback((restoreFocus = false) => {
|
|
41
|
+
restoreFocusRef.current = restoreFocus;
|
|
42
|
+
setMobileOpen(false);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (mobileOpen || !restoreFocusRef.current) return;
|
|
47
|
+
restoreFocusRef.current = false;
|
|
48
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
49
|
+
}, [mobileOpen]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!mobileOpen) return;
|
|
53
|
+
const query = window.matchMedia("(max-width: 1080px)");
|
|
54
|
+
if (!query.matches) return;
|
|
55
|
+
const previousOverflow = document.body.style.overflow;
|
|
56
|
+
const focusFrame = window.requestAnimationFrame(() => {
|
|
57
|
+
closeRef.current?.focus({ preventScroll: true });
|
|
58
|
+
});
|
|
59
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
60
|
+
if (event.key === "Escape") closeMobile(true);
|
|
61
|
+
};
|
|
62
|
+
const onChange = () => {
|
|
63
|
+
if (!query.matches) setMobileOpen(false);
|
|
64
|
+
};
|
|
65
|
+
document.body.style.overflow = "hidden";
|
|
66
|
+
window.addEventListener("keydown", onKeyDown);
|
|
67
|
+
query.addEventListener("change", onChange);
|
|
68
|
+
return () => {
|
|
69
|
+
window.cancelAnimationFrame(focusFrame);
|
|
70
|
+
document.body.style.overflow = previousOverflow;
|
|
71
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
72
|
+
query.removeEventListener("change", onChange);
|
|
73
|
+
};
|
|
74
|
+
}, [closeMobile, mobileOpen]);
|
|
75
|
+
|
|
76
|
+
const isOverview = currentPath === "";
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<>
|
|
80
|
+
<div className="gdoc-mobile-bar" aria-label={t.mobileBar}>
|
|
81
|
+
<button
|
|
82
|
+
ref={triggerRef}
|
|
83
|
+
type="button"
|
|
84
|
+
className="gdoc-mobile-trigger"
|
|
85
|
+
aria-controls={drawerId}
|
|
86
|
+
aria-expanded={mobileOpen}
|
|
87
|
+
onClick={() => setMobileOpen(true)}
|
|
88
|
+
>
|
|
89
|
+
<List size={16} weight="bold" aria-hidden="true" />
|
|
90
|
+
<span>{t.sidebarShort}</span>
|
|
91
|
+
</button>
|
|
92
|
+
<a
|
|
93
|
+
className="gdoc-nojs-trigger"
|
|
94
|
+
id={`${drawerId}-open`}
|
|
95
|
+
href={`#${drawerId}`}
|
|
96
|
+
aria-label={t.openSidebar}
|
|
97
|
+
>
|
|
98
|
+
<List size={16} weight="bold" aria-hidden="true" />
|
|
99
|
+
<span>{t.sidebarShort}</span>
|
|
100
|
+
</a>
|
|
101
|
+
<div className="gdoc-mobile-crumb" aria-hidden="true">
|
|
102
|
+
<span>{sectionTitle(currentSection, lang) ?? t.name}</span>
|
|
103
|
+
<CaretRight size={12} />
|
|
104
|
+
<strong>{currentDoc ? (lang === "en" ? currentDoc.navTitle ?? currentDoc.title : NAV_SHORT_TITLE[currentDoc.path] ?? currentDoc.title) : t.overviewHint}</strong>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
className="gdoc-backdrop"
|
|
111
|
+
aria-label={t.closeSidebar}
|
|
112
|
+
tabIndex={mobileOpen ? 0 : -1}
|
|
113
|
+
data-open={mobileOpen}
|
|
114
|
+
onClick={() => closeMobile(true)}
|
|
115
|
+
/>
|
|
116
|
+
|
|
117
|
+
<aside
|
|
118
|
+
id={drawerId}
|
|
119
|
+
className="gdoc-sidebar"
|
|
120
|
+
aria-label={t.sidebar}
|
|
121
|
+
data-open={mobileOpen}
|
|
122
|
+
>
|
|
123
|
+
<div className="gdoc-sidebar-head">
|
|
124
|
+
<Link
|
|
125
|
+
href={siteHref(handbookPath(lang))}
|
|
126
|
+
className="gdoc-overview-link"
|
|
127
|
+
aria-current={isOverview ? "page" : undefined}
|
|
128
|
+
onClick={() => closeMobile(false)}
|
|
129
|
+
>
|
|
130
|
+
<span className="gdoc-overview-mark">
|
|
131
|
+
<BookOpenText size={16} weight="duotone" aria-hidden="true" />
|
|
132
|
+
<span>{t.name}</span>
|
|
133
|
+
</span>
|
|
134
|
+
<small>{t.overviewHint}</small>
|
|
135
|
+
</Link>
|
|
136
|
+
<button
|
|
137
|
+
ref={closeRef}
|
|
138
|
+
type="button"
|
|
139
|
+
className="gdoc-mobile-close"
|
|
140
|
+
aria-label={t.closeSidebar}
|
|
141
|
+
onClick={() => closeMobile(true)}
|
|
142
|
+
>
|
|
143
|
+
<X size={17} aria-hidden="true" />
|
|
144
|
+
</button>
|
|
145
|
+
<a
|
|
146
|
+
className="gdoc-nojs-close"
|
|
147
|
+
href={`#${drawerId}-open`}
|
|
148
|
+
aria-label={t.closeSidebar}
|
|
149
|
+
>
|
|
150
|
+
<X size={17} aria-hidden="true" />
|
|
151
|
+
</a>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<nav className="gdoc-nav" aria-label={t.chapters}>
|
|
155
|
+
{groups.map((group) => (
|
|
156
|
+
<div key={group.id} className="gdoc-nav-group">
|
|
157
|
+
<h2 className="gdoc-nav-group-head">{sectionTitle(HANDBOOK_SECTIONS.find((sec) => sec.id === group.id), lang) ?? group.title}</h2>
|
|
158
|
+
<ol className="gdoc-nav-list">
|
|
159
|
+
{group.items.map((item) => {
|
|
160
|
+
const active = item.path === currentPath;
|
|
161
|
+
const badge = COMPONENT_BADGE[item.path];
|
|
162
|
+
const label = lang === "en" ? item.navTitle ?? item.title : NAV_SHORT_TITLE[item.path] ?? item.title;
|
|
163
|
+
return (
|
|
164
|
+
<li key={item.path} className={active ? "is-active" : undefined}>
|
|
165
|
+
<Link
|
|
166
|
+
href={siteHref(handbookPath(lang, item.path))}
|
|
167
|
+
aria-current={active ? "page" : undefined}
|
|
168
|
+
onClick={() => closeMobile(false)}
|
|
169
|
+
>
|
|
170
|
+
{badge ? (
|
|
171
|
+
<>
|
|
172
|
+
<code className="gdoc-comp-pill">{badge}</code>
|
|
173
|
+
<span className="gdoc-comp-label">{label}</span>
|
|
174
|
+
</>
|
|
175
|
+
) : (
|
|
176
|
+
<span>{label}</span>
|
|
177
|
+
)}
|
|
178
|
+
</Link>
|
|
179
|
+
</li>
|
|
180
|
+
);
|
|
181
|
+
})}
|
|
182
|
+
</ol>
|
|
183
|
+
</div>
|
|
184
|
+
))}
|
|
185
|
+
</nav>
|
|
186
|
+
</aside>
|
|
187
|
+
</>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { HANDBOOK_LANG_KEY, type HandbookLang } from "../lib/garden-docs-meta";
|
|
5
|
+
import { swapHandbookLang } from "../lib/handbook-path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* First visit: follow the browser language. Afterwards: follow the reader's
|
|
9
|
+
* saved choice (set by the 中 / EN switch). Only moves to a page that exists,
|
|
10
|
+
* which the hreflang link in <head> tells us.
|
|
11
|
+
*/
|
|
12
|
+
export function HandbookLangRedirect({ lang }: { lang: HandbookLang }) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
let saved: string | null = null;
|
|
15
|
+
try {
|
|
16
|
+
saved = localStorage.getItem(HANDBOOK_LANG_KEY);
|
|
17
|
+
} catch {}
|
|
18
|
+
const wanted: HandbookLang = saved === "en" || saved === "zh"
|
|
19
|
+
? saved
|
|
20
|
+
: /^en\b/i.test(navigator.language || "") ? "en" : "zh";
|
|
21
|
+
if (wanted === lang) return;
|
|
22
|
+
const link = document.querySelector<HTMLLinkElement>(`link[rel="alternate"][hreflang="${wanted === "en" ? "en" : "zh-CN"}"]`);
|
|
23
|
+
if (!link?.href) return;
|
|
24
|
+
// The hreflang link says the page exists; the address comes from this one,
|
|
25
|
+
// so the origin and any basePath stay as they are.
|
|
26
|
+
const pathname = swapHandbookLang(location.pathname, wanted);
|
|
27
|
+
if (pathname && pathname !== location.pathname) location.replace(pathname + location.search + location.hash);
|
|
28
|
+
}, [lang]);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { HANDBOOK_LANG_KEY, handbookBase, type HandbookLang } from "../lib/garden-docs-meta";
|
|
6
|
+
import { siteHref } from "../lib/site-href";
|
|
7
|
+
import { swapHandbookLang } from "../lib/handbook-path";
|
|
8
|
+
|
|
9
|
+
const noSubscribe = () => () => {};
|
|
10
|
+
|
|
11
|
+
/** The counterpart page from the hreflang link in <head>, on this origin. */
|
|
12
|
+
function counterpartHref(target: HandbookLang) {
|
|
13
|
+
const link = document.querySelector<HTMLLinkElement>(`link[rel="alternate"][hreflang="${target === "en" ? "en" : "zh-CN"}"]`);
|
|
14
|
+
if (link?.href) return swapHandbookLang(location.pathname, target);
|
|
15
|
+
return target === "zh" ? siteHref(`${handbookBase("zh")}/`) : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 「中 / EN」 in the top bar on handbook pages. Goes to the counterpart page. */
|
|
19
|
+
export function HandbookLangSwitch() {
|
|
20
|
+
const pathname = usePathname() ?? "";
|
|
21
|
+
const lang: HandbookLang = /\/garden\/en(\/|$)/.test(pathname) ? "en" : "zh";
|
|
22
|
+
const target: HandbookLang = lang === "en" ? "zh" : "en";
|
|
23
|
+
const href = useSyncExternalStore(
|
|
24
|
+
noSubscribe,
|
|
25
|
+
() => counterpartHref(target),
|
|
26
|
+
() => null,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const remember = () => {
|
|
30
|
+
try {
|
|
31
|
+
localStorage.setItem(HANDBOOK_LANG_KEY, target);
|
|
32
|
+
} catch {}
|
|
33
|
+
};
|
|
34
|
+
const label = (
|
|
35
|
+
<>
|
|
36
|
+
<span aria-current={lang === "zh" ? "true" : undefined}>中</span>
|
|
37
|
+
<span aria-hidden="true"> / </span>
|
|
38
|
+
<span aria-current={lang === "en" ? "true" : undefined}>EN</span>
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
if (!href) {
|
|
42
|
+
return <span className="sh-lang is-disabled" title="No English version of this page yet">{label}</span>;
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
<a className="sh-lang" href={href} hrefLang={target === "en" ? "en" : "zh-CN"} lang={target === "en" ? "en" : undefined} onClick={remember} aria-label={target === "en" ? "Read in English" : "Switch to Chinese"}>
|
|
46
|
+
{label}
|
|
47
|
+
</a>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
orderedHandbookDocs,
|
|
21
|
+
sectionTitle,
|
|
22
|
+
toHandbookNavItems,
|
|
23
|
+
type HandbookLang,
|
|
24
|
+
} from "../lib/garden-docs-meta";
|
|
25
|
+
import { siteConfig } from "../site.config";
|
|
26
|
+
|
|
27
|
+
const other = (lang: HandbookLang): HandbookLang => (lang === "en" ? "zh" : "en");
|
|
28
|
+
const hreflang = (lang: HandbookLang) => (lang === "en" ? "en" : "zh-CN");
|
|
29
|
+
|
|
30
|
+
/** canonical + hreflang links; the counterpart is listed only when it exists. */
|
|
31
|
+
export function handbookLanguages(lang: HandbookLang, docPath: string) {
|
|
32
|
+
if (!hasHandbookPage(other(lang), docPath)) return undefined;
|
|
33
|
+
const zh = siteHref(handbookPath("zh", docPath));
|
|
34
|
+
const en = siteHref(handbookPath("en", docPath));
|
|
35
|
+
return { "zh-CN": zh, en, "x-default": zh };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const handbookAlternates = (lang: HandbookLang, docPath: string) =>
|
|
39
|
+
pageAlternates(handbookPath(lang, docPath), handbookLanguages(lang, docPath));
|
|
40
|
+
|
|
41
|
+
export async function handbookDocMetadata(lang: HandbookLang, doc: GardenDocEntry | undefined) {
|
|
42
|
+
if (!doc) return {};
|
|
43
|
+
const t = HANDBOOK_UI[lang];
|
|
44
|
+
const baseUrl = await getSiteUrl();
|
|
45
|
+
const pathname = siteHref(handbookPath(lang, doc.path));
|
|
46
|
+
return {
|
|
47
|
+
title: `${doc.title} · ${t.name}`,
|
|
48
|
+
description: doc.description,
|
|
49
|
+
alternates: handbookAlternates(lang, doc.path),
|
|
50
|
+
openGraph: {
|
|
51
|
+
type: "article",
|
|
52
|
+
url: absoluteUrl(pathname, baseUrl),
|
|
53
|
+
title: `${doc.title} · ${t.name}`,
|
|
54
|
+
description: doc.description,
|
|
55
|
+
locale: lang === "en" ? "en_US" : "zh_CN",
|
|
56
|
+
},
|
|
57
|
+
} satisfies Metadata;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function HandbookOverview({ lang }: { lang: HandbookLang }) {
|
|
61
|
+
const t = HANDBOOK_UI[lang];
|
|
62
|
+
const docs = handbookDocs(lang);
|
|
63
|
+
const indexDoc = handbookIndexDoc(lang);
|
|
64
|
+
const navItems = toHandbookNavItems(docs);
|
|
65
|
+
const firstDoc = orderedHandbookDocs(docs)[0] ?? null;
|
|
66
|
+
const content = indexDoc?.content ?? "";
|
|
67
|
+
const headings = extractHeadings(content);
|
|
68
|
+
// The site config title/subtitle is written in the site's own language.
|
|
69
|
+
const configured = lang === "zh" ? siteConfig.pages.garden : { title: "", subtitle: "" };
|
|
70
|
+
const title = configured.title?.trim() || indexDoc?.title || t.name;
|
|
71
|
+
const subtitle = configured.subtitle?.trim() || indexDoc?.description || "";
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<main className="column-reading-page garden-doc-page" lang={hreflang(lang)}>
|
|
75
|
+
<HandbookLangRedirect lang={lang} />
|
|
76
|
+
<GardenDocsNav docs={navItems} currentPath="" lang={lang} />
|
|
77
|
+
|
|
78
|
+
<article className="article-column garden-doc-article h-entry">
|
|
79
|
+
<header className="gdoc-article-head">
|
|
80
|
+
<div className="gdoc-eyebrow">
|
|
81
|
+
<span>{t.name}</span>
|
|
82
|
+
<span aria-hidden="true">/</span>
|
|
83
|
+
<span>{t.overview}</span>
|
|
84
|
+
</div>
|
|
85
|
+
<ArticleTitle title={title} />
|
|
86
|
+
{subtitle ? <p className="gdoc-article-deck p-summary">{subtitle}</p> : null}
|
|
87
|
+
</header>
|
|
88
|
+
|
|
89
|
+
<ArticleMeasure />
|
|
90
|
+
|
|
91
|
+
<div className="e-content">
|
|
92
|
+
<MarkdownArticle content={content} lang={lang} />
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<ArticleReferences references={indexDoc?.references} />
|
|
96
|
+
|
|
97
|
+
{firstDoc ? (
|
|
98
|
+
<nav className="gdoc-pager" aria-label={t.pager}>
|
|
99
|
+
<Link href={siteHref(handbookPath(lang, firstDoc.path))} className="gdoc-pager-card is-next">
|
|
100
|
+
<span className="gdoc-pager-dir">
|
|
101
|
+
{t.next}
|
|
102
|
+
<ArrowRight size={14} aria-hidden="true" />
|
|
103
|
+
</span>
|
|
104
|
+
<strong>{firstDoc.title}</strong>
|
|
105
|
+
</Link>
|
|
106
|
+
</nav>
|
|
107
|
+
) : null}
|
|
108
|
+
</article>
|
|
109
|
+
|
|
110
|
+
<TableOfContents headings={headings} label={t.toc} lang={lang} />
|
|
111
|
+
</main>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function HandbookDoc({ lang, doc }: { lang: HandbookLang; doc: GardenDocEntry }) {
|
|
116
|
+
const t = HANDBOOK_UI[lang];
|
|
117
|
+
const docs = handbookDocs(lang);
|
|
118
|
+
const ordered = orderedHandbookDocs(docs);
|
|
119
|
+
const navItems = toHandbookNavItems(docs);
|
|
120
|
+
const index = ordered.findIndex((item) => item.path === doc.path);
|
|
121
|
+
const prev = index > 0 ? ordered[index - 1] : null;
|
|
122
|
+
const next = index >= 0 && index < ordered.length - 1 ? ordered[index + 1] : null;
|
|
123
|
+
const headings = extractHeadings(doc.content);
|
|
124
|
+
const sectionMeta = HANDBOOK_SECTIONS.find((sec) => sec.id === doc.section);
|
|
125
|
+
const componentBadge = COMPONENT_BADGE[doc.path];
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<main className="column-reading-page garden-doc-page" lang={hreflang(lang)}>
|
|
129
|
+
<HandbookLangRedirect lang={lang} />
|
|
130
|
+
<GardenDocsNav docs={navItems} currentPath={doc.path} lang={lang} />
|
|
131
|
+
|
|
132
|
+
<article className="article-column garden-doc-article h-entry">
|
|
133
|
+
<header className="gdoc-article-head">
|
|
134
|
+
<div className="gdoc-eyebrow">
|
|
135
|
+
<Link href={siteHref(handbookPath(lang))} className="gdoc-eyebrow-root">
|
|
136
|
+
{t.name}
|
|
137
|
+
</Link>
|
|
138
|
+
<span aria-hidden="true">/</span>
|
|
139
|
+
<span>{sectionTitle(sectionMeta, lang) ?? doc.sectionTitle ?? t.guide}</span>
|
|
140
|
+
{componentBadge ? <code className="gdoc-eyebrow-fence">{`garden ${componentBadge}`}</code> : null}
|
|
141
|
+
</div>
|
|
142
|
+
<ArticleTitle title={doc.title} />
|
|
143
|
+
{doc.description ? <p className="gdoc-article-deck p-summary">{doc.description}</p> : null}
|
|
144
|
+
</header>
|
|
145
|
+
|
|
146
|
+
<ArticleMeasure />
|
|
147
|
+
|
|
148
|
+
<div className="e-content">
|
|
149
|
+
<MarkdownArticle content={doc.content} lang={lang} />
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<ArticleReferences references={doc.references} />
|
|
153
|
+
|
|
154
|
+
<nav className="gdoc-pager" aria-label={t.pager}>
|
|
155
|
+
{prev ? (
|
|
156
|
+
<Link href={siteHref(handbookPath(lang, prev.path))} className="gdoc-pager-card is-prev">
|
|
157
|
+
<span className="gdoc-pager-dir">
|
|
158
|
+
<ArrowLeft size={14} aria-hidden="true" />
|
|
159
|
+
{t.prev}
|
|
160
|
+
</span>
|
|
161
|
+
<strong>{prev.title}</strong>
|
|
162
|
+
</Link>
|
|
163
|
+
) : (
|
|
164
|
+
<Link href={siteHref(handbookPath(lang))} className="gdoc-pager-card is-prev">
|
|
165
|
+
<span className="gdoc-pager-dir">
|
|
166
|
+
<ArrowLeft size={14} aria-hidden="true" />
|
|
167
|
+
{t.backToOverview}
|
|
168
|
+
</span>
|
|
169
|
+
<strong>{t.overviewDocTitle}</strong>
|
|
170
|
+
</Link>
|
|
171
|
+
)}
|
|
172
|
+
{next ? (
|
|
173
|
+
<Link href={siteHref(handbookPath(lang, next.path))} className="gdoc-pager-card is-next">
|
|
174
|
+
<span className="gdoc-pager-dir">
|
|
175
|
+
{t.next}
|
|
176
|
+
<ArrowRight size={14} aria-hidden="true" />
|
|
177
|
+
</span>
|
|
178
|
+
<strong>{next.title}</strong>
|
|
179
|
+
</Link>
|
|
180
|
+
) : null}
|
|
181
|
+
</nav>
|
|
182
|
+
</article>
|
|
183
|
+
|
|
184
|
+
<TableOfContents headings={headings} label={t.toc} lang={lang} />
|
|
185
|
+
</main>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { UiLangProvider } from "./UiLang";
|
|
2
|
+
import { 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";
|
|
@@ -8,7 +10,7 @@ import {
|
|
|
8
10
|
type HTMLAttributes,
|
|
9
11
|
type ReactNode,
|
|
10
12
|
} from "react";
|
|
11
|
-
import { articleHeadingBase,
|
|
13
|
+
import { articleHeadingBase, autoNumbersHeadings, displayHeadingLevel, splitHeadingNumber } from "../lib/content";
|
|
12
14
|
import Link from "next/link";
|
|
13
15
|
import ReactMarkdown from "react-markdown";
|
|
14
16
|
import rehypeRaw from "rehype-raw";
|
|
@@ -28,7 +30,7 @@ import { imageDimensions, lookupImageSize } from "../../ui/garden/image-dims.mjs
|
|
|
28
30
|
import { siteConfig, siteLinks } from "@garden/site-config";
|
|
29
31
|
import { articleLink, sameSiteMatcher } from "../../ui/garden/same-site.mjs";
|
|
30
32
|
import { columnRedirects, internalLinkResolver, readRedirects } from "../../ui/garden/internal-links.mjs";
|
|
31
|
-
import { columnHref, columns, postHref, posts, thoughts } from "../lib/content";
|
|
33
|
+
import { columnHref, columns, gardenDocHref, gardenDocs, gardenDocsEn, postHref, posts, thoughts } from "../lib/content";
|
|
32
34
|
import { LinkCardRefresh } from "./LinkCardRefresh";
|
|
33
35
|
import { linkHost } from "../../scripts/link-meta-core.mjs";
|
|
34
36
|
import { cardDate } from "../../ui/garden/card-refresh.mjs";
|
|
@@ -46,10 +48,12 @@ const internalRoute = internalLinkResolver({
|
|
|
46
48
|
...columnRedirects({ columns: [...new Set(columns.map((entry) => entry.column).filter(Boolean))].map((slug) => ({ slug })) }),
|
|
47
49
|
],
|
|
48
50
|
routes: [
|
|
49
|
-
"/", "/blog", "/columns", "/thoughts", "/tags", "/about",
|
|
51
|
+
"/", "/blog", "/columns", "/thoughts", "/tags", "/about", "/garden", "/garden/en",
|
|
50
52
|
...posts.map(postHref),
|
|
51
53
|
...columns.map(columnHref),
|
|
52
54
|
...thoughts.map((thought) => `/thoughts/${thought.slug}`),
|
|
55
|
+
...gardenDocs.map(gardenDocHref),
|
|
56
|
+
...gardenDocsEn.map((doc) => siteHref(`/garden/en/${doc.path}`)),
|
|
53
57
|
],
|
|
54
58
|
basePath: siteLinks?.basePath ?? "",
|
|
55
59
|
// Same shape as every other address in the site (the slash follows trailingSlash).
|
|
@@ -207,11 +211,12 @@ const headingText = (children: ReactNode) => {
|
|
|
207
211
|
return parts.join("");
|
|
208
212
|
};
|
|
209
213
|
|
|
210
|
-
export function MarkdownArticle({ content }: { content: string }) {
|
|
214
|
+
export function MarkdownArticle({ content, numbering, lang }: { content: string; numbering?: boolean; lang?: UiLang }) {
|
|
215
|
+
const t = ui(lang);
|
|
211
216
|
// Optional layer 2: a Worker that refreshes card text. Without it, cards stay as built.
|
|
212
217
|
const cardWorker = content.includes("{card}") ? siteConfig.linkPreview?.worker : undefined;
|
|
213
218
|
const numbers = { h2: 0, h3: 0 };
|
|
214
|
-
const
|
|
219
|
+
const autoNumber = autoNumbersHeadings(content, numbering);
|
|
215
220
|
const headingBase = articleHeadingBase(content);
|
|
216
221
|
const section = (depth: 2 | 3, children: ReactNode) => {
|
|
217
222
|
const raw = headingText(children).trim();
|
|
@@ -222,7 +227,7 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
222
227
|
const prefix = raw.slice(0, raw.length - split.text.length);
|
|
223
228
|
return { no: split.no, children: stripPrefix(children, prefix) };
|
|
224
229
|
}
|
|
225
|
-
if (
|
|
230
|
+
if (!autoNumber) return { no: "", children };
|
|
226
231
|
if (depth === 2) { numbers.h2 += 1; numbers.h3 = 0; return { no: String(numbers.h2), children }; }
|
|
227
232
|
numbers.h3 += 1;
|
|
228
233
|
return { no: numbers.h2 ? `${numbers.h2}.${numbers.h3}` : String(numbers.h3), children };
|
|
@@ -247,7 +252,7 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
247
252
|
"garden-block": (props: { dataPayload?: string; "data-payload"?: string }) => {
|
|
248
253
|
const payload = props.dataPayload ?? props["data-payload"];
|
|
249
254
|
const block = readGardenPayload(payload);
|
|
250
|
-
if (block?.name === "file" && !block.error) return <GardenFile props={(block.props ?? {}) as Record<string, unknown>} />;
|
|
255
|
+
if (block?.name === "file" && !block.error) return <GardenFile props={(block.props ?? {}) as Record<string, unknown>} lang={lang} />;
|
|
251
256
|
return <GardenBlock dataPayload={payload} />;
|
|
252
257
|
},
|
|
253
258
|
"garden-codediff": (props: {
|
|
@@ -265,6 +270,7 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
265
270
|
),
|
|
266
271
|
};
|
|
267
272
|
return (
|
|
273
|
+
<UiLangProvider lang={lang}>
|
|
268
274
|
<div className="markdown-body prose">
|
|
269
275
|
<ReactMarkdown
|
|
270
276
|
remarkPlugins={[...markdownPlugins, [remarkLinkCard, { sameSite }], [remarkA2, { imageSize: (src: string) => lookupImageSize(imageSizes, src) }]]}
|
|
@@ -319,7 +325,7 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
319
325
|
return <MarkdownImage {...props} {...dims} style={{ ...props.style, ...dims.style }} />;
|
|
320
326
|
},
|
|
321
327
|
table: ({ children, ...props }) => (
|
|
322
|
-
<div className="table-wrap" role="region" aria-label=
|
|
328
|
+
<div className="table-wrap" role="region" aria-label={t.tableScroll} tabIndex={0}>
|
|
323
329
|
<table {...props}>{children}</table>
|
|
324
330
|
</div>
|
|
325
331
|
),
|
|
@@ -355,5 +361,6 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
355
361
|
</ReactMarkdown>
|
|
356
362
|
{cardWorker ? <LinkCardRefresh endpoint={cardWorker} /> : null}
|
|
357
363
|
</div>
|
|
364
|
+
</UiLangProvider>
|
|
358
365
|
);
|
|
359
366
|
}
|
|
@@ -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
|
|
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={
|
|
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 { 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
|
-
|
|
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 (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 />
|
|
24
|
+
<SiteFooter docs={pathname?.startsWith("/garden") ?? false} lang={lang} />
|
|
16
25
|
</>
|
|
17
26
|
);
|
|
18
27
|
}
|
|
@@ -1,13 +1,22 @@
|
|
|
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. */
|
|
6
|
+
export function SiteFooter({ docs = false, lang }: { docs?: boolean; lang?: UiLang }) {
|
|
7
|
+
const t = ui(lang);
|
|
5
8
|
return (
|
|
6
9
|
<footer className="sh-footer">
|
|
7
10
|
<div className="sh-foot">
|
|
8
11
|
<p>
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
{docs ? (
|
|
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>
|
|
14
|
+
) : (
|
|
15
|
+
<>
|
|
16
|
+
© {new Date().getFullYear()} {siteConfig.author.name} · {siteConfig.footer}
|
|
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>
|
|
18
|
+
</>
|
|
19
|
+
)}
|
|
11
20
|
</p>
|
|
12
21
|
<div className="sh-foot-links">
|
|
13
22
|
<a href={githubUrl} rel="me">
|