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.
Files changed (45) hide show
  1. package/app/admin/preview/[...path]/page.tsx +1 -1
  2. package/app/blog/[...path]/page.tsx +2 -2
  3. package/app/columns/[column]/[...path]/page.tsx +2 -2
  4. package/app/components/ArticleMeasure.tsx +39 -4
  5. package/app/components/GardenDocsNav.tsx +184 -0
  6. package/app/components/MarkdownArticle.tsx +7 -6
  7. package/app/components/SiteChrome.tsx +1 -1
  8. package/app/components/SiteFooter.tsx +10 -3
  9. package/app/garden/[...slug]/page.tsx +148 -0
  10. package/app/garden/page.tsx +61 -128
  11. package/app/garden-modules.d.ts +15 -0
  12. package/app/globals.css +10 -10
  13. package/app/lib/content.ts +16 -10
  14. package/app/lib/garden-docs-meta.ts +83 -0
  15. package/app/lib/heading-number.ts +13 -0
  16. package/app/styles/a2.css +531 -0
  17. package/docs/handbook/assets/reading-layout.svg +26 -0
  18. package/docs/handbook/components/card.md +44 -0
  19. package/docs/handbook/components/chart.md +52 -0
  20. package/docs/handbook/components/compare.md +40 -0
  21. package/docs/handbook/components/figure.md +70 -0
  22. package/docs/handbook/components/file/checklist.md +17 -0
  23. package/docs/handbook/components/file.md +32 -0
  24. package/docs/handbook/components/html.md +67 -0
  25. package/docs/handbook/components/interactive.md +65 -0
  26. package/docs/handbook/components/stat.md +55 -0
  27. package/docs/handbook/components/timeline.md +50 -0
  28. package/docs/handbook/components.md +30 -0
  29. package/docs/handbook/config.md +125 -0
  30. package/docs/handbook/deploy.md +273 -0
  31. package/docs/handbook/design.md +81 -0
  32. package/docs/handbook/index.md +92 -0
  33. package/docs/handbook/start.md +92 -0
  34. package/docs/handbook/workflow.md +81 -0
  35. package/docs/handbook/writing.md +148 -0
  36. package/package.json +2 -1
  37. package/scripts/build-content.mjs +124 -15
  38. package/scripts/handbook-allowlist.mjs +68 -0
  39. package/scripts/handbook-facts.mjs +1104 -0
  40. package/site.config.yaml +2 -2
  41. package/templates/garden-blocks.md +1 -1
  42. package/templates/site/package.json +1 -3
  43. package/ui/garden/catalog.mjs +1 -1
  44. package/ui/preview/dist/garden-ui.esm.js +1 -1
  45. package/ui/preview/dist/garden-ui.iife.js +1 -1
@@ -1,148 +1,81 @@
1
- import { ArrowRight, ArrowUpRight, GithubLogo } from "@phosphor-icons/react/ssr";
2
- import { GardenAdminGuide } from "../components/GardenAdminGuide";
3
- import { GardenContentGuide } from "../components/GardenContentGuide";
1
+ import Link from "next/link";
2
+ import { ArrowRight } from "@phosphor-icons/react/ssr";
3
+ import { ArticleMeasure } from "../components/ArticleMeasure";
4
+ import { ArticleReferences } from "../components/ArticleReferences";
5
+ import { ArticleTitle } from "../components/ArticleTitle";
6
+ import { GardenDocsNav } from "../components/GardenDocsNav";
4
7
  import { GardenMediaGuide } from "../components/GardenMediaGuide";
5
- import { PageIntro } from "../components/PageIntro";
6
- import { siteConfig } from "../site.config";
8
+ import { MarkdownArticle } from "../components/MarkdownArticle";
9
+ import { TableOfContents } from "../components/TableOfContents";
10
+ import {
11
+ extractHeadings,
12
+ gardenDocHref,
13
+ gardenDocs,
14
+ gardenIndexDoc,
15
+ } from "../lib/content";
16
+ import {
17
+ orderedHandbookDocs,
18
+ toHandbookNavItems,
19
+ } from "../lib/garden-docs-meta";
7
20
  import { pageAlternates } from "../lib/page-alternates";
21
+ import { siteConfig } from "../site.config";
8
22
 
9
23
  export const metadata = {
10
- title: "Garden",
11
- description: "用 Markdown 写文章。推送之后,页面、搜索和 RSS 一起更新。",
24
+ title: "Garden 使用说明",
25
+ description:
26
+ "用 Markdown 写文章、随想与专栏。自动处理标题断行、宽图宽表展开、代码对比与 9 个内置组件。",
12
27
  alternates: pageAlternates("/garden"),
13
28
  };
14
29
 
15
- const jumps = [
16
- ["#write-post", "写文章"],
17
- ["#write-thought", "随想"],
18
- ["#make-column", "专栏"],
19
- ["#setup", "跑起来"],
20
- ["#media", "视频和链接"],
21
- ["#admin", "后台"],
22
- ];
23
-
24
- const setupSteps = [
25
- {
26
- title: "创建站点",
27
- body: (
28
- <>
29
- 运行 <code>npx hd-garden init my-blog</code>。目录里留下 <code>content/</code> 和 <code>site.config.yaml</code>。仓库名叫 <code>&lt;owner&gt;.github.io</code> 时,地址最直接。
30
- </>
31
- ),
32
- },
33
- {
34
- title: "在本地看",
35
- command: "make dev",
36
- body: "第一次会安装依赖。然后打开终端里的地址。",
37
- },
38
- {
39
- title: "换成你的内容",
40
- files: [
41
- ["site.config.yaml", "站名、作者、各页标题"],
42
- ["content/posts/", "长文和附件"],
43
- ["content/thoughts/", "随想"],
44
- ["content/columns.yaml", "专栏和阅读顺序"],
45
- ],
46
- },
47
- {
48
- title: "发布",
49
- command: "make update",
50
- body: "先检查,再提交并推送。main 更新后,网站、搜索和 RSS 会一起更新。",
51
- },
52
- ];
53
-
54
30
  export default function GardenPage() {
55
- return (
56
- <main className="page-shell garden-page">
57
- <PageIntro title={siteConfig.pages.garden.title} subtitle={siteConfig.pages.garden.subtitle}>
58
- <p className="garden-lede">
59
- 文章放在你自己的仓库里。你改文件,网站跟着更新。
60
- </p>
61
- <div className="garden-actions">
62
- <a
63
- className="garden-primary-link"
64
- href="https://github.com/hedon954/garden"
65
- target="_blank"
66
- rel="noreferrer"
67
- >
68
- <GithubLogo size={18} weight="fill" />
69
- 查看源码
70
- <ArrowUpRight size={16} />
71
- </a>
72
- <a className="inline-cta" href="#setup">
73
- 在本地跑起来
74
- <ArrowRight size={16} />
75
- </a>
76
- </div>
77
- </PageIntro>
78
-
79
- <nav className="garden-jumps" aria-label="本页">
80
- {jumps.map(([href, label]) => (
81
- <a key={href} href={href}>{label}</a>
82
- ))}
83
- </nav>
31
+ const navItems = toHandbookNavItems(gardenDocs);
32
+ const ordered = orderedHandbookDocs(gardenDocs);
33
+ const firstDoc = ordered[0] ?? null;
34
+ const content = gardenIndexDoc?.content ?? "";
35
+ const headings = extractHeadings(content);
36
+ const configuredTitle = siteConfig.pages.garden.title?.trim() || gardenIndexDoc?.title || "Garden 使用说明";
37
+ const configuredSubtitle = siteConfig.pages.garden.subtitle?.trim() || gardenIndexDoc?.description || "";
38
+ void GardenMediaGuide;
84
39
 
85
- <GardenContentGuide />
40
+ return (
41
+ <main className="column-reading-page garden-doc-page">
42
+ <GardenDocsNav docs={navItems} currentPath="" />
86
43
 
87
- <section className="garden-setup" id="setup" aria-labelledby="garden-setup-title">
88
- <header className="garden-section-heading">
89
- <h2 id="garden-setup-title">跑起来</h2>
90
- <p>不需要数据库。换上你的名字和第一篇文章,推送即可。</p>
44
+ <article className="article-column garden-doc-article h-entry">
45
+ <header className="gdoc-article-head">
46
+ <div className="gdoc-eyebrow">
47
+ <span>Garden 使用说明</span>
48
+ <span aria-hidden="true">/</span>
49
+ <span>总览</span>
50
+ </div>
51
+ <ArticleTitle title={configuredTitle} />
52
+ {configuredSubtitle ? (
53
+ <p className="gdoc-article-deck p-summary">{configuredSubtitle}</p>
54
+ ) : null}
91
55
  </header>
92
56
 
93
- <ol className="garden-steps">
94
- {setupSteps.map((step, index) => (
95
- <li key={step.title}>
96
- <span>{index + 1}</span>
97
- <div>
98
- <h3>{step.title}</h3>
99
- {"command" in step && step.command ? <pre><code>{step.command}</code></pre> : null}
100
- {"files" in step && step.files ? (
101
- <dl>
102
- {step.files.map(([name, note]) => (
103
- <div key={name}>
104
- <dt>{name}</dt>
105
- <dd>{note}</dd>
106
- </div>
107
- ))}
108
- </dl>
109
- ) : null}
110
- {"body" in step && step.body ? <p>{step.body}</p> : null}
111
- </div>
112
- </li>
113
- ))}
114
- </ol>
57
+ <ArticleMeasure />
115
58
 
116
- <div className="garden-setup-notes">
117
- <p>
118
- <strong>仓库可以是私有的。</strong>
119
- 公开仓库一种就够。想把草稿留在私有仓库时,用 <code>PAGES_REPOSITORY</code> 指向公开的页面仓库。两种都是 <code>make update</code>。
120
- </p>
121
- <p>
122
- <strong>先看草稿。</strong>
123
- <code>CONTENT_INCLUDE_DRAFTS=1 make dev</code> 会把草稿和还没到时间的文章也显示出来。
124
- </p>
125
- <p>
126
- <strong>密钥不进仓库。</strong>
127
- 评论、统计和后台的密钥放在部署平台。
128
- </p>
59
+ <div className="e-content">
60
+ <MarkdownArticle content={content} />
129
61
  </div>
130
- </section>
131
62
 
132
- <GardenMediaGuide />
63
+ <ArticleReferences references={gardenIndexDoc?.references} />
133
64
 
134
- <section className="garden-after" aria-labelledby="garden-after-title">
135
- <header className="garden-section-heading">
136
- <h2 id="garden-after-title">推送之后</h2>
137
- </header>
138
- <p className="garden-after-lead">推送之后,网站公开更新。</p>
139
- <div className="garden-after-copy">
140
- <p>文章在你自己的仓库里。读者打开的是已经发布的页面。</p>
141
- <p>一次更新会带上正文、目录、前后篇、搜索、RSS 和 sitemap。后台和评论都可以以后再加。</p>
142
- </div>
143
- </section>
65
+ {firstDoc ? (
66
+ <nav className="gdoc-pager" aria-label="说明书上下篇导航">
67
+ <Link href={gardenDocHref(firstDoc)} className="gdoc-pager-card is-next">
68
+ <span className="gdoc-pager-dir">
69
+ 下一篇
70
+ <ArrowRight size={14} aria-hidden="true" />
71
+ </span>
72
+ <strong>{firstDoc.title}</strong>
73
+ </Link>
74
+ </nav>
75
+ ) : null}
76
+ </article>
144
77
 
145
- <GardenAdminGuide />
78
+ <TableOfContents headings={headings} label="本页目录" />
146
79
  </main>
147
80
  );
148
81
  }
@@ -48,6 +48,21 @@ declare module "@garden/generated-content" {
48
48
  export const posts: ContentEntry[];
49
49
  export const columns: ContentEntry[];
50
50
  export const thoughts: ContentEntry[];
51
+ export type GardenDocEntry = {
52
+ title: string;
53
+ slug: string;
54
+ path: string;
55
+ description?: string;
56
+ section?: string;
57
+ sectionTitle?: string;
58
+ order?: number;
59
+ readingTime?: string;
60
+ references?: { title: string; url?: string; domain?: string }[];
61
+ sourcePath: string;
62
+ content: string;
63
+ };
64
+ export const gardenDocs: GardenDocEntry[];
65
+ export const gardenIndexDoc: GardenDocEntry | null;
51
66
  export const a2WordCounts: Record<string, number>;
52
67
  export type LinkCardMeta = {
53
68
  title: string;
package/app/globals.css CHANGED
@@ -4828,14 +4828,14 @@ html[data-theme="dark"] .search-footer {
4828
4828
  .garden-page,
4829
4829
  .admin-shell,
4830
4830
  .admin-access-page {
4831
- --garden-ink: #1d1d1f;
4832
- --garden-muted: #6e6e73;
4833
- --garden-stage: #f5f5f7;
4834
- --garden-link: #0071e3;
4835
- --garden-button: #1d1d1f;
4836
- --garden-button-text: #fff;
4831
+ --garden-ink: var(--ink);
4832
+ --garden-muted: var(--muted);
4833
+ --garden-stage: var(--paper-deep);
4834
+ --garden-link: var(--ink);
4835
+ --garden-button: var(--ink);
4836
+ --garden-button-text: var(--paper);
4837
4837
  color: var(--garden-ink);
4838
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", sans-serif;
4838
+ font-family: var(--sans);
4839
4839
  }
4840
4840
 
4841
4841
  :root[data-theme="dark"] .garden-page,
@@ -4844,9 +4844,9 @@ html[data-theme="dark"] .search-footer {
4844
4844
  --garden-ink: var(--ink);
4845
4845
  --garden-muted: var(--muted);
4846
4846
  --garden-stage: var(--paper-deep);
4847
- --garden-link: #8ab4f8;
4848
- --garden-button: #f5f5f7;
4849
- --garden-button-text: #1d1d1f;
4847
+ --garden-link: var(--ink);
4848
+ --garden-button: var(--ink);
4849
+ --garden-button-text: var(--paper);
4850
4850
  }
4851
4851
 
4852
4852
  .garden-page pre,
@@ -1,9 +1,12 @@
1
1
  import { markdownPlugins } from "./markdown-plugins";
2
2
  import {
3
3
  columns,
4
+ gardenDocs,
5
+ gardenIndexDoc,
4
6
  posts,
5
7
  thoughts,
6
8
  type ContentEntry,
9
+ type GardenDocEntry,
7
10
  type MediaItem,
8
11
  } from "@garden/generated-content";
9
12
  import GithubSlugger from "github-slugger";
@@ -14,13 +17,14 @@ import { toString } from "mdast-util-to-string";
14
17
  import remarkParse from "remark-parse";
15
18
  import { unified } from "unified";
16
19
  import { visit } from "unist-util-visit";
17
- import { splitHeadingNumber } from "./heading-number";
20
+ import { articleUsesHeadingNumbers, autoNumbersHeadings, splitHeadingNumber } from "./heading-number";
18
21
  import { siteHref } from "./site-href";
19
22
 
20
- export { columns, posts, thoughts };
21
- export type { ContentEntry, MediaItem };
23
+ export { columns, gardenDocs, gardenIndexDoc, posts, thoughts };
24
+ export type { ContentEntry, GardenDocEntry, MediaItem };
22
25
 
23
26
  export const postHref = (post: Pick<ContentEntry, "path">) => siteHref(`/blog/${post.path}`);
27
+ export const gardenDocHref = (doc: Pick<GardenDocEntry, "path">) => siteHref(`/garden/${doc.path}`);
24
28
  export const columnEntryPath = (entry: Pick<ContentEntry, "column" | "path">) => {
25
29
  const prefix = entry.column ? `${entry.column}/` : "";
26
30
  return prefix && entry.path.startsWith(prefix) ? entry.path.slice(prefix.length) : entry.path;
@@ -51,10 +55,8 @@ export const formatDate = (value: string, withTime = false) => {
51
55
  }).format(date);
52
56
  };
53
57
 
54
- const NUMBERED_ARTICLE = /^#{2,3}[ \t]+\d/m;
55
-
56
58
  /** An article that already writes its own heading numbers is not auto-numbered. */
57
- export const articleUsesHeadingNumbers = (markdown: string) => NUMBERED_ARTICLE.test(markdown);
59
+ export { articleUsesHeadingNumbers, autoNumbersHeadings };
58
60
 
59
61
  /**
60
62
  * Prototype render: an article with no `##` treats `###` as the top section.
@@ -76,11 +78,12 @@ export { splitHeadingNumber };
76
78
  * rehype-slug. Every heading depth advances the slugger so headings outside
77
79
  * the H2-H5 TOC range cannot shift the IDs shown in the directory.
78
80
  * `no` follows the prototype: keep an author-written number, otherwise
79
- * auto-number h2/h3 only when the article has no numbered headings.
81
+ * auto-number h2/h3 only when the article has no numbered headings and its
82
+ * front matter does not say `numbering: false`.
80
83
  */
81
- export const extractHeadings = (markdown: string) => {
84
+ export const extractHeadings = (markdown: string, { numbering }: { numbering?: boolean } = {}) => {
82
85
  const slugger = new GithubSlugger();
83
- const numbered = articleUsesHeadingNumbers(markdown);
86
+ const autoNumber = autoNumbersHeadings(markdown, numbering);
84
87
  const base = articleHeadingBase(markdown);
85
88
  const headings: Array<{ depth: number; text: string; id: string; no?: string }> = [];
86
89
  const tree = unified().use(remarkParse).use(markdownPlugins).parse(markdown);
@@ -101,7 +104,7 @@ export const extractHeadings = (markdown: string) => {
101
104
  text = split.text;
102
105
  if (level === 2) { h2 += 1; h3 = 0; }
103
106
  else h3 += 1;
104
- } else if (!numbered) {
107
+ } else if (autoNumber) {
105
108
  if (level === 2) { h2 += 1; h3 = 0; no = String(h2); }
106
109
  else { h3 += 1; no = h2 ? `${h2}.${h3}` : String(h3); }
107
110
  }
@@ -129,6 +132,9 @@ export const findPost = (path: string): ContentEntry | undefined =>
129
132
  export const findThought = (slug: string): ContentEntry | undefined =>
130
133
  thoughts.find((thought) => thought.slug === slug);
131
134
 
135
+ export const findGardenDoc = (path: string): GardenDocEntry | undefined =>
136
+ gardenDocs.find((doc) => doc.path === path);
137
+
132
138
  export const findColumnEntry = (
133
139
  column: string,
134
140
  path: string,
@@ -0,0 +1,83 @@
1
+ export type GardenDocNavItem = {
2
+ path: string;
3
+ title: string;
4
+ description?: string;
5
+ section?: string;
6
+ order?: number;
7
+ };
8
+
9
+ export type HandbookSectionMeta = {
10
+ id: string;
11
+ title: string;
12
+ note?: string;
13
+ };
14
+
15
+ export const HANDBOOK_SECTIONS: HandbookSectionMeta[] = [
16
+ { id: "getting-started", title: "快速上手", note: "建站、配置与发布" },
17
+ { id: "writing-and-reading", title: "写作与排版", note: "Markdown 与阅读版面" },
18
+ { id: "components", title: "内置组件", note: "garden <name>" },
19
+ { id: "workflow", title: "检查与补全", note: "语法检查与编辑器片段" },
20
+ ];
21
+
22
+ export const COMPONENT_BADGE: Record<string, string> = {
23
+ "components/card": "card",
24
+ "components/stat": "stat",
25
+ "components/compare": "compare",
26
+ "components/timeline": "timeline",
27
+ "components/chart": "chart",
28
+ "components/figure": "figure",
29
+ "components/html": "html",
30
+ "components/interactive": "interactive",
31
+ "components/file": "file",
32
+ };
33
+
34
+ export const NAV_SHORT_TITLE: Record<string, string> = {
35
+ start: "快速开始",
36
+ config: "站点与专栏配置",
37
+ deploy: "发布到 GitHub Pages",
38
+ writing: "写文章与随想",
39
+ design: "阅读排版是怎么工作的",
40
+ components: "组件总览",
41
+ "components/card": "知识卡片",
42
+ "components/stat": "数字指标",
43
+ "components/compare": "并排对比",
44
+ "components/timeline": "时间线",
45
+ "components/chart": "条形图表",
46
+ "components/figure": "分区结构图",
47
+ "components/html": "嵌入网页片段",
48
+ "components/interactive": "可调公式与参数",
49
+ "components/file": "文件下载与预览",
50
+ workflow: "语法检查与快捷输入",
51
+ };
52
+
53
+ export function groupHandbookDocs<T extends GardenDocNavItem>(docs: T[]) {
54
+ const bySection = new Map<string, T[]>();
55
+ for (const section of HANDBOOK_SECTIONS) {
56
+ bySection.set(section.id, []);
57
+ }
58
+ for (const doc of docs) {
59
+ const key = doc.section && bySection.has(doc.section) ? doc.section : "getting-started";
60
+ bySection.get(key)!.push(doc);
61
+ }
62
+ for (const list of bySection.values()) {
63
+ list.sort((a, b) => (a.order ?? 999) - (b.order ?? 999) || a.path.localeCompare(b.path));
64
+ }
65
+ return HANDBOOK_SECTIONS.map((section) => ({
66
+ ...section,
67
+ items: bySection.get(section.id) ?? [],
68
+ })).filter((section) => section.items.length > 0);
69
+ }
70
+
71
+ export function orderedHandbookDocs<T extends GardenDocNavItem>(docs: T[]): T[] {
72
+ return groupHandbookDocs(docs).flatMap((section) => section.items);
73
+ }
74
+
75
+ export function toHandbookNavItems(docs: GardenDocNavItem[]): GardenDocNavItem[] {
76
+ return orderedHandbookDocs(docs).map((doc) => ({
77
+ path: doc.path,
78
+ title: doc.title,
79
+ description: doc.description,
80
+ section: doc.section,
81
+ order: doc.order,
82
+ }));
83
+ }
@@ -7,3 +7,16 @@ export const splitHeadingNumber = (text: string) => {
7
7
  const match = /^(\d+(?:\.\d+)*\.?)[ \t]+([\s\S]+)$/u.exec(text.trim());
8
8
  return match ? { no: match[1], text: match[2] } : null;
9
9
  };
10
+
11
+ const NUMBERED_ARTICLE = /^#{2,3}[ \t]+\d/m;
12
+
13
+ /** True when the article writes its own section numbers (`## 2. …`). */
14
+ export const articleUsesHeadingNumbers = (markdown: string) => NUMBERED_ARTICLE.test(markdown);
15
+
16
+ /**
17
+ * Whether h2/h3 get automatic numbers. Off when the author writes numbers, or
18
+ * when the front matter says `numbering: false`. Author-written numbers are
19
+ * always kept.
20
+ */
21
+ export const autoNumbersHeadings = (markdown: string, numbering?: boolean) =>
22
+ numbering !== false && !articleUsesHeadingNumbers(markdown);