hd-garden 0.3.13 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/garden-interactive-chart/SKILL.md +6 -6
- package/.agents/skills/garden-ui/SKILL.md +4 -4
- package/app/a2/[slug]/page.tsx +61 -0
- package/app/a2/page.tsx +27 -0
- package/app/blog/[...path]/page.tsx +32 -22
- package/app/blog/page.tsx +6 -7
- package/app/columns/[column]/[...path]/page.tsx +32 -25
- package/app/columns/page.tsx +38 -52
- package/app/components/ArticleMeasure.tsx +95 -0
- package/app/components/ArticleTitle.tsx +20 -0
- package/app/components/BlogArchive.tsx +106 -102
- package/app/components/MarkdownArticle.tsx +145 -66
- package/app/components/MermaidDiagram.tsx +44 -71
- package/app/components/SiteFooter.tsx +3 -7
- package/app/components/SiteHeader.tsx +55 -41
- package/app/components/TableOfContents.tsx +3 -2
- package/app/components/TagIndex.tsx +97 -0
- package/app/components/garden/GardenBlock.tsx +45 -0
- package/app/components/garden/blocks.tsx +530 -0
- package/app/components/garden/chrome.tsx +350 -0
- package/app/components/garden/sandbox-message.ts +16 -0
- package/app/garden-modules.d.ts +2 -0
- package/app/globals.css +17 -12
- package/app/layout.tsx +9 -0
- package/app/lib/a2-meta.ts +22 -0
- package/app/lib/a2-preview.ts +50 -0
- package/app/lib/content.ts +52 -6
- package/app/lib/markdown-plugins.ts +2 -0
- package/app/lib/remark-a2.mjs +208 -0
- package/app/lib/remark-garden.mjs +88 -0
- package/app/page.tsx +122 -93
- package/app/styles/a2.css +1383 -0
- package/app/styles/shell.css +438 -0
- package/app/styles/tokens.css +111 -0
- package/app/tags/page.tsx +28 -0
- package/app/ui-parity/page.tsx +3 -2
- package/package.json +1 -1
- package/scripts/build-content.mjs +11 -1
- package/scripts/check-post.mjs +18 -4
- package/scripts/new-post.mjs +3 -0
- package/site.config.yaml +1 -1
- package/templates/garden-blocks.md +128 -0
- package/templates/garden-snippets.plist +121 -0
- package/ui/garden/catalog.mjs +196 -0
- package/ui/garden/check.mjs +103 -0
- package/ui/garden/enhance.mjs +89 -0
- package/ui/garden/parse.mjs +198 -0
- package/ui/garden/snippets.mjs +109 -0
- package/ui/garden/title.mjs +37 -0
- package/ui/garden/word-count.mjs +266 -0
- package/ui/host.mjs +45 -28
- package/ui/parity-fixtures.mjs +1 -1
- package/ui/preview/dist/garden-ui.css +81 -77
- package/ui/preview/dist/garden-ui.esm.js +124 -89
- package/ui/preview/dist/garden-ui.iife.js +124 -89
- package/ui/prompt/rules.md +17 -1
- package/ui/react/CatalogPage.tsx +12 -0
- package/ui/react/components/display.tsx +1 -1
- package/ui/react/components/media.tsx +2 -2
- 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
|
|
5
|
+
import { githubUrl, siteConfig } from "./site.config";
|
|
13
6
|
|
|
14
7
|
export const metadata = {
|
|
15
|
-
title:
|
|
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="
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
{
|
|
43
|
-
<
|
|
49
|
+
{posts.length > 0 && feature ? (
|
|
50
|
+
<article className="sh-feature" aria-labelledby="ft">
|
|
51
|
+
{feature.cover ? (
|
|
52
|
+
<div className="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
|
-
<
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
64
|
-
<
|
|
65
|
-
<div>
|
|
66
|
-
<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
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
{
|
|
85
|
-
<
|
|
86
|
-
<div>
|
|
87
|
-
<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
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
{
|
|
99
|
-
<
|
|
100
|
-
<div>
|
|
101
|
-
<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
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
</section>}
|
|
144
|
+
</section>
|
|
145
|
+
) : null}
|
|
117
146
|
</main>
|
|
118
147
|
);
|
|
119
148
|
}
|