hd-garden 0.7.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 +26 -19
- package/.env.en.example +67 -0
- package/README.en.md +57 -0
- package/README.md +31 -46
- 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 +14 -5
- package/app/blog/page.tsx +4 -2
- package/app/columns/[column]/[...path]/page.tsx +17 -18
- package/app/columns/page.tsx +12 -8
- package/app/components/AdminUiShell.tsx +4 -3
- package/app/components/ArticlePager.tsx +32 -0
- 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 +17 -10
- package/app/components/ContentEmptyState.tsx +29 -28
- package/app/components/ExternalEmbed.tsx +15 -14
- package/app/components/FeedMore.tsx +70 -0
- package/app/components/GardenContentGuide.tsx +10 -2
- package/app/components/GardenDocsNav.tsx +29 -22
- package/app/components/GiscusComments.tsx +2 -1
- package/app/components/GithubLiveOverview.tsx +10 -8
- package/app/components/GithubProfile.tsx +31 -21
- package/app/components/HandbookLangRedirect.tsx +30 -0
- package/app/components/HandbookLangSwitch.tsx +53 -0
- package/app/components/HandbookView.tsx +270 -0
- package/app/components/MarkdownArticle.tsx +39 -9
- package/app/components/MarkdownImage.tsx +6 -4
- package/app/components/SiteChrome.tsx +11 -2
- package/app/components/SiteFooter.tsx +6 -4
- package/app/components/SiteHeader.tsx +38 -25
- package/app/components/TableOfContents.tsx +15 -9
- package/app/components/ThoughtCard.tsx +16 -14
- package/app/components/ThoughtFeed.tsx +41 -0
- package/app/components/UiBlock.tsx +3 -2
- package/app/components/UiLang.tsx +18 -0
- package/app/components/Webmentions.tsx +13 -12
- package/app/components/WidgetChart.tsx +8 -6
- package/app/components/garden/FilePreview.tsx +14 -12
- package/app/components/garden/GardenBlock.tsx +10 -1
- package/app/components/garden/GardenFile.tsx +10 -8
- package/app/components/garden/RefDemo.tsx +62 -0
- package/app/components/garden/blocks.tsx +90 -32
- package/app/components/garden/chrome.tsx +20 -13
- package/app/components/garden/patterns.tsx +497 -0
- package/app/garden/[...slug]/page.tsx +11 -130
- package/app/garden/en/[...slug]/page.tsx +28 -0
- package/app/garden/en/page.tsx +16 -0
- package/app/garden/page.tsx +8 -74
- package/app/garden-modules.d.ts +3 -0
- package/app/globals.css +10 -0
- package/app/layout.tsx +2 -4
- package/app/lib/a2-meta.ts +26 -3
- package/app/lib/content.ts +13 -3
- package/app/lib/garden-docs-meta.ts +84 -5
- package/app/lib/github-id.mjs +8 -0
- package/app/lib/handbook-path.ts +11 -0
- package/app/lib/handbook-switch.mjs +12 -0
- package/app/lib/i18n.ts +621 -0
- package/app/lib/page-alternates.ts +2 -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 +34 -4
- package/app/page.tsx +17 -14
- package/app/rss.xml/route.ts +2 -1
- package/app/site.config.ts +1 -1
- package/app/styles/a2.css +439 -32
- package/app/styles/shell.css +21 -2
- package/app/tags/page.tsx +7 -5
- package/app/thoughts/[slug]/page.tsx +2 -1
- package/app/thoughts/page/[n]/page.tsx +44 -0
- package/app/thoughts/page.tsx +4 -7
- package/app/ui-parity/page.tsx +38 -22
- package/bin/garden.mjs +133 -19
- package/docs/handbook/ai-writing.md +99 -0
- package/docs/handbook/assets/reading-layout-en.svg +26 -0
- package/docs/handbook/components/card.md +43 -20
- package/docs/handbook/components/chart.md +10 -24
- package/docs/handbook/components/choice.md +42 -0
- package/docs/handbook/components/compare.md +34 -21
- package/docs/handbook/components/figure.md +33 -44
- package/docs/handbook/components/file.md +10 -13
- package/docs/handbook/components/html.md +10 -28
- package/docs/handbook/components/interactive.md +9 -33
- package/docs/handbook/components/list.md +65 -0
- package/docs/handbook/components/relations.md +95 -0
- package/docs/handbook/components/stat.md +10 -26
- package/docs/handbook/components/timeline.md +13 -26
- package/docs/handbook/components.md +210 -19
- package/docs/handbook/config.md +7 -5
- package/docs/handbook/deploy.md +2 -2
- package/docs/handbook/design.md +23 -15
- package/docs/handbook/en/ai-writing.md +99 -0
- package/docs/handbook/en/components/card.md +67 -0
- package/docs/handbook/en/components/chart.md +38 -0
- package/docs/handbook/en/components/choice.md +42 -0
- package/docs/handbook/en/components/compare.md +53 -0
- package/docs/handbook/en/components/figure.md +59 -0
- package/docs/handbook/en/components/file/checklist.md +17 -0
- package/docs/handbook/en/components/file.md +29 -0
- package/docs/handbook/en/components/html.md +49 -0
- package/docs/handbook/en/components/interactive.md +41 -0
- 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 +39 -0
- package/docs/handbook/en/components/timeline.md +37 -0
- package/docs/handbook/en/components.md +222 -0
- package/docs/handbook/en/config.md +127 -0
- package/docs/handbook/en/deploy.md +273 -0
- package/docs/handbook/en/design.md +88 -0
- package/docs/handbook/en/index.md +192 -0
- 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 +92 -0
- package/docs/handbook/en/workflow.md +82 -0
- package/docs/handbook/en/writing.md +157 -0
- package/docs/handbook/index.md +99 -4
- 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 +21 -12
- 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 +57 -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 +339 -11
- package/scripts/html-lang.mjs +36 -0
- 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/scripts/static-404.mjs +25 -0
- package/scripts/ui-cli.mjs +14 -8
- 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/site/site.config.yaml +2 -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/core/apply.mjs +22 -4
- package/ui/core/cli-locale.mjs +67 -0
- package/ui/core/fence.mjs +18 -3
- package/ui/core/fix-prompt.mjs +18 -15
- package/ui/core/garden-fence.mjs +64 -0
- package/ui/core/scan.mjs +19 -3
- package/ui/garden/adjacent.mjs +21 -0
- package/ui/garden/catalog.mjs +205 -4
- package/ui/garden/check.mjs +12 -0
- package/ui/garden/file-block.mjs +8 -7
- package/ui/garden/html-lang.mjs +88 -0
- package/ui/garden/markdown-links.mjs +7 -4
- package/ui/garden/paging.mjs +30 -0
- 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.css +9 -9
- package/ui/preview/dist/garden-ui.esm.js +206 -169
- package/ui/preview/dist/garden-ui.iife.js +206 -169
- package/ui/prompt/rules.md +28 -1
- package/ui/react/components/chart.tsx +8 -5
- package/ui/react/components/display.tsx +7 -4
- package/ui/react/components/inputs.tsx +6 -3
- package/ui/react/components/layout.tsx +6 -4
- package/ui/react/components/media.tsx +7 -4
- package/ui/react/components/sandbox.tsx +3 -1
- package/ui/react/index.tsx +11 -7
- package/ui/react/lang.tsx +89 -0
- package/ui/react/parity.tsx +8 -6
- package/ui/react/runtime.tsx +3 -1
- package/ui/react/ui.css +9 -9
- package/worker/index.ts +14 -1
|
@@ -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:
|
|
@@ -6,18 +6,28 @@ import { BookOpenText, CaretRight, List, X } from "@phosphor-icons/react";
|
|
|
6
6
|
import {
|
|
7
7
|
COMPONENT_BADGE,
|
|
8
8
|
groupHandbookDocs,
|
|
9
|
+
HANDBOOK_SECTIONS,
|
|
10
|
+
HANDBOOK_UI,
|
|
9
11
|
NAV_SHORT_TITLE,
|
|
12
|
+
REFERENCE_SECTION,
|
|
13
|
+
isReferencePath,
|
|
14
|
+
handbookPath,
|
|
15
|
+
sectionTitle,
|
|
10
16
|
type GardenDocNavItem,
|
|
17
|
+
type HandbookLang,
|
|
11
18
|
} from "../lib/garden-docs-meta";
|
|
12
19
|
import { siteHref } from "../lib/site-href";
|
|
13
20
|
|
|
14
21
|
export function GardenDocsNav({
|
|
15
22
|
docs,
|
|
16
23
|
currentPath = "",
|
|
24
|
+
lang = "zh",
|
|
17
25
|
}: {
|
|
18
26
|
docs: GardenDocNavItem[];
|
|
19
27
|
currentPath?: string;
|
|
28
|
+
lang?: HandbookLang;
|
|
20
29
|
}) {
|
|
30
|
+
const t = HANDBOOK_UI[lang];
|
|
21
31
|
const groups = groupHandbookDocs(docs);
|
|
22
32
|
const currentDoc = docs.find((doc) => doc.path === currentPath);
|
|
23
33
|
const currentSection = groups.find((group) =>
|
|
@@ -69,7 +79,7 @@ export function GardenDocsNav({
|
|
|
69
79
|
|
|
70
80
|
return (
|
|
71
81
|
<>
|
|
72
|
-
<div className="gdoc-mobile-bar" aria-label=
|
|
82
|
+
<div className="gdoc-mobile-bar" aria-label={t.mobileBar}>
|
|
73
83
|
<button
|
|
74
84
|
ref={triggerRef}
|
|
75
85
|
type="button"
|
|
@@ -79,28 +89,28 @@ export function GardenDocsNav({
|
|
|
79
89
|
onClick={() => setMobileOpen(true)}
|
|
80
90
|
>
|
|
81
91
|
<List size={16} weight="bold" aria-hidden="true" />
|
|
82
|
-
<span
|
|
92
|
+
<span>{t.sidebarShort}</span>
|
|
83
93
|
</button>
|
|
84
94
|
<a
|
|
85
95
|
className="gdoc-nojs-trigger"
|
|
86
96
|
id={`${drawerId}-open`}
|
|
87
97
|
href={`#${drawerId}`}
|
|
88
|
-
aria-label=
|
|
98
|
+
aria-label={t.openSidebar}
|
|
89
99
|
>
|
|
90
100
|
<List size={16} weight="bold" aria-hidden="true" />
|
|
91
|
-
<span
|
|
101
|
+
<span>{t.sidebarShort}</span>
|
|
92
102
|
</a>
|
|
93
103
|
<div className="gdoc-mobile-crumb" aria-hidden="true">
|
|
94
|
-
<span>{currentSection
|
|
104
|
+
<span>{sectionTitle(currentSection, lang) ?? t.name}</span>
|
|
95
105
|
<CaretRight size={12} />
|
|
96
|
-
<strong>{currentDoc ? (NAV_SHORT_TITLE[currentDoc.path] ?? currentDoc.title) :
|
|
106
|
+
<strong>{currentDoc ? (lang === "en" ? currentDoc.navTitle ?? currentDoc.title : NAV_SHORT_TITLE[currentDoc.path] ?? currentDoc.title) : t.overviewHint}</strong>
|
|
97
107
|
</div>
|
|
98
108
|
</div>
|
|
99
109
|
|
|
100
110
|
<button
|
|
101
111
|
type="button"
|
|
102
112
|
className="gdoc-backdrop"
|
|
103
|
-
aria-label=
|
|
113
|
+
aria-label={t.closeSidebar}
|
|
104
114
|
tabIndex={mobileOpen ? 0 : -1}
|
|
105
115
|
data-open={mobileOpen}
|
|
106
116
|
onClick={() => closeMobile(true)}
|
|
@@ -109,27 +119,27 @@ export function GardenDocsNav({
|
|
|
109
119
|
<aside
|
|
110
120
|
id={drawerId}
|
|
111
121
|
className="gdoc-sidebar"
|
|
112
|
-
aria-label=
|
|
122
|
+
aria-label={t.sidebar}
|
|
113
123
|
data-open={mobileOpen}
|
|
114
124
|
>
|
|
115
125
|
<div className="gdoc-sidebar-head">
|
|
116
126
|
<Link
|
|
117
|
-
href={siteHref(
|
|
127
|
+
href={siteHref(handbookPath(lang))}
|
|
118
128
|
className="gdoc-overview-link"
|
|
119
129
|
aria-current={isOverview ? "page" : undefined}
|
|
120
130
|
onClick={() => closeMobile(false)}
|
|
121
131
|
>
|
|
122
132
|
<span className="gdoc-overview-mark">
|
|
123
133
|
<BookOpenText size={16} weight="duotone" aria-hidden="true" />
|
|
124
|
-
<span>
|
|
134
|
+
<span>{t.name}</span>
|
|
125
135
|
</span>
|
|
126
|
-
<small
|
|
136
|
+
<small>{t.overviewHint}</small>
|
|
127
137
|
</Link>
|
|
128
138
|
<button
|
|
129
139
|
ref={closeRef}
|
|
130
140
|
type="button"
|
|
131
141
|
className="gdoc-mobile-close"
|
|
132
|
-
aria-label=
|
|
142
|
+
aria-label={t.closeSidebar}
|
|
133
143
|
onClick={() => closeMobile(true)}
|
|
134
144
|
>
|
|
135
145
|
<X size={17} aria-hidden="true" />
|
|
@@ -137,35 +147,32 @@ export function GardenDocsNav({
|
|
|
137
147
|
<a
|
|
138
148
|
className="gdoc-nojs-close"
|
|
139
149
|
href={`#${drawerId}-open`}
|
|
140
|
-
aria-label=
|
|
150
|
+
aria-label={t.closeSidebar}
|
|
141
151
|
>
|
|
142
152
|
<X size={17} aria-hidden="true" />
|
|
143
153
|
</a>
|
|
144
154
|
</div>
|
|
145
155
|
|
|
146
|
-
<nav className="gdoc-nav" aria-label=
|
|
147
|
-
{groups.map((group) => (
|
|
156
|
+
<nav className="gdoc-nav" aria-label={t.chapters}>
|
|
157
|
+
{(isReferencePath(currentPath) ? [...groups].sort((a, b) => Number(b.id === REFERENCE_SECTION) - Number(a.id === REFERENCE_SECTION)) : groups).map((group) => (
|
|
148
158
|
<div key={group.id} className="gdoc-nav-group">
|
|
149
|
-
<
|
|
150
|
-
<h2>{group.title}</h2>
|
|
151
|
-
{group.note ? <span>{group.note}</span> : null}
|
|
152
|
-
</div>
|
|
159
|
+
<h2 className="gdoc-nav-group-head">{sectionTitle(HANDBOOK_SECTIONS.find((sec) => sec.id === group.id), lang) ?? group.title}</h2>
|
|
153
160
|
<ol className="gdoc-nav-list">
|
|
154
161
|
{group.items.map((item) => {
|
|
155
162
|
const active = item.path === currentPath;
|
|
156
163
|
const badge = COMPONENT_BADGE[item.path];
|
|
157
|
-
const label = NAV_SHORT_TITLE[item.path] ?? item.title;
|
|
164
|
+
const label = lang === "en" ? item.navTitle ?? item.title : NAV_SHORT_TITLE[item.path] ?? item.title;
|
|
158
165
|
return (
|
|
159
166
|
<li key={item.path} className={active ? "is-active" : undefined}>
|
|
160
167
|
<Link
|
|
161
|
-
href={siteHref(
|
|
168
|
+
href={siteHref(handbookPath(lang, item.path))}
|
|
162
169
|
aria-current={active ? "page" : undefined}
|
|
163
170
|
onClick={() => closeMobile(false)}
|
|
164
171
|
>
|
|
165
172
|
{badge ? (
|
|
166
173
|
<>
|
|
167
174
|
<code className="gdoc-comp-pill">{badge}</code>
|
|
168
|
-
<span className="gdoc-comp-label">{label}</span>
|
|
175
|
+
{group.id === REFERENCE_SECTION ? null : <span className="gdoc-comp-label">{label}</span>}
|
|
169
176
|
</>
|
|
170
177
|
) : (
|
|
171
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>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { HANDBOOK_LANG_KEY, type HandbookLang } from "../lib/garden-docs-meta";
|
|
5
|
+
import { swapHandbookLang } from "../lib/handbook-path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* First visit: follow the browser language. Afterwards: follow the reader's
|
|
9
|
+
* saved choice (set by the 中 / EN switch). Only moves to a page that exists,
|
|
10
|
+
* which the hreflang link in <head> tells us.
|
|
11
|
+
*/
|
|
12
|
+
export function HandbookLangRedirect({ lang }: { lang: HandbookLang }) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
let saved: string | null = null;
|
|
15
|
+
try {
|
|
16
|
+
saved = localStorage.getItem(HANDBOOK_LANG_KEY);
|
|
17
|
+
} catch {}
|
|
18
|
+
const wanted: HandbookLang = saved === "en" || saved === "zh"
|
|
19
|
+
? saved
|
|
20
|
+
: /^en\b/i.test(navigator.language || "") ? "en" : "zh";
|
|
21
|
+
if (wanted === lang) return;
|
|
22
|
+
const link = document.querySelector<HTMLLinkElement>(`link[rel="alternate"][hreflang="${wanted === "en" ? "en" : "zh-CN"}"]`);
|
|
23
|
+
if (!link?.href) return;
|
|
24
|
+
// The hreflang link says the page exists; the address comes from this one,
|
|
25
|
+
// so the origin and any basePath stay as they are.
|
|
26
|
+
const pathname = swapHandbookLang(location.pathname, wanted);
|
|
27
|
+
if (pathname && pathname !== location.pathname) location.replace(pathname + location.search + location.hash);
|
|
28
|
+
}, [lang]);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { HANDBOOK_LANG_KEY, handbookBase, type HandbookLang } from "../lib/garden-docs-meta";
|
|
6
|
+
import { siteHref } from "../lib/site-href";
|
|
7
|
+
import { swapHandbookLang } from "../lib/handbook-path";
|
|
8
|
+
import { siteLang } from "../lib/i18n";
|
|
9
|
+
import { handbookPageLang, handbookSwitchLabel, handbookSwitchTarget, handbookSwitchTitle } from "../lib/handbook-switch.mjs";
|
|
10
|
+
|
|
11
|
+
const noSubscribe = () => () => {};
|
|
12
|
+
|
|
13
|
+
/** The counterpart page from the hreflang link in <head>, on this origin. */
|
|
14
|
+
function counterpartHref(target: HandbookLang) {
|
|
15
|
+
const link = document.querySelector<HTMLLinkElement>(`link[rel="alternate"][hreflang="${target === "en" ? "en" : "zh-CN"}"]`);
|
|
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);
|
|
19
|
+
return target === "zh" ? siteHref(`${handbookBase("zh")}/`) : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 「中 / EN」 in the top bar on handbook pages. Goes to the counterpart page. */
|
|
23
|
+
export function HandbookLangSwitch() {
|
|
24
|
+
const pathname = usePathname() ?? "";
|
|
25
|
+
const lang: HandbookLang = handbookPageLang(pathname);
|
|
26
|
+
const target: HandbookLang = handbookSwitchTarget(lang);
|
|
27
|
+
const href = useSyncExternalStore(
|
|
28
|
+
noSubscribe,
|
|
29
|
+
() => counterpartHref(target),
|
|
30
|
+
() => null,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const remember = () => {
|
|
34
|
+
try {
|
|
35
|
+
localStorage.setItem(HANDBOOK_LANG_KEY, target);
|
|
36
|
+
} catch {}
|
|
37
|
+
};
|
|
38
|
+
const label = (
|
|
39
|
+
<>
|
|
40
|
+
<span aria-current={lang === "zh" ? "true" : undefined}>中</span>
|
|
41
|
+
<span aria-hidden="true"> / </span>
|
|
42
|
+
<span aria-current={lang === "en" ? "true" : undefined}>EN</span>
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
if (!href) {
|
|
46
|
+
return <span className="sh-lang is-disabled" title={handbookSwitchTitle(lang, siteLang)}>{label}</span>;
|
|
47
|
+
}
|
|
48
|
+
return (
|
|
49
|
+
<a className="sh-lang" href={href} hrefLang={target === "en" ? "en" : "zh-CN"} lang={target === "en" ? "en" : undefined} onClick={remember} aria-label={handbookSwitchLabel(lang)}>
|
|
50
|
+
{label}
|
|
51
|
+
</a>
|
|
52
|
+
);
|
|
53
|
+
}
|