hd-garden 0.6.1 → 0.7.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/app/admin/preview/[...path]/page.tsx +1 -1
- package/app/blog/[...path]/page.tsx +2 -2
- package/app/columns/[column]/[...path]/page.tsx +2 -2
- package/app/components/ArticleMeasure.tsx +39 -4
- package/app/components/GardenDocsNav.tsx +184 -0
- package/app/components/MarkdownArticle.tsx +7 -6
- package/app/components/SiteChrome.tsx +1 -1
- package/app/components/SiteFooter.tsx +10 -3
- package/app/garden/[...slug]/page.tsx +148 -0
- package/app/garden/page.tsx +61 -128
- package/app/garden-modules.d.ts +15 -0
- package/app/globals.css +10 -10
- package/app/lib/content.ts +16 -10
- package/app/lib/garden-docs-meta.ts +83 -0
- package/app/lib/heading-number.ts +13 -0
- package/app/styles/a2.css +531 -0
- package/docs/handbook/assets/reading-layout.svg +26 -0
- package/docs/handbook/components/card.md +44 -0
- package/docs/handbook/components/chart.md +52 -0
- package/docs/handbook/components/compare.md +40 -0
- package/docs/handbook/components/figure.md +70 -0
- package/docs/handbook/components/file/checklist.md +17 -0
- package/docs/handbook/components/file.md +32 -0
- package/docs/handbook/components/html.md +67 -0
- package/docs/handbook/components/interactive.md +65 -0
- package/docs/handbook/components/stat.md +55 -0
- package/docs/handbook/components/timeline.md +50 -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/index.md +92 -0
- package/docs/handbook/start.md +92 -0
- package/docs/handbook/workflow.md +81 -0
- package/docs/handbook/writing.md +148 -0
- package/package.json +2 -1
- package/scripts/build-content.mjs +124 -15
- package/scripts/handbook-allowlist.mjs +68 -0
- package/scripts/handbook-facts.mjs +1104 -0
- package/site.config.yaml +2 -2
- package/templates/garden-blocks.md +1 -1
- package/templates/site/package.json +1 -3
- package/ui/garden/catalog.mjs +1 -1
- package/ui/preview/dist/garden-ui.esm.js +1 -1
- package/ui/preview/dist/garden-ui.iife.js +1 -1
|
@@ -88,7 +88,7 @@ export default async function DraftPreviewPage({ params }: { params: Promise<{ p
|
|
|
88
88
|
<h1 className="ui-admin-preview-title">{typeof data.title === "string" ? data.title : post}</h1>
|
|
89
89
|
{typeof data.description === "string" ? <p className="ui-studio-meta">{data.description}</p> : null}
|
|
90
90
|
<article className="article-body">
|
|
91
|
-
<MarkdownArticle content={proxyAssets(content, post)} />
|
|
91
|
+
<MarkdownArticle content={proxyAssets(content, post)} numbering={data.numbering === false ? false : undefined} />
|
|
92
92
|
</article>
|
|
93
93
|
</section>
|
|
94
94
|
</AdminUiShell>
|
|
@@ -77,7 +77,7 @@ export default async function PostPage({
|
|
|
77
77
|
if (isStaticPlaceholder(path)) return null;
|
|
78
78
|
notFound();
|
|
79
79
|
}
|
|
80
|
-
const headings = extractHeadings(post.content);
|
|
80
|
+
const headings = extractHeadings(post.content, { numbering: post.numbering });
|
|
81
81
|
const baseUrl = await getSiteUrl();
|
|
82
82
|
const pathname = postHref(post);
|
|
83
83
|
const canonicalUrl = absoluteUrl(pathname, baseUrl);
|
|
@@ -125,7 +125,7 @@ export default async function PostPage({
|
|
|
125
125
|
<ArticleMeasure />
|
|
126
126
|
<div className="e-content">
|
|
127
127
|
{post.description ? <p className="lead quote-plain p-summary">{post.description}</p> : null}
|
|
128
|
-
<MarkdownArticle content={post.content} />
|
|
128
|
+
<MarkdownArticle content={post.content} numbering={post.numbering} />
|
|
129
129
|
</div>
|
|
130
130
|
<ArticleReferences references={post.references} />
|
|
131
131
|
<Webmentions
|
|
@@ -89,7 +89,7 @@ export default async function ColumnEntryPage({
|
|
|
89
89
|
const listed = [...series].sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
|
|
90
90
|
const currentIndex = series.findIndex((item) => item.path === entry.path);
|
|
91
91
|
const next = series[currentIndex + 1];
|
|
92
|
-
const headings = extractHeadings(entry.content);
|
|
92
|
+
const headings = extractHeadings(entry.content, { numbering: entry.numbering });
|
|
93
93
|
const baseUrl = await getSiteUrl();
|
|
94
94
|
const pathname = columnHref(entry);
|
|
95
95
|
const canonicalUrl = absoluteUrl(pathname, baseUrl);
|
|
@@ -147,7 +147,7 @@ export default async function ColumnEntryPage({
|
|
|
147
147
|
<ArticleMeasure />
|
|
148
148
|
<div className="e-content">
|
|
149
149
|
{entry.description ? <p className="lead p-summary">{entry.description}</p> : null}
|
|
150
|
-
<MarkdownArticle content={entry.content} />
|
|
150
|
+
<MarkdownArticle content={entry.content} numbering={entry.numbering} />
|
|
151
151
|
</div>
|
|
152
152
|
<ArticleReferences references={entry.references} />
|
|
153
153
|
{next && (
|
|
@@ -45,7 +45,14 @@ export function ArticleMeasure() {
|
|
|
45
45
|
return width;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
const docPage = article.closest<HTMLElement>(".garden-doc-page");
|
|
49
|
+
|
|
48
50
|
const placeToc = () => {
|
|
51
|
+
if (docPage) {
|
|
52
|
+
root.removeAttribute("data-toc-yield");
|
|
53
|
+
root.removeAttribute("data-toc-hide");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
49
56
|
if (!toc) {
|
|
50
57
|
root.removeAttribute("data-toc-yield");
|
|
51
58
|
return;
|
|
@@ -74,10 +81,38 @@ export function ArticleMeasure() {
|
|
|
74
81
|
}
|
|
75
82
|
};
|
|
76
83
|
|
|
84
|
+
const clampMargin = (margin: { left: number; right: number }, maxRoom: number | null) => {
|
|
85
|
+
if (maxRoom === null) return margin;
|
|
86
|
+
const side = Math.max(0, Math.min(margin.left, maxRoom));
|
|
87
|
+
return { left: side, right: side };
|
|
88
|
+
};
|
|
89
|
+
|
|
77
90
|
const measure = (refresh: boolean) => {
|
|
78
91
|
// Measure the text column, not the whole article grid (which includes the TOC rail).
|
|
79
92
|
const articleBox = (article.querySelector(".markdown-body") ?? article).getBoundingClientRect();
|
|
80
93
|
const viewport = root.clientWidth;
|
|
94
|
+
let docMaxRoom: number | null = null;
|
|
95
|
+
if (docPage) {
|
|
96
|
+
const sidebar = docPage.querySelector<HTMLElement>(".gdoc-sidebar");
|
|
97
|
+
let leftLimit = 24;
|
|
98
|
+
if (sidebar && getComputedStyle(sidebar).position === "sticky") {
|
|
99
|
+
const sbBox = sidebar.getBoundingClientRect();
|
|
100
|
+
if (sbBox.width > 0 && sbBox.right <= articleBox.left + 1) {
|
|
101
|
+
leftLimit = sbBox.right + 16;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let rightLimit = viewport - 24;
|
|
105
|
+
if (toc && getComputedStyle(toc).display !== "none") {
|
|
106
|
+
const tocBox = toc.getBoundingClientRect();
|
|
107
|
+
if (tocBox.width > 0 && tocBox.left >= articleBox.right - 1) {
|
|
108
|
+
rightLimit = tocBox.left - translatedX(toc) - 16;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
docMaxRoom = Math.max(
|
|
112
|
+
0,
|
|
113
|
+
Math.floor(Math.min(articleBox.left - leftLimit, rightLimit - articleBox.right)),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
81
116
|
const widen = (element: HTMLElement, margin: { left: number; right: number }) => {
|
|
82
117
|
if (margin.left <= 0) return;
|
|
83
118
|
element.style.setProperty("--wl", `${margin.left}px`);
|
|
@@ -89,13 +124,13 @@ export function ArticleMeasure() {
|
|
|
89
124
|
const width = tableWidth(wrap, refresh);
|
|
90
125
|
resetWide(wrap);
|
|
91
126
|
if (!width) return;
|
|
92
|
-
widen(wrap, tableBreakout({
|
|
127
|
+
widen(wrap, clampMargin(tableBreakout({
|
|
93
128
|
viewport,
|
|
94
129
|
articleLeft: articleBox.left,
|
|
95
130
|
articleRight: articleBox.right,
|
|
96
131
|
tableClient: wrap.clientWidth,
|
|
97
132
|
tableScroll: width,
|
|
98
|
-
}));
|
|
133
|
+
}), docMaxRoom));
|
|
99
134
|
});
|
|
100
135
|
// Images: as wide as they display (2x screenshots at half their pixels), up to 1040px in total.
|
|
101
136
|
article.querySelectorAll<HTMLElement>(".fig").forEach((figure) => {
|
|
@@ -115,7 +150,7 @@ export function ArticleMeasure() {
|
|
|
115
150
|
if (!pixels) return;
|
|
116
151
|
// width="" written by the build is the pixel size, not an author's choice.
|
|
117
152
|
const attrWidth = known ? Number(image.dataset.aw) || 0 : Number(image.getAttribute("width")) || 0;
|
|
118
|
-
const breakout = figureBreakout({
|
|
153
|
+
const breakout = clampMargin(figureBreakout({
|
|
119
154
|
viewport,
|
|
120
155
|
articleLeft: articleBox.left,
|
|
121
156
|
articleRight: articleBox.right,
|
|
@@ -123,7 +158,7 @@ export function ArticleMeasure() {
|
|
|
123
158
|
imageClient: image.clientWidth,
|
|
124
159
|
naturalWidth: pixels,
|
|
125
160
|
attrWidth,
|
|
126
|
-
});
|
|
161
|
+
}), docMaxRoom);
|
|
127
162
|
widen(figure, breakout);
|
|
128
163
|
// Only a widened image, or one with a width attribute, gets an inline width.
|
|
129
164
|
const width = figureImageWidth({ naturalWidth: pixels, attrWidth, widened: breakout.left > 0 });
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
NAV_SHORT_TITLE,
|
|
10
|
+
type GardenDocNavItem,
|
|
11
|
+
} from "../lib/garden-docs-meta";
|
|
12
|
+
import { siteHref } from "../lib/site-href";
|
|
13
|
+
|
|
14
|
+
export function GardenDocsNav({
|
|
15
|
+
docs,
|
|
16
|
+
currentPath = "",
|
|
17
|
+
}: {
|
|
18
|
+
docs: GardenDocNavItem[];
|
|
19
|
+
currentPath?: string;
|
|
20
|
+
}) {
|
|
21
|
+
const groups = groupHandbookDocs(docs);
|
|
22
|
+
const currentDoc = docs.find((doc) => doc.path === currentPath);
|
|
23
|
+
const currentSection = groups.find((group) =>
|
|
24
|
+
group.items.some((item) => item.path === currentPath),
|
|
25
|
+
);
|
|
26
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
27
|
+
const drawerId = useId();
|
|
28
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
29
|
+
const closeRef = useRef<HTMLButtonElement>(null);
|
|
30
|
+
const restoreFocusRef = useRef(false);
|
|
31
|
+
|
|
32
|
+
const closeMobile = useCallback((restoreFocus = false) => {
|
|
33
|
+
restoreFocusRef.current = restoreFocus;
|
|
34
|
+
setMobileOpen(false);
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (mobileOpen || !restoreFocusRef.current) return;
|
|
39
|
+
restoreFocusRef.current = false;
|
|
40
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
41
|
+
}, [mobileOpen]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!mobileOpen) return;
|
|
45
|
+
const query = window.matchMedia("(max-width: 1080px)");
|
|
46
|
+
if (!query.matches) return;
|
|
47
|
+
const previousOverflow = document.body.style.overflow;
|
|
48
|
+
const focusFrame = window.requestAnimationFrame(() => {
|
|
49
|
+
closeRef.current?.focus({ preventScroll: true });
|
|
50
|
+
});
|
|
51
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
52
|
+
if (event.key === "Escape") closeMobile(true);
|
|
53
|
+
};
|
|
54
|
+
const onChange = () => {
|
|
55
|
+
if (!query.matches) setMobileOpen(false);
|
|
56
|
+
};
|
|
57
|
+
document.body.style.overflow = "hidden";
|
|
58
|
+
window.addEventListener("keydown", onKeyDown);
|
|
59
|
+
query.addEventListener("change", onChange);
|
|
60
|
+
return () => {
|
|
61
|
+
window.cancelAnimationFrame(focusFrame);
|
|
62
|
+
document.body.style.overflow = previousOverflow;
|
|
63
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
64
|
+
query.removeEventListener("change", onChange);
|
|
65
|
+
};
|
|
66
|
+
}, [closeMobile, mobileOpen]);
|
|
67
|
+
|
|
68
|
+
const isOverview = currentPath === "";
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<div className="gdoc-mobile-bar" aria-label="说明书目录栏">
|
|
73
|
+
<button
|
|
74
|
+
ref={triggerRef}
|
|
75
|
+
type="button"
|
|
76
|
+
className="gdoc-mobile-trigger"
|
|
77
|
+
aria-controls={drawerId}
|
|
78
|
+
aria-expanded={mobileOpen}
|
|
79
|
+
onClick={() => setMobileOpen(true)}
|
|
80
|
+
>
|
|
81
|
+
<List size={16} weight="bold" aria-hidden="true" />
|
|
82
|
+
<span>说明目录</span>
|
|
83
|
+
</button>
|
|
84
|
+
<a
|
|
85
|
+
className="gdoc-nojs-trigger"
|
|
86
|
+
id={`${drawerId}-open`}
|
|
87
|
+
href={`#${drawerId}`}
|
|
88
|
+
aria-label="打开说明目录"
|
|
89
|
+
>
|
|
90
|
+
<List size={16} weight="bold" aria-hidden="true" />
|
|
91
|
+
<span>说明目录</span>
|
|
92
|
+
</a>
|
|
93
|
+
<div className="gdoc-mobile-crumb" aria-hidden="true">
|
|
94
|
+
<span>{currentSection?.title ?? "Garden 使用说明"}</span>
|
|
95
|
+
<CaretRight size={12} />
|
|
96
|
+
<strong>{currentDoc ? (NAV_SHORT_TITLE[currentDoc.path] ?? currentDoc.title) : "总览与快速入口"}</strong>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className="gdoc-backdrop"
|
|
103
|
+
aria-label="关闭说明目录"
|
|
104
|
+
tabIndex={mobileOpen ? 0 : -1}
|
|
105
|
+
data-open={mobileOpen}
|
|
106
|
+
onClick={() => closeMobile(true)}
|
|
107
|
+
/>
|
|
108
|
+
|
|
109
|
+
<aside
|
|
110
|
+
id={drawerId}
|
|
111
|
+
className="gdoc-sidebar"
|
|
112
|
+
aria-label="Garden 使用说明目录"
|
|
113
|
+
data-open={mobileOpen}
|
|
114
|
+
>
|
|
115
|
+
<div className="gdoc-sidebar-head">
|
|
116
|
+
<Link
|
|
117
|
+
href={siteHref("/garden")}
|
|
118
|
+
className="gdoc-overview-link"
|
|
119
|
+
aria-current={isOverview ? "page" : undefined}
|
|
120
|
+
onClick={() => closeMobile(false)}
|
|
121
|
+
>
|
|
122
|
+
<span className="gdoc-overview-mark">
|
|
123
|
+
<BookOpenText size={16} weight="duotone" aria-hidden="true" />
|
|
124
|
+
<span>Garden 使用说明</span>
|
|
125
|
+
</span>
|
|
126
|
+
<small>总览与快速入口</small>
|
|
127
|
+
</Link>
|
|
128
|
+
<button
|
|
129
|
+
ref={closeRef}
|
|
130
|
+
type="button"
|
|
131
|
+
className="gdoc-mobile-close"
|
|
132
|
+
aria-label="关闭说明目录"
|
|
133
|
+
onClick={() => closeMobile(true)}
|
|
134
|
+
>
|
|
135
|
+
<X size={17} aria-hidden="true" />
|
|
136
|
+
</button>
|
|
137
|
+
<a
|
|
138
|
+
className="gdoc-nojs-close"
|
|
139
|
+
href={`#${drawerId}-open`}
|
|
140
|
+
aria-label="关闭说明目录"
|
|
141
|
+
>
|
|
142
|
+
<X size={17} aria-hidden="true" />
|
|
143
|
+
</a>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<nav className="gdoc-nav" aria-label="章节导航">
|
|
147
|
+
{groups.map((group) => (
|
|
148
|
+
<div key={group.id} className="gdoc-nav-group">
|
|
149
|
+
<div className="gdoc-nav-group-head">
|
|
150
|
+
<h2>{group.title}</h2>
|
|
151
|
+
{group.note ? <span>{group.note}</span> : null}
|
|
152
|
+
</div>
|
|
153
|
+
<ol className="gdoc-nav-list">
|
|
154
|
+
{group.items.map((item) => {
|
|
155
|
+
const active = item.path === currentPath;
|
|
156
|
+
const badge = COMPONENT_BADGE[item.path];
|
|
157
|
+
const label = NAV_SHORT_TITLE[item.path] ?? item.title;
|
|
158
|
+
return (
|
|
159
|
+
<li key={item.path} className={active ? "is-active" : undefined}>
|
|
160
|
+
<Link
|
|
161
|
+
href={siteHref(`/garden/${item.path}`)}
|
|
162
|
+
aria-current={active ? "page" : undefined}
|
|
163
|
+
onClick={() => closeMobile(false)}
|
|
164
|
+
>
|
|
165
|
+
{badge ? (
|
|
166
|
+
<>
|
|
167
|
+
<code className="gdoc-comp-pill">{badge}</code>
|
|
168
|
+
<span className="gdoc-comp-label">{label}</span>
|
|
169
|
+
</>
|
|
170
|
+
) : (
|
|
171
|
+
<span>{label}</span>
|
|
172
|
+
)}
|
|
173
|
+
</Link>
|
|
174
|
+
</li>
|
|
175
|
+
);
|
|
176
|
+
})}
|
|
177
|
+
</ol>
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</nav>
|
|
181
|
+
</aside>
|
|
182
|
+
</>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type HTMLAttributes,
|
|
9
9
|
type ReactNode,
|
|
10
10
|
} from "react";
|
|
11
|
-
import { articleHeadingBase,
|
|
11
|
+
import { articleHeadingBase, autoNumbersHeadings, displayHeadingLevel, splitHeadingNumber } from "../lib/content";
|
|
12
12
|
import Link from "next/link";
|
|
13
13
|
import ReactMarkdown from "react-markdown";
|
|
14
14
|
import rehypeRaw from "rehype-raw";
|
|
@@ -28,7 +28,7 @@ import { imageDimensions, lookupImageSize } from "../../ui/garden/image-dims.mjs
|
|
|
28
28
|
import { siteConfig, siteLinks } from "@garden/site-config";
|
|
29
29
|
import { articleLink, sameSiteMatcher } from "../../ui/garden/same-site.mjs";
|
|
30
30
|
import { columnRedirects, internalLinkResolver, readRedirects } from "../../ui/garden/internal-links.mjs";
|
|
31
|
-
import { columnHref, columns, postHref, posts, thoughts } from "../lib/content";
|
|
31
|
+
import { columnHref, columns, gardenDocHref, gardenDocs, postHref, posts, thoughts } from "../lib/content";
|
|
32
32
|
import { LinkCardRefresh } from "./LinkCardRefresh";
|
|
33
33
|
import { linkHost } from "../../scripts/link-meta-core.mjs";
|
|
34
34
|
import { cardDate } from "../../ui/garden/card-refresh.mjs";
|
|
@@ -46,10 +46,11 @@ const internalRoute = internalLinkResolver({
|
|
|
46
46
|
...columnRedirects({ columns: [...new Set(columns.map((entry) => entry.column).filter(Boolean))].map((slug) => ({ slug })) }),
|
|
47
47
|
],
|
|
48
48
|
routes: [
|
|
49
|
-
"/", "/blog", "/columns", "/thoughts", "/tags", "/about",
|
|
49
|
+
"/", "/blog", "/columns", "/thoughts", "/tags", "/about", "/garden",
|
|
50
50
|
...posts.map(postHref),
|
|
51
51
|
...columns.map(columnHref),
|
|
52
52
|
...thoughts.map((thought) => `/thoughts/${thought.slug}`),
|
|
53
|
+
...gardenDocs.map(gardenDocHref),
|
|
53
54
|
],
|
|
54
55
|
basePath: siteLinks?.basePath ?? "",
|
|
55
56
|
// Same shape as every other address in the site (the slash follows trailingSlash).
|
|
@@ -207,11 +208,11 @@ const headingText = (children: ReactNode) => {
|
|
|
207
208
|
return parts.join("");
|
|
208
209
|
};
|
|
209
210
|
|
|
210
|
-
export function MarkdownArticle({ content }: { content: string }) {
|
|
211
|
+
export function MarkdownArticle({ content, numbering }: { content: string; numbering?: boolean }) {
|
|
211
212
|
// Optional layer 2: a Worker that refreshes card text. Without it, cards stay as built.
|
|
212
213
|
const cardWorker = content.includes("{card}") ? siteConfig.linkPreview?.worker : undefined;
|
|
213
214
|
const numbers = { h2: 0, h3: 0 };
|
|
214
|
-
const
|
|
215
|
+
const autoNumber = autoNumbersHeadings(content, numbering);
|
|
215
216
|
const headingBase = articleHeadingBase(content);
|
|
216
217
|
const section = (depth: 2 | 3, children: ReactNode) => {
|
|
217
218
|
const raw = headingText(children).trim();
|
|
@@ -222,7 +223,7 @@ export function MarkdownArticle({ content }: { content: string }) {
|
|
|
222
223
|
const prefix = raw.slice(0, raw.length - split.text.length);
|
|
223
224
|
return { no: split.no, children: stripPrefix(children, prefix) };
|
|
224
225
|
}
|
|
225
|
-
if (
|
|
226
|
+
if (!autoNumber) return { no: "", children };
|
|
226
227
|
if (depth === 2) { numbers.h2 += 1; numbers.h3 = 0; return { no: String(numbers.h2), children }; }
|
|
227
228
|
numbers.h3 += 1;
|
|
228
229
|
return { no: numbers.h2 ? `${numbers.h2}.${numbers.h3}` : String(numbers.h3), children };
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { GithubLogo, Rss } from "@phosphor-icons/react/ssr";
|
|
2
2
|
import { githubUrl, siteConfig } from "../site.config";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** On the Garden docs pages the footer carries no site-owner line. */
|
|
5
|
+
export function SiteFooter({ docs = false }: { docs?: boolean }) {
|
|
5
6
|
return (
|
|
6
7
|
<footer className="sh-footer">
|
|
7
8
|
<div className="sh-foot">
|
|
8
9
|
<p>
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
{docs ? (
|
|
11
|
+
<span className="footer-framework">基于 <a href="https://github.com/hedon954/garden" target="_blank" rel="noreferrer">Garden</a> 构建</span>
|
|
12
|
+
) : (
|
|
13
|
+
<>
|
|
14
|
+
© {new Date().getFullYear()} {siteConfig.author.name} · {siteConfig.footer}
|
|
15
|
+
<span className="footer-framework"> · 基于 <a href="https://github.com/hedon954/garden" target="_blank" rel="noreferrer">Garden</a> 构建</span>
|
|
16
|
+
</>
|
|
17
|
+
)}
|
|
11
18
|
</p>
|
|
12
19
|
<div className="sh-foot-links">
|
|
13
20
|
<a href={githubUrl} rel="me">
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import type { Metadata } from "next";
|
|
3
|
+
import { notFound } from "next/navigation";
|
|
4
|
+
import { ArrowLeft, ArrowRight } from "@phosphor-icons/react/ssr";
|
|
5
|
+
import {
|
|
6
|
+
extractHeadings,
|
|
7
|
+
findGardenDoc,
|
|
8
|
+
gardenDocHref,
|
|
9
|
+
gardenDocs,
|
|
10
|
+
} from "../../lib/content";
|
|
11
|
+
import { FEED_ALTERNATES } from "../../lib/page-alternates";
|
|
12
|
+
import { absoluteUrl, getSiteUrl } from "../../lib/site";
|
|
13
|
+
import { siteHref } from "../../lib/site-href";
|
|
14
|
+
import { ArticleMeasure } from "../../components/ArticleMeasure";
|
|
15
|
+
import { ArticleReferences } from "../../components/ArticleReferences";
|
|
16
|
+
import { ArticleTitle } from "../../components/ArticleTitle";
|
|
17
|
+
import { MarkdownArticle } from "../../components/MarkdownArticle";
|
|
18
|
+
import { TableOfContents } from "../../components/TableOfContents";
|
|
19
|
+
import { GardenDocsNav } from "../../components/GardenDocsNav";
|
|
20
|
+
import {
|
|
21
|
+
COMPONENT_BADGE,
|
|
22
|
+
HANDBOOK_SECTIONS,
|
|
23
|
+
orderedHandbookDocs,
|
|
24
|
+
toHandbookNavItems,
|
|
25
|
+
} from "../../lib/garden-docs-meta";
|
|
26
|
+
import {
|
|
27
|
+
isStaticPlaceholder,
|
|
28
|
+
STATIC_PLACEHOLDER,
|
|
29
|
+
staticParams,
|
|
30
|
+
} from "../../../ui/garden/static-params.mjs";
|
|
31
|
+
|
|
32
|
+
export function generateStaticParams() {
|
|
33
|
+
return staticParams(
|
|
34
|
+
gardenDocs.map((doc) => ({ slug: doc.path.split("/") })),
|
|
35
|
+
{ slug: [STATIC_PLACEHOLDER] },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function generateMetadata({
|
|
40
|
+
params,
|
|
41
|
+
}: {
|
|
42
|
+
params: Promise<{ slug: string[] }>;
|
|
43
|
+
}) {
|
|
44
|
+
const { slug } = await params;
|
|
45
|
+
const doc = findGardenDoc(slug.join("/"));
|
|
46
|
+
if (!doc) return {};
|
|
47
|
+
const baseUrl = await getSiteUrl();
|
|
48
|
+
const pathname = gardenDocHref(doc);
|
|
49
|
+
return {
|
|
50
|
+
title: `${doc.title} · Garden 使用说明`,
|
|
51
|
+
description: doc.description,
|
|
52
|
+
alternates: { canonical: pathname, types: FEED_ALTERNATES },
|
|
53
|
+
openGraph: {
|
|
54
|
+
type: "article",
|
|
55
|
+
url: absoluteUrl(pathname, baseUrl),
|
|
56
|
+
title: `${doc.title} · Garden 使用说明`,
|
|
57
|
+
description: doc.description,
|
|
58
|
+
},
|
|
59
|
+
} satisfies Metadata;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default async function GardenDocPage({
|
|
63
|
+
params,
|
|
64
|
+
}: {
|
|
65
|
+
params: Promise<{ slug: string[] }>;
|
|
66
|
+
}) {
|
|
67
|
+
const { slug } = await params;
|
|
68
|
+
const doc = findGardenDoc(slug.join("/"));
|
|
69
|
+
if (!doc) {
|
|
70
|
+
if (isStaticPlaceholder(slug)) return null;
|
|
71
|
+
notFound();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const ordered = orderedHandbookDocs(gardenDocs);
|
|
75
|
+
const navItems = toHandbookNavItems(gardenDocs);
|
|
76
|
+
const index = ordered.findIndex((item) => item.path === doc.path);
|
|
77
|
+
const prev = index > 0 ? ordered[index - 1] : null;
|
|
78
|
+
const next = index >= 0 && index < ordered.length - 1 ? ordered[index + 1] : null;
|
|
79
|
+
const headings = extractHeadings(doc.content);
|
|
80
|
+
const sectionMeta = HANDBOOK_SECTIONS.find((sec) => sec.id === doc.section);
|
|
81
|
+
const componentBadge = COMPONENT_BADGE[doc.path];
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<main className="column-reading-page garden-doc-page">
|
|
85
|
+
<GardenDocsNav docs={navItems} currentPath={doc.path} />
|
|
86
|
+
|
|
87
|
+
<article className="article-column garden-doc-article h-entry">
|
|
88
|
+
<header className="gdoc-article-head">
|
|
89
|
+
<div className="gdoc-eyebrow">
|
|
90
|
+
<Link href={siteHref("/garden")} className="gdoc-eyebrow-root">
|
|
91
|
+
Garden 使用说明
|
|
92
|
+
</Link>
|
|
93
|
+
<span aria-hidden="true">/</span>
|
|
94
|
+
<span>{sectionMeta?.title ?? doc.sectionTitle ?? "指南"}</span>
|
|
95
|
+
{componentBadge ? (
|
|
96
|
+
<>
|
|
97
|
+
<code className="gdoc-eyebrow-fence">{`garden ${componentBadge}`}</code>
|
|
98
|
+
</>
|
|
99
|
+
) : null}
|
|
100
|
+
</div>
|
|
101
|
+
<ArticleTitle title={doc.title} />
|
|
102
|
+
{doc.description ? (
|
|
103
|
+
<p className="gdoc-article-deck p-summary">{doc.description}</p>
|
|
104
|
+
) : null}
|
|
105
|
+
</header>
|
|
106
|
+
|
|
107
|
+
<ArticleMeasure />
|
|
108
|
+
|
|
109
|
+
<div className="e-content">
|
|
110
|
+
<MarkdownArticle content={doc.content} />
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<ArticleReferences references={doc.references} />
|
|
114
|
+
|
|
115
|
+
<nav className="gdoc-pager" aria-label="说明书上下篇导航">
|
|
116
|
+
{prev ? (
|
|
117
|
+
<Link href={gardenDocHref(prev)} className="gdoc-pager-card is-prev">
|
|
118
|
+
<span className="gdoc-pager-dir">
|
|
119
|
+
<ArrowLeft size={14} aria-hidden="true" />
|
|
120
|
+
上一篇
|
|
121
|
+
</span>
|
|
122
|
+
<strong>{prev.title}</strong>
|
|
123
|
+
</Link>
|
|
124
|
+
) : (
|
|
125
|
+
<Link href={siteHref("/garden")} className="gdoc-pager-card is-prev">
|
|
126
|
+
<span className="gdoc-pager-dir">
|
|
127
|
+
<ArrowLeft size={14} aria-hidden="true" />
|
|
128
|
+
返回总览
|
|
129
|
+
</span>
|
|
130
|
+
<strong>Garden 使用说明首页</strong>
|
|
131
|
+
</Link>
|
|
132
|
+
)}
|
|
133
|
+
{next ? (
|
|
134
|
+
<Link href={gardenDocHref(next)} className="gdoc-pager-card is-next">
|
|
135
|
+
<span className="gdoc-pager-dir">
|
|
136
|
+
下一篇
|
|
137
|
+
<ArrowRight size={14} aria-hidden="true" />
|
|
138
|
+
</span>
|
|
139
|
+
<strong>{next.title}</strong>
|
|
140
|
+
</Link>
|
|
141
|
+
) : null}
|
|
142
|
+
</nav>
|
|
143
|
+
</article>
|
|
144
|
+
|
|
145
|
+
<TableOfContents headings={headings} label="本页目录" />
|
|
146
|
+
</main>
|
|
147
|
+
);
|
|
148
|
+
}
|