hd-garden 0.3.13 → 0.4.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 (58) hide show
  1. package/app/a2/[slug]/page.tsx +61 -0
  2. package/app/a2/page.tsx +27 -0
  3. package/app/blog/[...path]/page.tsx +33 -22
  4. package/app/blog/page.tsx +6 -7
  5. package/app/columns/[column]/[...path]/page.tsx +33 -25
  6. package/app/columns/page.tsx +38 -52
  7. package/app/components/ArticleMeasure.tsx +95 -0
  8. package/app/components/ArticleTitle.tsx +20 -0
  9. package/app/components/BlogArchive.tsx +106 -102
  10. package/app/components/MarkdownArticle.tsx +145 -66
  11. package/app/components/MermaidDiagram.tsx +44 -71
  12. package/app/components/SiteFooter.tsx +3 -7
  13. package/app/components/SiteHeader.tsx +55 -41
  14. package/app/components/TableOfContents.tsx +3 -2
  15. package/app/components/TagIndex.tsx +97 -0
  16. package/app/components/garden/GardenBlock.tsx +45 -0
  17. package/app/components/garden/blocks.tsx +530 -0
  18. package/app/components/garden/chrome.tsx +350 -0
  19. package/app/components/garden/sandbox-message.ts +16 -0
  20. package/app/garden-modules.d.ts +2 -0
  21. package/app/globals.css +17 -12
  22. package/app/layout.tsx +9 -0
  23. package/app/lib/a2-meta.ts +22 -0
  24. package/app/lib/a2-preview.ts +50 -0
  25. package/app/lib/content.ts +48 -5
  26. package/app/lib/markdown-plugins.ts +2 -0
  27. package/app/lib/remark-a2.mjs +208 -0
  28. package/app/lib/remark-garden.mjs +88 -0
  29. package/app/page.tsx +122 -93
  30. package/app/styles/a2.css +1383 -0
  31. package/app/styles/shell.css +438 -0
  32. package/app/styles/tokens.css +111 -0
  33. package/app/tags/page.tsx +28 -0
  34. package/app/ui-parity/page.tsx +3 -2
  35. package/package.json +1 -1
  36. package/scripts/build-content.mjs +11 -1
  37. package/scripts/check-post.mjs +18 -4
  38. package/scripts/new-post.mjs +3 -0
  39. package/site.config.yaml +1 -1
  40. package/templates/garden-blocks.md +128 -0
  41. package/templates/garden-snippets.plist +121 -0
  42. package/ui/garden/catalog.mjs +196 -0
  43. package/ui/garden/check.mjs +103 -0
  44. package/ui/garden/enhance.mjs +89 -0
  45. package/ui/garden/parse.mjs +198 -0
  46. package/ui/garden/snippets.mjs +109 -0
  47. package/ui/garden/title.mjs +37 -0
  48. package/ui/garden/word-count.mjs +266 -0
  49. package/ui/host.mjs +45 -28
  50. package/ui/parity-fixtures.mjs +1 -1
  51. package/ui/preview/dist/garden-ui.css +81 -77
  52. package/ui/preview/dist/garden-ui.esm.js +124 -89
  53. package/ui/preview/dist/garden-ui.iife.js +124 -89
  54. package/ui/prompt/rules.md +17 -1
  55. package/ui/react/CatalogPage.tsx +12 -0
  56. package/ui/react/components/display.tsx +1 -1
  57. package/ui/react/components/media.tsx +2 -2
  58. package/ui/react/ui.css +81 -77
@@ -0,0 +1,208 @@
1
+ import { toString } from "mdast-util-to-string";
2
+
3
+ const ALERT_TITLE = {
4
+ note: "提醒",
5
+ tip: "建议",
6
+ important: "重要",
7
+ warning: "警告",
8
+ caution: "注意",
9
+ };
10
+
11
+ const LEGACY_KIND = {
12
+ info: "note",
13
+ success: "tip",
14
+ warning: "warning",
15
+ danger: "caution",
16
+ note: "note",
17
+ };
18
+
19
+ const ALERT_ICON = {
20
+ note: '<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M8 7.2v4"/><path d="M8 4.8v.1" stroke-width="2"/></svg>',
21
+ tip: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 12.5h4M6.5 14.5h3M8 1.75a4.5 4.5 0 0 0-2.6 8.2c.4.3.6.7.6 1.2v.35h4v-.35c0-.5.2-.9.6-1.2A4.5 4.5 0 0 0 8 1.75Z"/></svg>',
22
+ important: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.25 3.25a1 1 0 0 1 1-1h9.5a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7l-3 2.5v-2.5h-.75a1 1 0 0 1-1-1Z"/><path d="M8 4.75v3"/><path d="M8 9.4v.1" stroke-width="2"/></svg>',
23
+ warning: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2 1.75 13.25h12.5Z"/><path d="M8 6.5v3.25"/><path d="M8 11.6v.1" stroke-width="2"/></svg>',
24
+ caution: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5.3 1.75h5.4l3.55 3.55v5.4l-3.55 3.55H5.3l-3.55-3.55V5.3Z"/><path d="M8 4.75v3.75"/><path d="M8 10.9v.1" stroke-width="2"/></svg>',
25
+ };
26
+
27
+ function meaningfulChildren(node) {
28
+ return (node.children ?? []).filter((child) => !(child.type === "text" && !String(child.value).trim()));
29
+ }
30
+
31
+ function visibleText(node) {
32
+ const parts = [];
33
+ const visit = (current) => {
34
+ if (!current || current.type === "html") return;
35
+ if (current.type === "text") {
36
+ parts.push(current.value);
37
+ return;
38
+ }
39
+ for (const child of current.children ?? []) visit(child);
40
+ };
41
+ visit(node);
42
+ return parts.join("").trim();
43
+ }
44
+
45
+ function isBlankParagraph(node) {
46
+ return node?.type === "paragraph" && toString(node).trim() === "" && meaningfulChildren(node).length === 0;
47
+ }
48
+
49
+ function alertTitle(kind, label) {
50
+ return {
51
+ type: "paragraph",
52
+ data: { hName: "p", hProperties: { className: ["markdown-alert-title"] } },
53
+ children: [
54
+ { type: "html", value: ALERT_ICON[kind] },
55
+ { type: "text", value: label || ALERT_TITLE[kind] },
56
+ ],
57
+ };
58
+ }
59
+
60
+ const ALERT_KIND = {
61
+ NOTE: "note",
62
+ INFO: "note",
63
+ TIP: "tip",
64
+ SUCCESS: "tip",
65
+ IMPORTANT: "important",
66
+ WARNING: "warning",
67
+ CAUTION: "caution",
68
+ DANGER: "caution",
69
+ FAILURE: "caution",
70
+ };
71
+
72
+ function paragraphFirstLine(node) {
73
+ let line = "";
74
+ for (const child of node.children ?? []) {
75
+ if (child.type === "break") break;
76
+ if (child.type === "text") {
77
+ const newline = child.value.indexOf("\n");
78
+ line += newline >= 0 ? child.value.slice(0, newline) : child.value;
79
+ if (newline >= 0) break;
80
+ continue;
81
+ }
82
+ line += toString(child);
83
+ }
84
+ return line;
85
+ }
86
+
87
+ function withoutMarkerLine(node) {
88
+ const children = [];
89
+ let skipping = true;
90
+ for (const child of node.children ?? []) {
91
+ if (!skipping) {
92
+ children.push(child);
93
+ continue;
94
+ }
95
+ if (child.type === "break") {
96
+ skipping = false;
97
+ continue;
98
+ }
99
+ if (child.type === "text") {
100
+ const newline = child.value.indexOf("\n");
101
+ if (newline >= 0) {
102
+ skipping = false;
103
+ const rest = child.value.slice(newline + 1);
104
+ if (rest) children.push({ type: "text", value: rest });
105
+ }
106
+ }
107
+ }
108
+ if (!children.some((child) => child.type !== "text" || child.value.trim())) return null;
109
+ return { type: "paragraph", children };
110
+ }
111
+
112
+ function gfmAlert(node) {
113
+ if (node?.type !== "paragraph") return null;
114
+ const match = /^\[!(NOTE|INFO|TIP|SUCCESS|IMPORTANT|WARNING|CAUTION|DANGER|FAILURE)\][ \t]*(.*)$/iu.exec(paragraphFirstLine(node).trim());
115
+ if (!match) return null;
116
+ return {
117
+ kind: ALERT_KIND[match[1].toUpperCase()],
118
+ label: match[2].trim(),
119
+ body: withoutMarkerLine(node),
120
+ };
121
+ }
122
+
123
+ function legacyAlert(node) {
124
+ if (node?.type !== "paragraph" || visibleText(node)) return null;
125
+ const html = (node.children ?? []).filter((child) => child.type === "html").map((child) => child.value).join("");
126
+ const match = /garden-callout-marker--(info|success|warning|danger|note)/u.exec(html);
127
+ if (!match) return null;
128
+ return LEGACY_KIND[match[1]] || "note";
129
+ }
130
+
131
+ function strongOnlyText(node) {
132
+ if (node?.type !== "paragraph") return "";
133
+ const children = meaningfulChildren(node);
134
+ if (children.length !== 1 || children[0].type !== "strong") return "";
135
+ return toString(children[0]).trim();
136
+ }
137
+
138
+ function rewriteAlert(node) {
139
+ const children = node.children ?? [];
140
+ const gfm = gfmAlert(children[0]);
141
+ const legacy = gfm ? null : legacyAlert(children[0]);
142
+ if (!gfm && !legacy) return;
143
+ const kind = gfm ? gfm.kind : legacy;
144
+ let label = gfm ? gfm.label : "";
145
+ let rest = gfm?.body ? [gfm.body, ...children.slice(1)] : children.slice(1);
146
+ if (!label) {
147
+ const custom = strongOnlyText(rest.find((child) => !isBlankParagraph(child)));
148
+ if (custom) {
149
+ label = custom;
150
+ const index = rest.findIndex((child) => !isBlankParagraph(child));
151
+ rest = rest.filter((_, childIndex) => childIndex !== index);
152
+ }
153
+ }
154
+ while (rest.length && isBlankParagraph(rest[0])) rest.shift();
155
+ node.children = [alertTitle(kind, label), ...rest];
156
+ node.data ??= {};
157
+ node.data.hProperties ??= {};
158
+ const existing = node.data.hProperties.className;
159
+ const classes = Array.isArray(existing) ? existing : existing ? [existing] : [];
160
+ node.data.hProperties.className = [...classes, "markdown-alert", `markdown-alert-${kind}`];
161
+ }
162
+
163
+ function figureCaption(image) {
164
+ const title = String(image.title ?? "").trim();
165
+ const alt = String(image.alt ?? "").trim();
166
+ if (title) return title;
167
+ if (alt && !/^image|^截图|^img/iu.test(alt)) return alt;
168
+ return "";
169
+ }
170
+
171
+ function asFigure(node) {
172
+ if (node?.type !== "paragraph" || node.data?.hName) return null;
173
+ const children = meaningfulChildren(node);
174
+ if (children.length !== 1 || children[0].type !== "image") return null;
175
+ const image = children[0];
176
+ const caption = figureCaption(image);
177
+ const card = {
178
+ type: "paragraph",
179
+ data: { hName: "div", hProperties: { className: ["fig-card"] } },
180
+ children: [image],
181
+ };
182
+ const figureChildren = caption
183
+ ? [card, {
184
+ type: "paragraph",
185
+ data: { hName: "figcaption" },
186
+ children: [{ type: "text", value: caption }],
187
+ }]
188
+ : [card];
189
+ return {
190
+ type: "paragraph",
191
+ data: { hName: "figure", hProperties: { className: ["fig"] } },
192
+ children: figureChildren,
193
+ };
194
+ }
195
+
196
+ function walk(node) {
197
+ if (!node || typeof node !== "object" || !Array.isArray(node.children)) return;
198
+ if (node.type === "blockquote") rewriteAlert(node);
199
+ node.children = node.children.map((child) => asFigure(child) ?? child);
200
+ for (const child of node.children) walk(child);
201
+ }
202
+
203
+ /** Demo alert cards and captioned figures. Article rendering only. */
204
+ export function remarkA2() {
205
+ return (tree) => {
206
+ walk(tree);
207
+ };
208
+ }
@@ -0,0 +1,88 @@
1
+ import { diffToNewCode, protectMathIdentifiers } from "../../ui/garden/enhance.mjs";
2
+ import { classifyFence } from "../../ui/garden/parse.mjs";
3
+
4
+ function gardenElement(payload) {
5
+ return {
6
+ type: "gardenBlock",
7
+ data: {
8
+ hName: "garden-block",
9
+ hProperties: { dataPayload: encodeURIComponent(JSON.stringify(payload)) },
10
+ },
11
+ };
12
+ }
13
+
14
+ function codeDiffElement(code, diff) {
15
+ return {
16
+ type: "gardenCodeDiff",
17
+ data: {
18
+ hName: "garden-codediff",
19
+ hProperties: {
20
+ dataNew: encodeURIComponent(diffToNewCode(diff.value)),
21
+ dataDiff: encodeURIComponent(diff.value),
22
+ },
23
+ },
24
+ children: [code],
25
+ };
26
+ }
27
+
28
+ function fenceNodeToRecord(node) {
29
+ return { lang: String(node.lang || "").toLowerCase(), meta: String(node.meta || ""), info: `${node.lang || ""} ${node.meta || ""}`.trim() };
30
+ }
31
+
32
+ function rewriteChildren(node) {
33
+ if (!Array.isArray(node.children)) return;
34
+ const next = [];
35
+ for (let index = 0; index < node.children.length; index += 1) {
36
+ const child = node.children[index];
37
+ const following = node.children[index + 1];
38
+ if (child?.type === "code" && following?.type === "code" && String(following.lang || "").toLowerCase() === "diff") {
39
+ const current = fenceNodeToRecord(child);
40
+ const kind = classifyFence({ lang: current.lang, meta: current.meta, info: current.info });
41
+ if (kind.kind === "code") {
42
+ next.push(codeDiffElement(child, following));
43
+ index += 1;
44
+ continue;
45
+ }
46
+ }
47
+ if (child?.type === "code") {
48
+ const record = fenceNodeToRecord(child);
49
+ const kind = classifyFence({ ...record, body: child.value, line: 1, endLine: 1 });
50
+ if (kind.kind === "legacy") {
51
+ next.push(gardenElement({ legacy: true, from: kind.lang || record.lang, suggestion: kind.suggestion }));
52
+ continue;
53
+ }
54
+ if (kind.kind === "garden" && kind.block) {
55
+ next.push(gardenElement(kind.block));
56
+ continue;
57
+ }
58
+ }
59
+ next.push(child);
60
+ }
61
+ node.children = next;
62
+ }
63
+
64
+ function walk(node) {
65
+ if (!node || typeof node !== "object") return;
66
+ if (node.type === "math" || node.type === "inlineMath") {
67
+ node.value = protectMathIdentifiers(node.value);
68
+ }
69
+ if (Array.isArray(node.children)) {
70
+ for (const child of node.children) walk(child);
71
+ rewriteChildren(node);
72
+ }
73
+ }
74
+
75
+ export function remarkGarden() {
76
+ return (tree) => {
77
+ walk(tree);
78
+ };
79
+ }
80
+
81
+ export function readGardenPayload(value) {
82
+ if (!value) return null;
83
+ try {
84
+ return JSON.parse(decodeURIComponent(String(value)));
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
package/app/page.tsx CHANGED
@@ -1,119 +1,148 @@
1
1
  import Link from "next/link";
2
- import {
3
- ArrowRight,
4
- ArrowUpRight,
5
- GithubLogo,
6
- PushPin,
7
- } from "@phosphor-icons/react/ssr";
8
- import { columnHref, columns, formatDate, postHref, posts } from "./lib/content";
2
+ import { columns, postHref, posts } from "./lib/content";
9
3
  import { ThoughtCard } from "./components/ThoughtCard";
10
- import { PageIntro } from "./components/PageIntro";
11
4
  import { getPublishedThoughts } from "./lib/public-thoughts";
12
- import { githubUrl, siteConfig, siteDocumentTitle } from "./site.config";
5
+ import { githubUrl, siteConfig } from "./site.config";
13
6
 
14
7
  export const metadata = {
15
- title: siteDocumentTitle,
8
+ title: { absolute: siteConfig.name },
16
9
  description: siteConfig.description,
17
10
  };
18
11
 
12
+ function day(value: string) {
13
+ const date = new Date(value);
14
+ if (Number.isNaN(date.getTime())) return value.slice(0, 10);
15
+ return new Intl.DateTimeFormat("en-CA", {
16
+ timeZone: "Asia/Shanghai",
17
+ year: "numeric",
18
+ month: "2-digit",
19
+ day: "2-digit",
20
+ }).format(date);
21
+ }
22
+
19
23
  export default function Home() {
24
+ const recent = [...posts].sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
25
+ const feature = recent[0];
20
26
  const pinned = posts.filter((post) => post.pinned);
21
27
  const thoughts = getPublishedThoughts();
28
+ const series = Array.from(
29
+ columns.reduce((result, entry) => {
30
+ const existing = result.get(entry.column ?? "");
31
+ if (existing) existing.push(entry);
32
+ else result.set(entry.column ?? "", [entry]);
33
+ return result;
34
+ }, new Map<string, typeof columns>()),
35
+ );
22
36
 
23
37
  return (
24
- <main className="page-shell home-page">
25
- <PageIntro
26
- eyebrow="HELLO, WORLD · 你好,世界"
27
- title={siteConfig.pages.home.title}
28
- subtitle={siteConfig.pages.home.subtitle}
29
- >
30
- <a
31
- className="inline-cta"
32
- href={githubUrl}
33
- target="_blank"
34
- rel="me noopener noreferrer"
35
- >
36
- <GithubLogo size={21} weight="fill" />
37
- 在 GitHub 找到我
38
- <ArrowUpRight size={16} />
39
- </a>
40
- </PageIntro>
38
+ <main className="sh-main" id="content">
39
+ <section className="sh-intro">
40
+ <h1>{siteConfig.pages.home.title}</h1>
41
+ <p>{siteConfig.pages.home.subtitle || siteConfig.description}</p>
42
+ <div className="links">
43
+ <Link href="/about">关于我</Link>
44
+ <a href={githubUrl} rel="me">GitHub</a>
45
+ <a href="/rss.xml">RSS 订阅</a>
46
+ </div>
47
+ </section>
41
48
 
42
- {pinned.length > 0 && <section className="home-section">
43
- <div className="section-heading accent-heading">
49
+ {posts.length > 0 && feature ? (
50
+ <article className="sh-feature" aria-labelledby="ft">
51
+ {feature.cover ? (
52
+ <div className="cover" style={{ background: "var(--cover)" }}>
53
+ {/* eslint-disable-next-line @next/next/no-img-element */}
54
+ <img src={feature.cover} alt="" />
55
+ </div>
56
+ ) : null}
44
57
  <div>
45
- <h2>置顶博文</h2>
58
+ <p className="kicker">
59
+ <span>最新文章</span>
60
+ <time className="num" dateTime={day(feature.date)}>{day(feature.date)}</time>
61
+ {feature.topic ? <span>{feature.topic}</span> : null}
62
+ </p>
63
+ <h2 id="ft"><Link href={postHref(feature)}>{feature.title}</Link></h2>
64
+ {feature.description ? <p>{feature.description}</p> : null}
65
+ {feature.tags?.length ? (
66
+ <p className="tags">{feature.tags.map((tag) => <span key={tag}>#{tag}</span>)}</p>
67
+ ) : null}
46
68
  </div>
47
- <PushPin size={20} weight="light" aria-hidden="true" />
48
- </div>
49
- <div className="post-list">
50
- {pinned.map((post) => (
51
- <Link href={postHref(post)} key={post.path} className="post-row">
52
- <time>{formatDate(post.date)}</time>
53
- <span>
54
- <strong>{post.title}</strong>
55
- <small>{post.topic}</small>
56
- </span>
57
- <ArrowRight size={18} />
58
- </Link>
59
- ))}
60
- </div>
61
- </section>}
69
+ </article>
70
+ ) : null}
62
71
 
63
- {posts.length > 0 && <section className="home-section">
64
- <div className="section-heading accent-heading">
65
- <div>
66
- <h2>最近博文</h2>
72
+ <div className="sh-home-grid">
73
+ <section className="sh-section" aria-labelledby="h-recent">
74
+ <div className="sh-sec-head">
75
+ <h2 id="h-recent">最近更新</h2>
76
+ <Link className="sh-more" href="/blog">全部 {posts.length} 篇</Link>
67
77
  </div>
68
- <Link href="/blog">全部文章 →</Link>
69
- </div>
70
- <div className="post-list">
71
- {posts.slice(0, 5).map((post) => (
72
- <Link href={postHref(post)} key={post.path} className="post-row">
73
- <time>{formatDate(post.date)}</time>
74
- <span>
75
- <strong>{post.title}</strong>
76
- <small>{post.topic}</small>
77
- </span>
78
- <ArrowRight size={18} />
79
- </Link>
80
- ))}
81
- </div>
82
- </section>}
78
+ <ol className="sh-rows">
79
+ {recent.slice(0, 7).map((post) => (
80
+ <li className="sh-row" key={post.path}>
81
+ <time className="num" dateTime={day(post.date)}>{day(post.date)}</time>
82
+ <h3><Link href={postHref(post)}>{post.title}</Link></h3>
83
+ {post.description ? <p>{post.description}</p> : null}
84
+ {post.topic ? <Link className="cat" href={`/blog?topic=${encodeURIComponent(post.topic)}`}>{post.topic}</Link> : null}
85
+ </li>
86
+ ))}
87
+ </ol>
88
+ </section>
89
+ {pinned.length > 0 ? (
90
+ <section className="sh-section" aria-labelledby="h-pin">
91
+ <div className="sh-sec-head section-heading accent-heading"><h2 id="h-pin">置顶</h2></div>
92
+ <ul className="sh-side-list">
93
+ {pinned.map((post) => (
94
+ <li key={post.path}>
95
+ <Link href={postHref(post)}>{post.title}</Link>
96
+ <span><time className="num" dateTime={day(post.date)}>{day(post.date)}</time>{post.topic ? ` ${post.topic}` : ""}</span>
97
+ </li>
98
+ ))}
99
+ </ul>
100
+ </section>
101
+ ) : null}
102
+ </div>
83
103
 
84
- {thoughts.length > 0 && <section className="home-section">
85
- <div className="section-heading accent-heading">
86
- <div>
87
- <h2>最近随想</h2>
104
+ {columns[0] && series.length ? (
105
+ <section className="sh-section" aria-labelledby="h-cols">
106
+ <div className="sh-sec-head">
107
+ <h2 id="h-cols">专栏</h2>
108
+ <Link className="sh-more" href="/columns">全部专栏</Link>
88
109
  </div>
89
- <Link href="/thoughts">进入随想 →</Link>
90
- </div>
91
- <div className="home-thought-list">
92
- {thoughts.slice(0, 5).map((thought) => (
93
- <ThoughtCard key={thought.slug} thought={thought} />
94
- ))}
95
- </div>
96
- </section>}
110
+ <div className="sh-cols">
111
+ {series.map(([slug, entries]) => {
112
+ const first = entries[0];
113
+ return (
114
+ <article className="sh-col" key={slug}>
115
+ <div className="cover" style={{ background: "var(--well)" }}>
116
+ {first.columnCover ? (
117
+ // eslint-disable-next-line @next/next/no-img-element
118
+ <img src={first.columnCover} alt={first.columnCoverAlt ?? ""} />
119
+ ) : null}
120
+ </div>
121
+ <div className="body">
122
+ <h3><Link href={`/columns#${slug}`}>{first.columnTitle}</Link></h3>
123
+ <p className="count">{entries.length} 篇,{first.columnStatus ?? "持续更新"}</p>
124
+ <p>{first.columnDescription ?? first.description}</p>
125
+ </div>
126
+ </article>
127
+ );
128
+ })}
129
+ </div>
130
+ </section>
131
+ ) : null}
97
132
 
98
- {columns[0] && <section className="home-section column-preview">
99
- <div className="section-heading accent-heading">
100
- <div>
101
- <h2>主题专栏</h2>
133
+ {thoughts.length > 0 ? (
134
+ <section className="sh-section" aria-labelledby="h-thoughts">
135
+ <div className="sh-sec-head">
136
+ <h2 id="h-thoughts">最近随想</h2>
137
+ <Link className="sh-more" href="/thoughts">全部随想</Link>
102
138
  </div>
103
- <Link href="/columns">全部专栏 →</Link>
104
- </div>
105
- <Link
106
- href={columnHref(columns[0])}
107
- className="column-card"
108
- >
109
- <div>
110
- <span>连载中 · {columns.length} 篇</span>
111
- <h3>{columns[0].columnTitle}</h3>
112
- <p>记录一个人从想法、最小闭环到稳定发布的完整构建过程。</p>
139
+ <div className="home-thought-list">
140
+ {thoughts.slice(0, 5).map((thought) => (
141
+ <ThoughtCard key={thought.slug} thought={thought} />
142
+ ))}
113
143
  </div>
114
- <span className="column-number">#01</span>
115
- </Link>
116
- </section>}
144
+ </section>
145
+ ) : null}
117
146
  </main>
118
147
  );
119
148
  }