hd-garden 0.8.0 → 0.8.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-ui/SKILL.md +2 -2
- package/.env.en.example +67 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/app/a2/[slug]/page.tsx +4 -3
- package/app/a2/page.tsx +4 -3
- package/app/about/page.tsx +8 -9
- package/app/admin/page.tsx +7 -6
- package/app/admin/preview/[...path]/page.tsx +2 -1
- package/app/admin/ui/blocks/page.tsx +2 -1
- package/app/admin/ui/catalog/page.tsx +2 -1
- package/app/admin/ui/evals/page.tsx +2 -1
- package/app/admin/ui/page.tsx +2 -1
- package/app/blog/[...path]/page.tsx +7 -6
- package/app/blog/page.tsx +4 -2
- package/app/columns/[column]/[...path]/page.tsx +8 -7
- package/app/columns/page.tsx +12 -8
- package/app/components/AdminUiShell.tsx +4 -3
- package/app/components/ArticleReferences.tsx +4 -1
- package/app/components/ArticleTags.tsx +3 -2
- package/app/components/BlogArchive.tsx +20 -16
- package/app/components/Comments.tsx +16 -10
- package/app/components/ContentEmptyState.tsx +29 -28
- package/app/components/ExternalEmbed.tsx +15 -14
- package/app/components/GardenContentGuide.tsx +10 -2
- package/app/components/GardenDocsNav.tsx +4 -2
- package/app/components/GiscusComments.tsx +2 -1
- package/app/components/GithubLiveOverview.tsx +10 -8
- package/app/components/GithubProfile.tsx +31 -21
- package/app/components/HandbookLangSwitch.tsx +8 -4
- package/app/components/HandbookView.tsx +92 -9
- package/app/components/MarkdownArticle.tsx +31 -7
- package/app/components/SiteChrome.tsx +3 -3
- package/app/components/SiteFooter.tsx +1 -1
- package/app/components/SiteHeader.tsx +13 -4
- package/app/components/ThoughtCard.tsx +16 -14
- package/app/components/UiBlock.tsx +3 -2
- package/app/components/Webmentions.tsx +11 -11
- package/app/components/WidgetChart.tsx +8 -6
- package/app/components/garden/FilePreview.tsx +2 -2
- package/app/components/garden/GardenBlock.tsx +7 -0
- package/app/components/garden/GardenFile.tsx +2 -2
- package/app/components/garden/RefDemo.tsx +62 -0
- package/app/components/garden/blocks.tsx +56 -8
- package/app/components/garden/patterns.tsx +497 -0
- package/app/garden/[...slug]/page.tsx +3 -2
- package/app/garden/en/page.tsx +7 -7
- package/app/garden/page.tsx +3 -2
- package/app/globals.css +10 -0
- package/app/lib/a2-meta.ts +26 -3
- package/app/lib/content.ts +2 -2
- package/app/lib/garden-docs-meta.ts +24 -0
- package/app/lib/github-id.mjs +8 -0
- package/app/lib/handbook-switch.mjs +12 -0
- package/app/lib/i18n.ts +401 -2
- package/app/lib/rehype-field-tables.mjs +47 -0
- package/app/lib/remark-a2.mjs +18 -8
- package/app/lib/remark-link-card.mjs +22 -0
- package/app/lib/remark-ref-demos.mjs +42 -0
- package/app/lib/tag-filter.ts +9 -2
- package/app/not-found.tsx +32 -4
- package/app/page.tsx +17 -14
- package/app/rss.xml/route.ts +2 -1
- package/app/styles/a2.css +338 -0
- package/app/styles/shell.css +15 -2
- package/app/tags/page.tsx +7 -5
- package/app/thoughts/[slug]/page.tsx +2 -1
- package/app/thoughts/page/[n]/page.tsx +1 -1
- package/app/thoughts/page.tsx +2 -1
- package/app/ui-parity/page.tsx +38 -22
- package/bin/garden.mjs +129 -19
- package/docs/handbook/ai-writing.md +2 -2
- package/docs/handbook/assets/reading-layout-en.svg +26 -0
- package/docs/handbook/components/card.md +43 -22
- package/docs/handbook/components/chart.md +10 -26
- package/docs/handbook/components/choice.md +42 -0
- package/docs/handbook/components/compare.md +32 -17
- package/docs/handbook/components/figure.md +33 -46
- package/docs/handbook/components/file.md +10 -15
- package/docs/handbook/components/html.md +10 -30
- package/docs/handbook/components/interactive.md +9 -35
- package/docs/handbook/components/list.md +65 -0
- package/docs/handbook/components/relations.md +95 -0
- package/docs/handbook/components/stat.md +10 -28
- package/docs/handbook/components/timeline.md +13 -28
- package/docs/handbook/components.md +210 -19
- package/docs/handbook/config.md +5 -3
- package/docs/handbook/deploy.md +1 -1
- package/docs/handbook/design.md +20 -12
- package/docs/handbook/en/ai-writing.md +2 -2
- package/docs/handbook/en/components/card.md +42 -21
- package/docs/handbook/en/components/chart.md +10 -26
- package/docs/handbook/en/components/choice.md +42 -0
- package/docs/handbook/en/components/compare.md +30 -15
- package/docs/handbook/en/components/figure.md +29 -42
- package/docs/handbook/en/components/file.md +9 -14
- package/docs/handbook/en/components/html.md +9 -29
- package/docs/handbook/en/components/interactive.md +8 -34
- package/docs/handbook/en/components/list.md +65 -0
- package/docs/handbook/en/components/relations.md +95 -0
- package/docs/handbook/en/components/stat.md +9 -27
- package/docs/handbook/en/components/timeline.md +9 -24
- package/docs/handbook/en/components.md +211 -19
- package/docs/handbook/en/config.md +32 -30
- package/docs/handbook/en/deploy.md +12 -12
- package/docs/handbook/en/design.md +19 -12
- package/docs/handbook/en/index.md +2 -2
- package/docs/handbook/en/reference/card.md +85 -0
- package/docs/handbook/en/reference/chart.md +68 -0
- package/docs/handbook/en/reference/choice.md +69 -0
- package/docs/handbook/en/reference/compare.md +59 -0
- package/docs/handbook/en/reference/figure.md +84 -0
- package/docs/handbook/en/reference/file/checklist.md +17 -0
- package/docs/handbook/en/reference/file.md +50 -0
- package/docs/handbook/en/reference/html.md +62 -0
- package/docs/handbook/en/reference/interactive.md +71 -0
- package/docs/handbook/en/reference/list.md +72 -0
- package/docs/handbook/en/reference/relations.md +113 -0
- package/docs/handbook/en/reference/stat.md +60 -0
- package/docs/handbook/en/reference/timeline.md +56 -0
- package/docs/handbook/en/reference.md +26 -0
- package/docs/handbook/en/start.md +2 -2
- package/docs/handbook/en/workflow.md +6 -5
- package/docs/handbook/en/writing.md +3 -2
- package/docs/handbook/index.md +1 -1
- package/docs/handbook/reference/card.md +85 -0
- package/docs/handbook/reference/chart.md +68 -0
- package/docs/handbook/reference/choice.md +69 -0
- package/docs/handbook/reference/compare.md +59 -0
- package/docs/handbook/reference/figure.md +84 -0
- package/docs/handbook/reference/file/checklist.md +17 -0
- package/docs/handbook/reference/file.md +50 -0
- package/docs/handbook/reference/html.md +62 -0
- package/docs/handbook/reference/interactive.md +71 -0
- package/docs/handbook/reference/list.md +72 -0
- package/docs/handbook/reference/relations.md +113 -0
- package/docs/handbook/reference/stat.md +60 -0
- package/docs/handbook/reference/timeline.md +56 -0
- package/docs/handbook/reference.md +26 -0
- package/docs/handbook/start.md +1 -1
- package/docs/handbook/workflow.md +6 -5
- package/docs/handbook/writing.md +3 -2
- package/next.config.ts +6 -0
- package/package.json +2 -1
- package/public/images/garden/ref-card-book.svg +1 -0
- package/public/images/garden/ref-card-lamp.svg +1 -0
- package/scripts/build-content.mjs +37 -16
- package/scripts/favicon-ico.mjs +9 -6
- package/scripts/file-preview.mjs +10 -7
- package/scripts/handbook-allowlist.mjs +18 -0
- package/scripts/handbook-facts.mjs +305 -11
- package/scripts/link-assets.mjs +7 -0
- package/scripts/new-post.mjs +18 -9
- package/scripts/prepare-pages-output.mjs +30 -18
- package/scripts/redirects.mjs +17 -6
- package/templates/garden-blocks.md +67 -0
- package/templates/garden-snippets.plist +61 -0
- package/templates/site/Makefile.en +45 -0
- package/templates/site/README.en.md +15 -0
- package/templates/site/site.config.en.yaml +55 -0
- package/templates/skills-en/garden-interactive-chart/SKILL.md +84 -0
- package/templates/skills-en/garden-interactive-chart/references/shell.html +49 -0
- package/templates/skills-en/garden-interactive-chart/references/style.md +61 -0
- package/templates/skills-en/garden-post-review/SKILL.md +63 -0
- package/templates/skills-en/garden-ui/SKILL.md +118 -0
- package/templates/workflows/pages.en.yml +195 -0
- package/templates/workflows/pages.yml +195 -0
- package/ui/garden/catalog.mjs +205 -4
- package/ui/garden/check.mjs +6 -0
- package/ui/garden/file-block.mjs +8 -7
- package/ui/garden/html-lang.mjs +73 -1
- package/ui/garden/markdown-links.mjs +7 -4
- package/ui/garden/parse.mjs +45 -0
- package/ui/garden/relations.mjs +252 -0
- package/ui/garden/search-index.mjs +6 -4
- package/ui/garden/source-icon.mjs +54 -0
- package/ui/parity-fixtures.mjs +77 -0
- package/ui/preview/dist/garden-ui.esm.js +41 -4
- package/ui/preview/dist/garden-ui.iife.js +41 -4
- package/ui/prompt/rules.md +28 -1
- package/ui/react/components/chart.tsx +1 -1
- package/ui/react/components/display.tsx +1 -1
- package/ui/react/lang.tsx +4 -0
- package/ui/react/parity.tsx +8 -6
- package/worker/index.ts +8 -2
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "../lib/tag-filter";
|
|
18
18
|
import { ContentEmptyState } from "./ContentEmptyState";
|
|
19
19
|
import { siteHref } from "../lib/site-href";
|
|
20
|
+
import { siteLang, ui } from "../lib/i18n";
|
|
20
21
|
|
|
21
22
|
function day(value: string) {
|
|
22
23
|
const date = new Date(value);
|
|
@@ -72,6 +73,7 @@ export function BlogArchiveFallback({ posts, series }: ArchiveData) {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected: string[]; go: (next: string[]) => void }) {
|
|
76
|
+
const t = ui();
|
|
75
77
|
const [compact, setCompact] = useState(false);
|
|
76
78
|
const [expanded, setExpanded] = useState(false);
|
|
77
79
|
const options = useMemo(() => tagOptions(posts, selected), [posts, selected]);
|
|
@@ -81,7 +83,7 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
|
|
|
81
83
|
.sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
|
|
82
84
|
const years = new Map<string, typeof filtered>();
|
|
83
85
|
for (const post of filtered) {
|
|
84
|
-
const year = day(post.date).slice(0, 4) ||
|
|
86
|
+
const year = day(post.date).slice(0, 4) || t.unknownYear;
|
|
85
87
|
const list = years.get(year);
|
|
86
88
|
if (list) list.push(post);
|
|
87
89
|
else years.set(year, [post]);
|
|
@@ -92,7 +94,7 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
|
|
|
92
94
|
<div>
|
|
93
95
|
{options.length ? (
|
|
94
96
|
<div className="sh-tagfilter">
|
|
95
|
-
<ul className="sh-tagchips" aria-label=
|
|
97
|
+
<ul className="sh-tagchips" aria-label={t.tagFilterLabel}>
|
|
96
98
|
{shown.map((option) => (
|
|
97
99
|
<li key={option.tag}>
|
|
98
100
|
<button
|
|
@@ -111,7 +113,9 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
|
|
|
111
113
|
{hidden > 0 || expanded ? (
|
|
112
114
|
<li>
|
|
113
115
|
<button type="button" className="sh-tagmore" aria-expanded={expanded} onClick={() => setExpanded(!expanded)}>
|
|
114
|
-
{
|
|
116
|
+
{siteLang === "en"
|
|
117
|
+
? (expanded ? t.tagCollapse : t.tagExpandAll(options.length))
|
|
118
|
+
: (expanded ? "收起" : `展开全部 ${options.length} 个标签`)}
|
|
115
119
|
</button>
|
|
116
120
|
</li>
|
|
117
121
|
) : null}
|
|
@@ -119,17 +123,17 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
|
|
|
119
123
|
</div>
|
|
120
124
|
) : null}
|
|
121
125
|
<div className="sh-status">
|
|
122
|
-
<span aria-live="polite">{tagSummary(selected.length, filtered.length)}</span>
|
|
123
|
-
{selected.length ? <button type="button" onClick={() => go([])}
|
|
124
|
-
<div className="sh-view" role="group" aria-label=
|
|
125
|
-
<button type="button" aria-pressed={!compact} onClick={() => setCompact(false)}
|
|
126
|
-
<button type="button" aria-pressed={compact} onClick={() => setCompact(true)}
|
|
126
|
+
<span aria-live="polite">{tagSummary(selected.length, filtered.length, siteLang)}</span>
|
|
127
|
+
{selected.length ? <button type="button" onClick={() => go([])}>{siteLang === "en" ? t.tagClear : "清除"}</button> : null}
|
|
128
|
+
<div className="sh-view" role="group" aria-label={t.densityLabel}>
|
|
129
|
+
<button type="button" aria-pressed={!compact} onClick={() => setCompact(false)}>{t.densityDetailed}</button>
|
|
130
|
+
<button type="button" aria-pressed={compact} onClick={() => setCompact(true)}>{t.densityCompact}</button>
|
|
127
131
|
</div>
|
|
128
132
|
</div>
|
|
129
133
|
<div id="sh-list" className={compact ? "is-compact" : undefined}>
|
|
130
134
|
{filtered.length ? [...years.entries()].map(([year, entries]) => (
|
|
131
135
|
<section className="sh-year" id={`y${year}`} key={year} aria-labelledby={`yh${year}`}>
|
|
132
|
-
<h2 id={`yh${year}`}>{year}<span className="n">{entries.length}
|
|
136
|
+
<h2 id={`yh${year}`}>{year}<span className="n">{siteLang === "en" ? t.yearPostCount(entries.length) : <>{entries.length} 篇</>}</span></h2>
|
|
133
137
|
<ol className="sh-rows">
|
|
134
138
|
{entries.map((post) => (
|
|
135
139
|
<li className="sh-row" key={post.path}>
|
|
@@ -137,7 +141,7 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
|
|
|
137
141
|
<div className="archive-copy">
|
|
138
142
|
<h3>
|
|
139
143
|
<Link href={postHref(post)}>{post.title}</Link>
|
|
140
|
-
{post.pinned ? <span className="pin"
|
|
144
|
+
{post.pinned ? <span className="pin">{t.pinnedBadge}</span> : null}
|
|
141
145
|
</h3>
|
|
142
146
|
{post.description ? <p>{post.description}</p> : null}
|
|
143
147
|
</div>
|
|
@@ -148,19 +152,19 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
|
|
|
148
152
|
</section>
|
|
149
153
|
)) : selected.length ? (
|
|
150
154
|
<div className="sh-tagempty">
|
|
151
|
-
<p
|
|
155
|
+
<p>{siteLang === "en" ? t.tagEmptyMatch : "没有同时带这些标签的文章。"}</p>
|
|
152
156
|
<div className="acts">
|
|
153
157
|
{selected.length > 1 ? (
|
|
154
|
-
<button type="button" onClick={() => go(selected.slice(0, -1))}
|
|
158
|
+
<button type="button" onClick={() => go(selected.slice(0, -1))}>{t.tagDropLast(selected[selected.length - 1])}</button>
|
|
155
159
|
) : null}
|
|
156
|
-
<button type="button" onClick={() => go([])}
|
|
160
|
+
<button type="button" onClick={() => go([])}>{t.browseAllPosts}</button>
|
|
157
161
|
</div>
|
|
158
162
|
</div>
|
|
159
163
|
) : <ContentEmptyState kind="posts" />}
|
|
160
164
|
</div>
|
|
161
165
|
</div>
|
|
162
|
-
<aside className="sh-rail" aria-label=
|
|
163
|
-
{years.size ? <h2
|
|
166
|
+
<aside className="sh-rail" aria-label={t.railJumpByYear}>
|
|
167
|
+
{years.size ? <h2>{t.railByYear}</h2> : null}
|
|
164
168
|
<ol>
|
|
165
169
|
{[...years.keys()].map((year) => (
|
|
166
170
|
<li key={year}>
|
|
@@ -170,7 +174,7 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
|
|
|
170
174
|
</ol>
|
|
171
175
|
{series.length ? (
|
|
172
176
|
<>
|
|
173
|
-
<h2
|
|
177
|
+
<h2>{t.railColumns}</h2>
|
|
174
178
|
<ol>
|
|
175
179
|
{series.map((column) => (
|
|
176
180
|
<li key={column.slug}>
|
|
@@ -5,17 +5,16 @@ import { ui } from "../lib/i18n";
|
|
|
5
5
|
|
|
6
6
|
export function Comments({ slug }: { slug: string }) {
|
|
7
7
|
const config = readGiscusConfig();
|
|
8
|
+
const t = ui();
|
|
8
9
|
|
|
9
10
|
return (
|
|
10
11
|
<section className="comments" aria-labelledby={`comments-heading-${slug}`}>
|
|
11
12
|
<div className="comments-heading">
|
|
12
13
|
<ChatCircle size={23} />
|
|
13
14
|
<div>
|
|
14
|
-
<h2 id={`comments-heading-${slug}`}>{
|
|
15
|
+
<h2 id={`comments-heading-${slug}`}>{t.comments}</h2>
|
|
15
16
|
<p>
|
|
16
|
-
{config
|
|
17
|
-
? "评论由 GitHub Discussions 保存和管理。"
|
|
18
|
-
: "评论还没开放。配好下面四项后,这里才会出现评论框。"}
|
|
17
|
+
{config ? t.commentsSummaryReady : t.commentsSummaryUnconfigured}
|
|
19
18
|
</p>
|
|
20
19
|
</div>
|
|
21
20
|
</div>
|
|
@@ -26,14 +25,21 @@ export function Comments({ slug }: { slug: string }) {
|
|
|
26
25
|
<div className="integration-notice">
|
|
27
26
|
<GithubLogo size={20} weight="fill" />
|
|
28
27
|
<p>
|
|
29
|
-
|
|
30
|
-
<a href=
|
|
28
|
+
{t.commentsSetupBefore}{" "}
|
|
29
|
+
<a href={t.commentsGiscusGuideUrl} target="_blank" rel="noreferrer">
|
|
31
30
|
Giscus
|
|
32
31
|
</a>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<code>
|
|
32
|
+
{t.commentsSetupMiddle}
|
|
33
|
+
<code>.env.local</code>
|
|
34
|
+
{t.commentsSetupAnd}
|
|
35
|
+
<code>GISCUS_REPO</code>
|
|
36
|
+
{t.commentsSetupJoin}
|
|
37
|
+
<code>GISCUS_REPO_ID</code>
|
|
38
|
+
{t.commentsSetupJoin}
|
|
39
|
+
<code>GISCUS_CATEGORY</code>
|
|
40
|
+
{t.commentsSetupJoin}
|
|
41
|
+
<code>GISCUS_CATEGORY_ID</code>
|
|
42
|
+
{t.commentsSetupEnd}
|
|
37
43
|
</p>
|
|
38
44
|
</div>
|
|
39
45
|
)}
|
|
@@ -3,35 +3,36 @@
|
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { ArrowRight, Article, Books, Quotes } from "@phosphor-icons/react/ssr";
|
|
5
5
|
import { siteHref } from "../lib/site-href";
|
|
6
|
+
import { ui } from "../lib/i18n";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
8
|
+
export function ContentEmptyState({ kind }: { kind: "posts" | "thoughts" | "columns" }) {
|
|
9
|
+
const s = ui();
|
|
10
|
+
const emptyContent = {
|
|
11
|
+
posts: {
|
|
12
|
+
icon: Article,
|
|
13
|
+
eyebrow: s.emptyPostsEyebrow,
|
|
14
|
+
title: s.emptyPostsTitle,
|
|
15
|
+
description: s.emptyPostsDesc,
|
|
16
|
+
actionHref: "/",
|
|
17
|
+
actionLabel: s.emptyPostsCta,
|
|
18
|
+
},
|
|
19
|
+
thoughts: {
|
|
20
|
+
icon: Quotes,
|
|
21
|
+
eyebrow: s.emptyThoughtsEyebrow,
|
|
22
|
+
title: s.emptyThoughtsTitle,
|
|
23
|
+
description: s.emptyThoughtsDesc,
|
|
24
|
+
actionHref: "/blog",
|
|
25
|
+
actionLabel: s.emptyThoughtsCta,
|
|
26
|
+
},
|
|
27
|
+
columns: {
|
|
28
|
+
icon: Books,
|
|
29
|
+
eyebrow: s.emptyColumnsEyebrow,
|
|
30
|
+
title: s.emptyColumnsTitle,
|
|
31
|
+
description: s.emptyColumnsDesc,
|
|
32
|
+
actionHref: "/blog",
|
|
33
|
+
actionLabel: s.emptyColumnsCta,
|
|
34
|
+
},
|
|
35
|
+
} as const;
|
|
35
36
|
const content = emptyContent[kind];
|
|
36
37
|
const Icon = content.icon;
|
|
37
38
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
YoutubeLogo,
|
|
8
8
|
} from "@phosphor-icons/react/ssr";
|
|
9
9
|
import { parse } from "yaml";
|
|
10
|
+
import { ui, type UiLang } from "../lib/i18n";
|
|
10
11
|
|
|
11
12
|
type EmbedData = {
|
|
12
13
|
url: string;
|
|
@@ -70,23 +71,23 @@ function playableUrl(provider: EmbedProvider, url: URL) {
|
|
|
70
71
|
return null;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
bilibili: { label: "Bilibili", Icon: VideoCamera },
|
|
76
|
-
x: { label: "X", Icon: XLogo },
|
|
77
|
-
wechat: { label: "微信公众号", Icon: WechatLogo },
|
|
78
|
-
website: { label: "网页摘要", Icon: LinkSimple },
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export function ExternalEmbed({ source }: { source: string }) {
|
|
74
|
+
export function ExternalEmbed({ source, lang }: { source: string; lang?: UiLang }) {
|
|
75
|
+
const s = ui(lang);
|
|
82
76
|
const data = readEmbedData(source);
|
|
83
77
|
if (!data) {
|
|
84
|
-
return <p className="external-embed-error"
|
|
78
|
+
return <p className="external-embed-error">{s.embedParseError}</p>;
|
|
85
79
|
}
|
|
86
80
|
|
|
87
81
|
const url = new URL(data.url);
|
|
88
82
|
const provider = providerFor(url);
|
|
89
83
|
const playerUrl = playableUrl(provider, url);
|
|
84
|
+
const providerCopy = {
|
|
85
|
+
youtube: { label: "YouTube", Icon: YoutubeLogo },
|
|
86
|
+
bilibili: { label: "Bilibili", Icon: VideoCamera },
|
|
87
|
+
x: { label: "X", Icon: XLogo },
|
|
88
|
+
wechat: { label: s.embedProviderWechat, Icon: WechatLogo },
|
|
89
|
+
website: { label: s.embedProviderWeb, Icon: LinkSimple },
|
|
90
|
+
};
|
|
90
91
|
const { Icon, label } = providerCopy[provider];
|
|
91
92
|
|
|
92
93
|
if (playerUrl) {
|
|
@@ -95,7 +96,7 @@ export function ExternalEmbed({ source }: { source: string }) {
|
|
|
95
96
|
<div className="external-embed-frame">
|
|
96
97
|
<iframe
|
|
97
98
|
src={playerUrl}
|
|
98
|
-
title={data.title ??
|
|
99
|
+
title={data.title ?? s.embedFrameTitle(label)}
|
|
99
100
|
loading="lazy"
|
|
100
101
|
allow="accelerometer; autoplay; encrypted-media; fullscreen; picture-in-picture"
|
|
101
102
|
referrerPolicy="strict-origin-when-cross-origin"
|
|
@@ -104,7 +105,7 @@ export function ExternalEmbed({ source }: { source: string }) {
|
|
|
104
105
|
</div>
|
|
105
106
|
<figcaption>
|
|
106
107
|
<span><Icon size={17} />{label}</span>
|
|
107
|
-
<a href={data.url} target="_blank" rel="noopener noreferrer"
|
|
108
|
+
<a href={data.url} target="_blank" rel="noopener noreferrer">{s.embedOpenOriginal}<ArrowUpRight size={14} /></a>
|
|
108
109
|
</figcaption>
|
|
109
110
|
</figure>
|
|
110
111
|
);
|
|
@@ -118,8 +119,8 @@ export function ExternalEmbed({ source }: { source: string }) {
|
|
|
118
119
|
)}
|
|
119
120
|
<span className="external-embed-copy">
|
|
120
121
|
<small><Icon size={16} />{label} · {url.hostname}</small>
|
|
121
|
-
<strong>{data.title ??
|
|
122
|
-
<p>{data.description ?? (provider === "x" || provider === "wechat" ?
|
|
122
|
+
<strong>{data.title ?? s.embedFallbackTitle}</strong>
|
|
123
|
+
<p>{data.description ?? (provider === "x" || provider === "wechat" ? s.embedFallbackNoScript : s.embedFallbackWeb)}</p>
|
|
123
124
|
</span>
|
|
124
125
|
<ArrowUpRight size={21} />
|
|
125
126
|
</a>
|
|
@@ -32,7 +32,15 @@ cover: writing/first-post.jpg
|
|
|
32
32
|
|
|
33
33
|
## 一个小标题
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
可以继续插入图片、代码、公式、Mermaid 图表,或用 \`garden\` 围栏写组件:
|
|
36
|
+
|
|
37
|
+
\`\`\`garden list
|
|
38
|
+
items:
|
|
39
|
+
- icon: idea
|
|
40
|
+
text: 记下为什么重新开始写
|
|
41
|
+
- icon: book
|
|
42
|
+
text: 先把旧笔记整理成目录
|
|
43
|
+
\`\`\``;
|
|
36
44
|
|
|
37
45
|
const thoughtSource = `---
|
|
38
46
|
title: 雨后的散步
|
|
@@ -53,7 +61,7 @@ const columnSource = `columns:
|
|
|
53
61
|
- slug: writing-system
|
|
54
62
|
title: 写作系统
|
|
55
63
|
description: 记录从选题到发布的完整写作过程。
|
|
56
|
-
status:
|
|
64
|
+
status: ongoing
|
|
57
65
|
cover: /images/columns/writing-system.webp
|
|
58
66
|
coverAlt: 桌面上的笔记本与钢笔
|
|
59
67
|
posts:
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
HANDBOOK_SECTIONS,
|
|
10
10
|
HANDBOOK_UI,
|
|
11
11
|
NAV_SHORT_TITLE,
|
|
12
|
+
REFERENCE_SECTION,
|
|
13
|
+
isReferencePath,
|
|
12
14
|
handbookPath,
|
|
13
15
|
sectionTitle,
|
|
14
16
|
type GardenDocNavItem,
|
|
@@ -152,7 +154,7 @@ export function GardenDocsNav({
|
|
|
152
154
|
</div>
|
|
153
155
|
|
|
154
156
|
<nav className="gdoc-nav" aria-label={t.chapters}>
|
|
155
|
-
{groups.map((group) => (
|
|
157
|
+
{(isReferencePath(currentPath) ? [...groups].sort((a, b) => Number(b.id === REFERENCE_SECTION) - Number(a.id === REFERENCE_SECTION)) : groups).map((group) => (
|
|
156
158
|
<div key={group.id} className="gdoc-nav-group">
|
|
157
159
|
<h2 className="gdoc-nav-group-head">{sectionTitle(HANDBOOK_SECTIONS.find((sec) => sec.id === group.id), lang) ?? group.title}</h2>
|
|
158
160
|
<ol className="gdoc-nav-list">
|
|
@@ -170,7 +172,7 @@ export function GardenDocsNav({
|
|
|
170
172
|
{badge ? (
|
|
171
173
|
<>
|
|
172
174
|
<code className="gdoc-comp-pill">{badge}</code>
|
|
173
|
-
<span className="gdoc-comp-label">{label}</span>
|
|
175
|
+
{group.id === REFERENCE_SECTION ? null : <span className="gdoc-comp-label">{label}</span>}
|
|
174
176
|
</>
|
|
175
177
|
) : (
|
|
176
178
|
<span>{label}</span>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
4
|
|
|
5
5
|
import type { GiscusConfig } from "../lib/giscus-config";
|
|
6
|
+
import { ui } from "../lib/i18n";
|
|
6
7
|
|
|
7
8
|
export type { GiscusConfig };
|
|
8
9
|
|
|
@@ -38,7 +39,7 @@ export function GiscusComments({
|
|
|
38
39
|
script.dataset.emitMetadata = "0";
|
|
39
40
|
script.dataset.inputPosition = "bottom";
|
|
40
41
|
script.dataset.theme = currentGiscusTheme();
|
|
41
|
-
script.dataset.lang =
|
|
42
|
+
script.dataset.lang = ui().giscusLang;
|
|
42
43
|
script.dataset.loading = "lazy";
|
|
43
44
|
container.append(script);
|
|
44
45
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
MapPin,
|
|
9
9
|
Users,
|
|
10
10
|
} from "@phosphor-icons/react";
|
|
11
|
+
import { siteLang, ui } from "../lib/i18n";
|
|
11
12
|
|
|
12
13
|
export type GithubPublicProfile = {
|
|
13
14
|
avatar_url: string;
|
|
@@ -70,28 +71,29 @@ export function GithubLiveOverview({
|
|
|
70
71
|
return () => controller.abort();
|
|
71
72
|
}, [username]);
|
|
72
73
|
|
|
74
|
+
const t = ui();
|
|
73
75
|
const displayName = visibleGithubText(profile.name ?? profile.login) || profile.login;
|
|
74
76
|
const profileBio = profile.bio?.trim();
|
|
75
77
|
const signature = statusMessage?.trim() || profileBio;
|
|
76
78
|
const syncLabel = syncState === "synced"
|
|
77
|
-
? "本次访问已同步"
|
|
79
|
+
? (siteLang === "en" ? t.githubSyncSynced : "本次访问已同步")
|
|
78
80
|
: syncState === "snapshot"
|
|
79
|
-
?
|
|
80
|
-
:
|
|
81
|
+
? t.githubSyncSnapshot
|
|
82
|
+
: t.githubSyncSyncing;
|
|
81
83
|
|
|
82
84
|
return (
|
|
83
85
|
<>
|
|
84
86
|
<header className="github-overview">
|
|
85
87
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
86
|
-
<img src={profile.avatar_url} alt={
|
|
88
|
+
<img src={profile.avatar_url} alt={t.githubAvatarAlt(profile.login)} />
|
|
87
89
|
<div className="github-identity">
|
|
88
|
-
<span className="eyebrow" aria-live="polite"><GithubLogo size={17} />
|
|
90
|
+
<span className="eyebrow" aria-live="polite"><GithubLogo size={17} />{` ${t.githubOverviewPrefix} · `}{syncLabel}</span>
|
|
89
91
|
<h2>{displayName}</h2>
|
|
90
92
|
{signature && <p>{signature}</p>}
|
|
91
93
|
{profile.location && <span className="github-location"><MapPin size={14} />{profile.location}</span>}
|
|
92
94
|
</div>
|
|
93
95
|
<a href={profile.html_url} target="_blank" rel="noopener noreferrer">
|
|
94
|
-
|
|
96
|
+
{t.githubViewProfile}
|
|
95
97
|
<ArrowUpRight size={16} />
|
|
96
98
|
</a>
|
|
97
99
|
</header>
|
|
@@ -99,7 +101,7 @@ export function GithubLiveOverview({
|
|
|
99
101
|
<dl className="github-metrics">
|
|
100
102
|
<div>
|
|
101
103
|
<GithubLogo size={21} aria-hidden="true" />
|
|
102
|
-
<dt
|
|
104
|
+
<dt>{t.githubMetricRepos}</dt>
|
|
103
105
|
<dd>{syncState === "synced" || initialLive ? profile.public_repos : "—"}</dd>
|
|
104
106
|
</div>
|
|
105
107
|
<div>
|
|
@@ -109,7 +111,7 @@ export function GithubLiveOverview({
|
|
|
109
111
|
</div>
|
|
110
112
|
<div>
|
|
111
113
|
<GitCommit size={21} aria-hidden="true" />
|
|
112
|
-
<dt
|
|
114
|
+
<dt>{t.githubMetricCommits}</dt>
|
|
113
115
|
<dd>{recentCommits ?? "—"}</dd>
|
|
114
116
|
</div>
|
|
115
117
|
</dl>
|
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
Star,
|
|
5
5
|
} from "@phosphor-icons/react/ssr";
|
|
6
6
|
import { githubUsername } from "../lib/site";
|
|
7
|
+
import { hasRealGithubId } from "../lib/github-id.mjs";
|
|
7
8
|
import { siteConfig } from "../site.config";
|
|
9
|
+
import { ui } from "../lib/i18n";
|
|
8
10
|
import { GithubLiveOverview, type GithubPublicProfile } from "./GithubLiveOverview";
|
|
9
11
|
|
|
10
12
|
type GithubRepository = {
|
|
@@ -93,26 +95,31 @@ async function loadGithubUser(token?: string) {
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
function activityFromEvent(event: GithubEvent): GithubActivity | null {
|
|
98
|
+
const t = ui();
|
|
96
99
|
const repository = event.repo.name;
|
|
97
100
|
const repositoryUrl = `https://github.com/${repository}`;
|
|
98
101
|
|
|
99
102
|
if (event.type === "PushEvent") {
|
|
100
|
-
return { id: event.id, label:
|
|
103
|
+
return { id: event.id, label: t.githubEventPush, repository, date: event.created_at, url: `${repositoryUrl}/commits` };
|
|
101
104
|
}
|
|
102
105
|
if (event.type === "PullRequestEvent") {
|
|
103
|
-
const action = event.payload.action === "closed" ?
|
|
106
|
+
const action = event.payload.action === "closed" ? t.githubEventPrClosed : t.githubEventPrOpen;
|
|
104
107
|
return { id: event.id, label: action, repository, date: event.created_at, url: event.payload.pull_request?.html_url ?? repositoryUrl };
|
|
105
108
|
}
|
|
106
109
|
if (event.type === "IssuesEvent") {
|
|
107
|
-
const action = event.payload.action === "closed" ?
|
|
110
|
+
const action = event.payload.action === "closed" ? t.githubEventIssueClosed : t.githubEventIssueOpen;
|
|
108
111
|
return { id: event.id, label: action, repository, date: event.created_at, url: event.payload.issue?.html_url ?? repositoryUrl };
|
|
109
112
|
}
|
|
110
113
|
if (event.type === "CreateEvent") {
|
|
111
|
-
const target = event.payload.ref_type === "repository"
|
|
114
|
+
const target = event.payload.ref_type === "repository"
|
|
115
|
+
? t.githubEventCreateRepo
|
|
116
|
+
: event.payload.ref_type === "tag"
|
|
117
|
+
? t.githubEventCreateTag
|
|
118
|
+
: t.githubEventCreateBranch;
|
|
112
119
|
return { id: event.id, label: target, repository, date: event.created_at, url: repositoryUrl };
|
|
113
120
|
}
|
|
114
121
|
if (event.type === "ReleaseEvent") {
|
|
115
|
-
return { id: event.id, label:
|
|
122
|
+
return { id: event.id, label: t.githubEventRelease, repository, date: event.created_at, url: event.payload.release?.html_url ?? `${repositoryUrl}/releases` };
|
|
116
123
|
}
|
|
117
124
|
return null;
|
|
118
125
|
}
|
|
@@ -220,7 +227,7 @@ async function loadGithubGraph(token?: string) {
|
|
|
220
227
|
}
|
|
221
228
|
|
|
222
229
|
function formatActivityDate(value: string) {
|
|
223
|
-
return new Intl.DateTimeFormat(
|
|
230
|
+
return new Intl.DateTimeFormat(ui().dateLocale, { month: "2-digit", day: "2-digit" }).format(new Date(value));
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
function contributionLevelClass(level: string) {
|
|
@@ -228,6 +235,9 @@ function contributionLevelClass(level: string) {
|
|
|
228
235
|
}
|
|
229
236
|
|
|
230
237
|
export async function GithubProfile() {
|
|
238
|
+
// A new site still has the template id (or none): render the page without a profile and never call the API.
|
|
239
|
+
if (!hasRealGithubId(githubUsername)) return null;
|
|
240
|
+
const t = ui();
|
|
231
241
|
const token = process.env.GITHUB_TOKEN;
|
|
232
242
|
const [{ profile, live }, graph, activity] = await Promise.all([
|
|
233
243
|
loadGithubUser(token),
|
|
@@ -240,7 +250,7 @@ export async function GithubProfile() {
|
|
|
240
250
|
const calendarDays = graph?.year.contributionCalendar.weeks.flatMap((week) => week.contributionDays) ?? [];
|
|
241
251
|
|
|
242
252
|
return (
|
|
243
|
-
<section className="github-profile" aria-label=
|
|
253
|
+
<section className="github-profile" aria-label={t.githubProfileAria}>
|
|
244
254
|
<GithubLiveOverview
|
|
245
255
|
username={githubUsername}
|
|
246
256
|
initialProfile={profile}
|
|
@@ -252,8 +262,8 @@ export async function GithubProfile() {
|
|
|
252
262
|
<section className="github-pinned" aria-labelledby="github-pinned-title">
|
|
253
263
|
<header>
|
|
254
264
|
<div>
|
|
255
|
-
<p className="eyebrow">
|
|
256
|
-
<h3 id="github-pinned-title"
|
|
265
|
+
<p className="eyebrow">{t.githubPinnedEyebrow}</p>
|
|
266
|
+
<h3 id="github-pinned-title">{t.githubPinnedHeading}</h3>
|
|
257
267
|
</div>
|
|
258
268
|
<PushPin size={20} aria-hidden="true" />
|
|
259
269
|
</header>
|
|
@@ -262,7 +272,7 @@ export async function GithubProfile() {
|
|
|
262
272
|
{pinned.map((repository) => (
|
|
263
273
|
<a href={repository.url} target="_blank" rel="noopener noreferrer" key={repository.nameWithOwner}>
|
|
264
274
|
<strong>{repository.nameWithOwner.split("/").at(-1)}</strong>
|
|
265
|
-
<p>{repository.description ??
|
|
275
|
+
<p>{repository.description ?? t.githubPinnedFallbackDesc}</p>
|
|
266
276
|
<span>
|
|
267
277
|
{repository.primaryLanguage && <><i style={{ background: repository.primaryLanguage.color ?? undefined }} />{repository.primaryLanguage.name}</>}
|
|
268
278
|
<small><Star size={13} weight="fill" />{repository.stargazerCount}</small>
|
|
@@ -271,38 +281,38 @@ export async function GithubProfile() {
|
|
|
271
281
|
))}
|
|
272
282
|
</div>
|
|
273
283
|
) : (
|
|
274
|
-
<p className="github-data-note"
|
|
284
|
+
<p className="github-data-note">{t.githubPinnedEmpty}</p>
|
|
275
285
|
)}
|
|
276
286
|
</section>
|
|
277
287
|
|
|
278
288
|
<section className="github-contributions" aria-labelledby="github-contributions-title">
|
|
279
289
|
<header>
|
|
280
290
|
<div>
|
|
281
|
-
<p className="eyebrow">
|
|
282
|
-
<h3 id="github-contributions-title"
|
|
291
|
+
<p className="eyebrow">{t.githubContribEyebrow}</p>
|
|
292
|
+
<h3 id="github-contributions-title">{t.githubContribHeading}</h3>
|
|
283
293
|
</div>
|
|
284
|
-
<strong>{graph ?
|
|
294
|
+
<strong>{graph ? t.githubContribCount(graph.year.contributionCalendar.totalContributions) : t.githubContribNeedToken}</strong>
|
|
285
295
|
</header>
|
|
286
296
|
{calendarDays.length > 0 ? (
|
|
287
|
-
<div className="github-calendar" role="img" aria-label={
|
|
297
|
+
<div className="github-calendar" role="img" aria-label={t.githubContribAria(graph?.year.contributionCalendar.totalContributions ?? 0)}>
|
|
288
298
|
{calendarDays.map((day) => (
|
|
289
299
|
<span
|
|
290
300
|
className={`github-contribution-day level-${contributionLevelClass(day.contributionLevel)}`}
|
|
291
301
|
key={day.date}
|
|
292
|
-
title={
|
|
293
|
-
aria-label={
|
|
302
|
+
title={t.githubContribDayTitle(day.date, day.contributionCount)}
|
|
303
|
+
aria-label={t.githubContribDayAria(day.date, day.contributionCount)}
|
|
294
304
|
/>
|
|
295
305
|
))}
|
|
296
306
|
</div>
|
|
297
307
|
) : (
|
|
298
|
-
<p className="github-data-note"
|
|
308
|
+
<p className="github-data-note">{t.githubContribEmpty}</p>
|
|
299
309
|
)}
|
|
300
310
|
</section>
|
|
301
311
|
|
|
302
312
|
<section className="github-activity" aria-labelledby="github-activity-title">
|
|
303
313
|
<header>
|
|
304
|
-
<p className="eyebrow">
|
|
305
|
-
<h3 id="github-activity-title"
|
|
314
|
+
<p className="eyebrow">{t.githubActivityEyebrow}</p>
|
|
315
|
+
<h3 id="github-activity-title">{t.githubActivityHeading}</h3>
|
|
306
316
|
</header>
|
|
307
317
|
{activity.length > 0 ? (
|
|
308
318
|
<ol>
|
|
@@ -318,7 +328,7 @@ export async function GithubProfile() {
|
|
|
318
328
|
))}
|
|
319
329
|
</ol>
|
|
320
330
|
) : (
|
|
321
|
-
<p className="github-data-note">
|
|
331
|
+
<p className="github-data-note">{t.githubActivityEmpty}</p>
|
|
322
332
|
)}
|
|
323
333
|
</section>
|
|
324
334
|
</section>
|
|
@@ -5,6 +5,8 @@ import { usePathname } from "next/navigation";
|
|
|
5
5
|
import { HANDBOOK_LANG_KEY, handbookBase, type HandbookLang } from "../lib/garden-docs-meta";
|
|
6
6
|
import { siteHref } from "../lib/site-href";
|
|
7
7
|
import { swapHandbookLang } from "../lib/handbook-path";
|
|
8
|
+
import { siteLang } from "../lib/i18n";
|
|
9
|
+
import { handbookPageLang, handbookSwitchLabel, handbookSwitchTarget, handbookSwitchTitle } from "../lib/handbook-switch.mjs";
|
|
8
10
|
|
|
9
11
|
const noSubscribe = () => () => {};
|
|
10
12
|
|
|
@@ -12,14 +14,16 @@ const noSubscribe = () => () => {};
|
|
|
12
14
|
function counterpartHref(target: HandbookLang) {
|
|
13
15
|
const link = document.querySelector<HTMLLinkElement>(`link[rel="alternate"][hreflang="${target === "en" ? "en" : "zh-CN"}"]`);
|
|
14
16
|
if (link?.href) return swapHandbookLang(location.pathname, target);
|
|
17
|
+
// English sites leave hreflang out of <head>; the handbook ships every page in both languages there.
|
|
18
|
+
if (siteLang === "en") return swapHandbookLang(location.pathname, target);
|
|
15
19
|
return target === "zh" ? siteHref(`${handbookBase("zh")}/`) : null;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/** 「中 / EN」 in the top bar on handbook pages. Goes to the counterpart page. */
|
|
19
23
|
export function HandbookLangSwitch() {
|
|
20
24
|
const pathname = usePathname() ?? "";
|
|
21
|
-
const lang: HandbookLang =
|
|
22
|
-
const target: HandbookLang = lang
|
|
25
|
+
const lang: HandbookLang = handbookPageLang(pathname);
|
|
26
|
+
const target: HandbookLang = handbookSwitchTarget(lang);
|
|
23
27
|
const href = useSyncExternalStore(
|
|
24
28
|
noSubscribe,
|
|
25
29
|
() => counterpartHref(target),
|
|
@@ -39,10 +43,10 @@ export function HandbookLangSwitch() {
|
|
|
39
43
|
</>
|
|
40
44
|
);
|
|
41
45
|
if (!href) {
|
|
42
|
-
return <span className="sh-lang is-disabled" title=
|
|
46
|
+
return <span className="sh-lang is-disabled" title={handbookSwitchTitle(lang, siteLang)}>{label}</span>;
|
|
43
47
|
}
|
|
44
48
|
return (
|
|
45
|
-
<a className="sh-lang" href={href} hrefLang={target === "en" ? "en" : "zh-CN"} lang={target === "en" ? "en" : undefined} onClick={remember} aria-label={
|
|
49
|
+
<a className="sh-lang" href={href} hrefLang={target === "en" ? "en" : "zh-CN"} lang={target === "en" ? "en" : undefined} onClick={remember} aria-label={handbookSwitchLabel(lang)}>
|
|
46
50
|
{label}
|
|
47
51
|
</a>
|
|
48
52
|
);
|