hd-garden 0.8.0 → 0.8.2
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/ArticleMeasure.tsx +6 -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/MermaidDiagram.tsx +4 -3
- 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 +49 -1
- 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
package/scripts/link-assets.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import matter from "gray-matter";
|
|
|
19
19
|
import { fetchFavicon, fetchImage, fetchPageMeta, isPublicUrl, linkHost } from "./link-meta-core.mjs";
|
|
20
20
|
import { parse as parseYaml } from "yaml";
|
|
21
21
|
import { markdownLinks } from "../ui/garden/markdown-links.mjs";
|
|
22
|
+
import { gardenSourceUrls } from "../ui/garden/source-icon.mjs";
|
|
22
23
|
import { configuredSiteAddress, sameSiteMatcher } from "../ui/garden/same-site.mjs";
|
|
23
24
|
|
|
24
25
|
export const CACHE_FILE = "link-assets.json";
|
|
@@ -76,6 +77,12 @@ export function collectSiteLinks(siteRoot, env = process.env) {
|
|
|
76
77
|
const host = linkHost(url);
|
|
77
78
|
if (host && !hosts.has(host)) hosts.set(host, url);
|
|
78
79
|
}
|
|
80
|
+
// Card sources get the same favicon as links to that site.
|
|
81
|
+
for (const url of gardenSourceUrls(content)) {
|
|
82
|
+
if (!isPublicUrl(url) || sameSite(url)) continue;
|
|
83
|
+
const host = linkHost(url);
|
|
84
|
+
if (host && !hosts.has(host)) hosts.set(host, url);
|
|
85
|
+
}
|
|
79
86
|
}
|
|
80
87
|
return { hosts, cards, urls };
|
|
81
88
|
}
|
package/scripts/new-post.mjs
CHANGED
|
@@ -3,12 +3,14 @@ import { lstat, mkdir, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { formatAuthorDate } from "./site-date.mjs";
|
|
5
5
|
import { cheatSheet } from "../ui/garden/catalog.mjs";
|
|
6
|
+
import { siteCliLang } from "../ui/core/cli-locale.mjs";
|
|
6
7
|
|
|
7
8
|
const root = path.resolve(process.env.GARDEN_SITE || process.cwd());
|
|
8
9
|
const option = (name) => (process.env[`GARDEN_NEW_${name}`] ?? "").trim();
|
|
9
10
|
|
|
10
11
|
async function main() {
|
|
11
|
-
const
|
|
12
|
+
const lang = siteCliLang(root);
|
|
13
|
+
const title = option("TITLE") || (lang === "en" ? "New Post" : "新文章");
|
|
12
14
|
const date = formatAuthorDate();
|
|
13
15
|
const titleSlug = title.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "");
|
|
14
16
|
const slug = option("SLUG") || titleSlug || `post-${date.slice(0, 10)}-${randomUUID().slice(0, 8)}`;
|
|
@@ -41,16 +43,16 @@ async function main() {
|
|
|
41
43
|
"---",
|
|
42
44
|
`title: ${JSON.stringify(title)}`,
|
|
43
45
|
`date: ${date}`,
|
|
44
|
-
`description: ${JSON.stringify("请填写一句话摘要。")}`,
|
|
46
|
+
`description: ${JSON.stringify(lang === "en" ? "Write a one-sentence summary." : "请填写一句话摘要。")}`,
|
|
45
47
|
"tags: []",
|
|
46
48
|
"draft: true",
|
|
47
49
|
"---",
|
|
48
50
|
"",
|
|
49
|
-
"## 起因",
|
|
51
|
+
lang === "en" ? "## Context" : "## 起因",
|
|
50
52
|
"",
|
|
51
|
-
"从这里开始写正文。",
|
|
53
|
+
lang === "en" ? "Start writing here." : "从这里开始写正文。",
|
|
52
54
|
"",
|
|
53
|
-
cheatSheet(),
|
|
55
|
+
cheatSheet(lang),
|
|
54
56
|
"",
|
|
55
57
|
].join("\n");
|
|
56
58
|
try {
|
|
@@ -62,10 +64,17 @@ async function main() {
|
|
|
62
64
|
throw error;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
if (lang === "en") {
|
|
68
|
+
console.log(`Created draft: ${path.relative(root, destination)}`);
|
|
69
|
+
console.log(`Post URL: /blog/${[...segments, slug].join("/")}/`);
|
|
70
|
+
console.log("Preview: CONTENT_INCLUDE_DRAFTS=1 make dev");
|
|
71
|
+
console.log("Before publishing, fill in description, tags, and body, set draft to false, then run make update.");
|
|
72
|
+
} else {
|
|
73
|
+
console.log(`已创建草稿:${path.relative(root, destination)}`);
|
|
74
|
+
console.log(`文章路径:/blog/${[...segments, slug].join("/")}/`);
|
|
75
|
+
console.log("预览:CONTENT_INCLUDE_DRAFTS=1 make dev");
|
|
76
|
+
console.log("发布前请完善摘要、标签与正文,并将 draft 改为 false,再运行 make update。");
|
|
77
|
+
}
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
main().catch((error) => {
|
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { langOf, siteCliLang } from "../ui/core/cli-locale.mjs";
|
|
5
6
|
import { STATIC_PLACEHOLDER } from "../ui/garden/static-params.mjs";
|
|
6
7
|
|
|
7
8
|
const forbiddenDirectoryNames = new Set([
|
|
@@ -30,8 +31,12 @@ const isPublishedFile = (relativePath) =>
|
|
|
30
31
|
publishedExtensions[relativePath.split("/", 1)[0]]?.has(path.extname(relativePath)) === true;
|
|
31
32
|
const deploymentVersionPlaceholder = "00000000-0000-4000-8000-000000000000";
|
|
32
33
|
|
|
34
|
+
const cliLang = process.env.GARDEN_LOCALE ? langOf(process.env.GARDEN_LOCALE) : siteCliLang(process.env.GARDEN_SITE ?? process.cwd());
|
|
35
|
+
/** Chinese or English CLI text, by the site's locale. */
|
|
36
|
+
const msg = (zh, en) => (cliLang === "en" ? en : zh);
|
|
37
|
+
|
|
33
38
|
function fail(message) {
|
|
34
|
-
throw new Error(`Pages 产物无效:${message}`);
|
|
39
|
+
throw new Error(msg(`Pages 产物无效:${message}`, `Invalid Pages output: ${message}`));
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
async function assertDirectory(directory, label) {
|
|
@@ -39,9 +44,9 @@ async function assertDirectory(directory, label) {
|
|
|
39
44
|
try {
|
|
40
45
|
stat = await fs.stat(directory);
|
|
41
46
|
} catch {
|
|
42
|
-
fail(`${label}不存在:${directory}`);
|
|
47
|
+
fail(msg(`${label}不存在:${directory}`, `${label} does not exist: ${directory}`));
|
|
43
48
|
}
|
|
44
|
-
if (!stat.isDirectory()) fail(`${label}不是目录:${directory}`);
|
|
49
|
+
if (!stat.isDirectory()) fail(msg(`${label}不是目录:${directory}`, `${label} is not a directory: ${directory}`));
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
async function inspectTree(root, directory = root) {
|
|
@@ -51,20 +56,20 @@ async function inspectTree(root, directory = root) {
|
|
|
51
56
|
const relativePath = path.relative(root, absolutePath).split(path.sep).join("/");
|
|
52
57
|
const stat = await fs.lstat(absolutePath);
|
|
53
58
|
|
|
54
|
-
if (stat.isSymbolicLink()) fail(`不允许软链接:${relativePath}`);
|
|
59
|
+
if (stat.isSymbolicLink()) fail(msg(`不允许软链接:${relativePath}`, `Symlinks are not allowed: ${relativePath}`));
|
|
55
60
|
if (forbiddenDirectoryNames.has(entry.name)) {
|
|
56
|
-
fail(`不允许源码目录:${relativePath}`);
|
|
61
|
+
fail(msg(`不允许源码目录:${relativePath}`, `Source directories are not allowed: ${relativePath}`));
|
|
57
62
|
}
|
|
58
|
-
if (entry.name.startsWith(".env")) fail(`不允许环境文件:${relativePath}`);
|
|
59
|
-
if (entry.name.startsWith(STATIC_PLACEHOLDER)) fail(`空动态路由的占位页没有删掉:${relativePath}`);
|
|
63
|
+
if (entry.name.startsWith(".env")) fail(msg(`不允许环境文件:${relativePath}`, `Environment files are not allowed: ${relativePath}`));
|
|
64
|
+
if (entry.name.startsWith(STATIC_PLACEHOLDER)) fail(msg(`空动态路由的占位页没有删掉:${relativePath}`, `Placeholder page for an empty dynamic route was not removed: ${relativePath}`));
|
|
60
65
|
if (/^package(?:-lock)?\.json$/u.test(entry.name)) {
|
|
61
|
-
fail(`不允许依赖清单:${relativePath}`);
|
|
66
|
+
fail(msg(`不允许依赖清单:${relativePath}`, `Dependency manifests are not allowed: ${relativePath}`));
|
|
62
67
|
}
|
|
63
68
|
if (forbiddenFileNames.has(entry.name)) {
|
|
64
|
-
fail(`不允许源码配置:${relativePath}`);
|
|
69
|
+
fail(msg(`不允许源码配置:${relativePath}`, `Source config files are not allowed: ${relativePath}`));
|
|
65
70
|
}
|
|
66
71
|
if (entry.isFile() && forbiddenExtensions.has(path.extname(entry.name)) && !isPublishedFile(relativePath)) {
|
|
67
|
-
fail(`不允许源码或 Source Map:${relativePath}`);
|
|
72
|
+
fail(msg(`不允许源码或 Source Map:${relativePath}`, `Source files or source maps are not allowed: ${relativePath}`));
|
|
68
73
|
}
|
|
69
74
|
if (entry.isDirectory()) await inspectTree(root, absolutePath);
|
|
70
75
|
}
|
|
@@ -163,25 +168,25 @@ async function stabilizeDeploymentVersion(root) {
|
|
|
163
168
|
|
|
164
169
|
export async function validateStaticOutput(sourceDirectory) {
|
|
165
170
|
const source = path.resolve(sourceDirectory);
|
|
166
|
-
await assertDirectory(source, "静态产物目录");
|
|
171
|
+
await assertDirectory(source, msg("静态产物目录", "Static output directory"));
|
|
167
172
|
await fs.access(path.join(source, "index.html")).catch(() => {
|
|
168
|
-
fail(`缺少入口文件:${path.join(source, "index.html")}`);
|
|
173
|
+
fail(msg(`缺少入口文件:${path.join(source, "index.html")}`, `Missing entry file: ${path.join(source, "index.html")}`));
|
|
169
174
|
});
|
|
170
175
|
await inspectTree(source);
|
|
171
176
|
return source;
|
|
172
177
|
}
|
|
173
178
|
|
|
174
|
-
export async function preparePagesOutput(sourceDirectory, destinationDirectory) {
|
|
179
|
+
export async function preparePagesOutput(sourceDirectory, destinationDirectory, options = {}) {
|
|
175
180
|
const source = await validateStaticOutput(sourceDirectory);
|
|
176
181
|
const destination = path.resolve(destinationDirectory);
|
|
177
|
-
await assertDirectory(destination, "Pages 仓库目录");
|
|
182
|
+
await assertDirectory(destination, msg("Pages 仓库目录", "Pages repository directory"));
|
|
178
183
|
await fs.access(path.join(destination, ".git")).catch(() => {
|
|
179
|
-
fail(`目标不是 Git 仓库:${destination}`);
|
|
184
|
+
fail(msg(`目标不是 Git 仓库:${destination}`, `Destination is not a Git repository: ${destination}`));
|
|
180
185
|
});
|
|
181
186
|
|
|
182
187
|
const relativeFromDestination = path.relative(destination, source);
|
|
183
188
|
if (!relativeFromDestination.startsWith("..") && !path.isAbsolute(relativeFromDestination)) {
|
|
184
|
-
fail("静态产物目录不能位于目标仓库内部");
|
|
189
|
+
fail(msg("静态产物目录不能位于目标仓库内部", "The static output directory cannot be inside the destination repository"));
|
|
185
190
|
}
|
|
186
191
|
|
|
187
192
|
for (const entry of await fs.readdir(destination, { withFileTypes: true })) {
|
|
@@ -191,9 +196,16 @@ export async function preparePagesOutput(sourceDirectory, destinationDirectory)
|
|
|
191
196
|
|
|
192
197
|
await fs.cp(source, destination, { recursive: true, force: true });
|
|
193
198
|
await fs.writeFile(path.join(destination, ".nojekyll"), "");
|
|
199
|
+
let isEn = options.lang === "en" || /^en/iu.test(String(process.env.GARDEN_LOCALE ?? ""));
|
|
200
|
+
if (!isEn && options.lang !== "zh") {
|
|
201
|
+
const indexHtml = await fs.readFile(path.join(source, "index.html"), "utf8").catch(() => "");
|
|
202
|
+
if (/<html\b[^>]*\slang="en(?:-US)?"/iu.test(indexHtml)) isEn = true;
|
|
203
|
+
}
|
|
194
204
|
await fs.writeFile(
|
|
195
205
|
path.join(destination, "README.md"),
|
|
196
|
-
|
|
206
|
+
isEn
|
|
207
|
+
? "# Garden Static Site\n\nThis repository holds public static files generated automatically by Garden. Do not edit by hand.\n"
|
|
208
|
+
: "# Garden 静态站点\n\n此仓库只保存 Garden 自动生成的公开静态文件,请勿手工编辑。\n",
|
|
197
209
|
);
|
|
198
210
|
await stabilizeDeploymentVersion(destination);
|
|
199
211
|
}
|
|
@@ -204,7 +216,7 @@ const invokedDirectly = process.argv[1] &&
|
|
|
204
216
|
if (invokedDirectly) {
|
|
205
217
|
const [, , sourceDirectory, destinationDirectory] = process.argv;
|
|
206
218
|
if (!sourceDirectory || process.argv.length > 4) {
|
|
207
|
-
throw new Error("用法:node scripts/prepare-pages-output.mjs <静态产物目录> [Pages 仓库目录]");
|
|
219
|
+
throw new Error(msg("用法:node scripts/prepare-pages-output.mjs <静态产物目录> [Pages 仓库目录]", "Usage: node scripts/prepare-pages-output.mjs <static output directory> [Pages repository directory]"));
|
|
208
220
|
}
|
|
209
221
|
if (destinationDirectory) {
|
|
210
222
|
await preparePagesOutput(sourceDirectory, destinationDirectory);
|
package/scripts/redirects.mjs
CHANGED
|
@@ -35,20 +35,26 @@ export function redirectFile(from) {
|
|
|
35
35
|
return /\.html?$/iu.test(clean) ? clean : path.posix.join(clean.replace(/\/+$/u, ""), "index.html");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function redirectPage({ href, canonical, icon = "" }) {
|
|
38
|
+
export function redirectPage({ href, canonical, icon = "", lang = "zh" }) {
|
|
39
39
|
// The site's own icon, so the browser does not go looking for /favicon.ico.
|
|
40
40
|
const iconLink = icon ? `\n<link rel="icon" href="${escapeHtml(icon)}"${/\.svg(?:[?#]|$)/iu.test(icon) ? ' type="image/svg+xml"' : ""}>` : "";
|
|
41
|
+
const isEn = lang === "en";
|
|
42
|
+
const htmlLang = isEn ? "en" : "zh-CN";
|
|
43
|
+
const title = isEn ? "Page moved" : "页面已移动";
|
|
44
|
+
const bodyText = isEn
|
|
45
|
+
? `<p>This page has moved to <a href="${escapeHtml(href)}">${escapeHtml(canonical)}</a>.</p>`
|
|
46
|
+
: `<p>页面已移到 <a href="${escapeHtml(href)}">${escapeHtml(canonical)}</a>。</p>`;
|
|
41
47
|
return `<!doctype html>
|
|
42
|
-
<html lang="
|
|
48
|
+
<html lang="${htmlLang}">
|
|
43
49
|
<head>
|
|
44
50
|
<meta charset="utf-8">
|
|
45
|
-
<title
|
|
51
|
+
<title>${title}</title>
|
|
46
52
|
<link rel="canonical" href="${escapeHtml(canonical)}">${iconLink}
|
|
47
53
|
<meta http-equiv="refresh" content="0; url=${escapeHtml(href)}">
|
|
48
54
|
<script>location.replace(${JSON.stringify(href).replace(/</gu, "\\u003c")}${href.includes("#") ? "" : " + location.hash"})</script>
|
|
49
55
|
</head>
|
|
50
56
|
<body>
|
|
51
|
-
|
|
57
|
+
${bodyText}
|
|
52
58
|
</body>
|
|
53
59
|
</html>
|
|
54
60
|
`;
|
|
@@ -67,6 +73,7 @@ export function redirectPage({ href, canonical, icon = "" }) {
|
|
|
67
73
|
*/
|
|
68
74
|
export function generateRedirects({ outDir, config = {}, env = {}, basePath = "", log = () => {}, extra = [], icon = "" }) {
|
|
69
75
|
const base = String(basePath ?? "").replace(/\/+$/u, "");
|
|
76
|
+
const lang = /^en/iu.test(String(config?.locale ?? "")) ? "en" : "zh";
|
|
70
77
|
let origin = "";
|
|
71
78
|
try {
|
|
72
79
|
const address = configuredSiteAddress(config, env);
|
|
@@ -118,7 +125,7 @@ export function generateRedirects({ outDir, config = {}, env = {}, basePath = ""
|
|
|
118
125
|
const href = external ? to : withTrailingSlash(`${base}${to}`);
|
|
119
126
|
const canonical = external || !origin ? href : `${origin}${href}`;
|
|
120
127
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
121
|
-
fs.writeFileSync(file, redirectPage({ href, canonical, icon }));
|
|
128
|
+
fs.writeFileSync(file, redirectPage({ href, canonical, icon, lang }));
|
|
122
129
|
written.push({ from, to: href, file });
|
|
123
130
|
}
|
|
124
131
|
return { written, expected: pairs.length, skipped };
|
|
@@ -136,7 +143,11 @@ export function writeRedirects(options) {
|
|
|
136
143
|
* warning (a page went missing without a reason).
|
|
137
144
|
*/
|
|
138
145
|
export function runRedirectStep({ siteRoot, env = process.env, out = process.stdout, err = process.stderr, generate = generateRedirects }) {
|
|
139
|
-
const
|
|
146
|
+
const baseDir = String(env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/^\/+|\/+$/gu, "");
|
|
147
|
+
// A base-path build exports pages under dist/client/<base>/, so old paths and targets live there.
|
|
148
|
+
const outDir = baseDir && fs.existsSync(path.join(siteRoot, "dist", "client", baseDir))
|
|
149
|
+
? path.join(siteRoot, "dist", "client", baseDir)
|
|
150
|
+
: path.join(siteRoot, "dist", "client");
|
|
140
151
|
try {
|
|
141
152
|
const config = parseYaml(fs.readFileSync(path.join(siteRoot, "site.config.yaml"), "utf8")) ?? {};
|
|
142
153
|
const columnsFile = path.join(siteRoot, "content", "columns.yaml");
|
|
@@ -137,3 +137,70 @@ intent: |
|
|
|
137
137
|
src: ./post-name/spec.zip
|
|
138
138
|
note: 一句话说明里面有什么
|
|
139
139
|
```
|
|
140
|
+
|
|
141
|
+
## relations · 关系图
|
|
142
|
+
|
|
143
|
+
三到六个关键要素,读者需要看清谁影响谁、哪些是双向的;首尾相接的循环也用它,把每一步写成一条 link。
|
|
144
|
+
|
|
145
|
+
不该用:要素更多、有分支或先后顺序时,用 ```mermaid 流程图;只走一遍的步骤用 garden list。
|
|
146
|
+
|
|
147
|
+
```garden relations
|
|
148
|
+
items:
|
|
149
|
+
- title: 观察
|
|
150
|
+
text: 记下发生了什么
|
|
151
|
+
- title: 假设
|
|
152
|
+
text: 猜一个原因
|
|
153
|
+
- title: 实验
|
|
154
|
+
text: 只改一个变量
|
|
155
|
+
- title: 复盘
|
|
156
|
+
text: 留下能复用的结论
|
|
157
|
+
links:
|
|
158
|
+
- from: 1
|
|
159
|
+
to: 2
|
|
160
|
+
- from: 2
|
|
161
|
+
to: 3
|
|
162
|
+
- from: 3
|
|
163
|
+
to: 4
|
|
164
|
+
- from: 4
|
|
165
|
+
to: 1
|
|
166
|
+
- from: 4
|
|
167
|
+
to: 2
|
|
168
|
+
style: dashed
|
|
169
|
+
label: 改写
|
|
170
|
+
caption: 复盘的结论会改写下一轮的假设。
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## list · 图标列表
|
|
174
|
+
|
|
175
|
+
几条并列的行为、信号或做法,每条配一个线条图标;带标题时每条可以写几段说明。
|
|
176
|
+
|
|
177
|
+
不该用:条目之间有先后顺序或要编号时,用普通有序列表。
|
|
178
|
+
|
|
179
|
+
```garden list
|
|
180
|
+
items:
|
|
181
|
+
- icon: search
|
|
182
|
+
text: 搜索「降噪耳机」
|
|
183
|
+
- icon: eye
|
|
184
|
+
text: 看了 5 个商品详情
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## choice · 选择卡
|
|
188
|
+
|
|
189
|
+
给读者两三个具体选项让他选一个,每个选项有图标、一个醒目的数字和一句说明。
|
|
190
|
+
|
|
191
|
+
不该用:选项会改变后面的计算结果时,写成想法,编译成 interactive。
|
|
192
|
+
|
|
193
|
+
```garden choice
|
|
194
|
+
title: 你更倾向哪一种?
|
|
195
|
+
items:
|
|
196
|
+
- icon: shield
|
|
197
|
+
value: ¥2,950
|
|
198
|
+
text: 原装,价格略高
|
|
199
|
+
label: 更看重可靠
|
|
200
|
+
- icon: wallet
|
|
201
|
+
value: ¥2,450
|
|
202
|
+
text: 换过电池,价格更低
|
|
203
|
+
label: 更看重价格
|
|
204
|
+
none:
|
|
205
|
+
label: 都不合适
|
|
206
|
+
```
|
|
@@ -126,5 +126,66 @@ note: 一句话说明里面有什么
|
|
|
126
126
|
<key>shortcut</key>
|
|
127
127
|
<string>;gfile</string>
|
|
128
128
|
</dict>
|
|
129
|
+
<dict>
|
|
130
|
+
<key>phrase</key>
|
|
131
|
+
<string>```garden relations
|
|
132
|
+
items:
|
|
133
|
+
- title: 观察
|
|
134
|
+
text: 记下发生了什么
|
|
135
|
+
- title: 假设
|
|
136
|
+
text: 猜一个原因
|
|
137
|
+
- title: 实验
|
|
138
|
+
text: 只改一个变量
|
|
139
|
+
- title: 复盘
|
|
140
|
+
text: 留下能复用的结论
|
|
141
|
+
links:
|
|
142
|
+
- from: 1
|
|
143
|
+
to: 2
|
|
144
|
+
- from: 2
|
|
145
|
+
to: 3
|
|
146
|
+
- from: 3
|
|
147
|
+
to: 4
|
|
148
|
+
- from: 4
|
|
149
|
+
to: 1
|
|
150
|
+
- from: 4
|
|
151
|
+
to: 2
|
|
152
|
+
style: dashed
|
|
153
|
+
label: 改写
|
|
154
|
+
caption: 复盘的结论会改写下一轮的假设。
|
|
155
|
+
```</string>
|
|
156
|
+
<key>shortcut</key>
|
|
157
|
+
<string>;grel</string>
|
|
158
|
+
</dict>
|
|
159
|
+
<dict>
|
|
160
|
+
<key>phrase</key>
|
|
161
|
+
<string>```garden list
|
|
162
|
+
items:
|
|
163
|
+
- icon: search
|
|
164
|
+
text: 搜索「降噪耳机」
|
|
165
|
+
- icon: eye
|
|
166
|
+
text: 看了 5 个商品详情
|
|
167
|
+
```</string>
|
|
168
|
+
<key>shortcut</key>
|
|
169
|
+
<string>;glist</string>
|
|
170
|
+
</dict>
|
|
171
|
+
<dict>
|
|
172
|
+
<key>phrase</key>
|
|
173
|
+
<string>```garden choice
|
|
174
|
+
title: 你更倾向哪一种?
|
|
175
|
+
items:
|
|
176
|
+
- icon: shield
|
|
177
|
+
value: ¥2,950
|
|
178
|
+
text: 原装,价格略高
|
|
179
|
+
label: 更看重可靠
|
|
180
|
+
- icon: wallet
|
|
181
|
+
value: ¥2,450
|
|
182
|
+
text: 换过电池,价格更低
|
|
183
|
+
label: 更看重价格
|
|
184
|
+
none:
|
|
185
|
+
label: 都不合适
|
|
186
|
+
```</string>
|
|
187
|
+
<key>shortcut</key>
|
|
188
|
+
<string>;gchoice</string>
|
|
189
|
+
</dict>
|
|
129
190
|
</array>
|
|
130
191
|
</plist>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
MESSAGE ?= Update blog
|
|
2
|
+
|
|
3
|
+
.DEFAULT_GOAL := help
|
|
4
|
+
|
|
5
|
+
.PHONY: help new embed skill prepare dev check build update
|
|
6
|
+
|
|
7
|
+
unexport TITLE SLUG DIR URL
|
|
8
|
+
export GARDEN_NEW_TITLE := $(value TITLE)
|
|
9
|
+
export GARDEN_NEW_SLUG := $(value SLUG)
|
|
10
|
+
export GARDEN_NEW_DIR := $(value DIR)
|
|
11
|
+
export GARDEN_EMBED_URL := $(value URL)
|
|
12
|
+
|
|
13
|
+
help:
|
|
14
|
+
@printf "%s\n" "Common Garden commands:" " make new Create a post draft with the engine" ' make new TITLE="Post title" SLUG=my-post DIR=writing' ' make embed URL="https://example.com" Fetch external page metadata' " make skill Install interactive chart skills" " make dev Install dependencies and preview locally" " make build Build the public site" " make update Check, commit, and push to publish"
|
|
15
|
+
|
|
16
|
+
new: prepare
|
|
17
|
+
npx garden new
|
|
18
|
+
|
|
19
|
+
embed:
|
|
20
|
+
@npx garden embed
|
|
21
|
+
|
|
22
|
+
skill: prepare
|
|
23
|
+
npx garden skill
|
|
24
|
+
|
|
25
|
+
prepare:
|
|
26
|
+
@if [ ! -d node_modules ]; then npm install; fi
|
|
27
|
+
|
|
28
|
+
dev: prepare
|
|
29
|
+
npx garden dev
|
|
30
|
+
|
|
31
|
+
check: prepare
|
|
32
|
+
npx garden sync && npx garden build
|
|
33
|
+
|
|
34
|
+
build: prepare
|
|
35
|
+
npx garden build
|
|
36
|
+
|
|
37
|
+
update:
|
|
38
|
+
@if git diff --quiet && git diff --cached --quiet && [ -z "$$(git ls-files --others --exclude-standard)" ]; then \
|
|
39
|
+
echo "No changes to publish"; \
|
|
40
|
+
else \
|
|
41
|
+
$(MAKE) check && \
|
|
42
|
+
git add -A && \
|
|
43
|
+
git commit -m "$(MESSAGE)" && \
|
|
44
|
+
git push origin HEAD; \
|
|
45
|
+
fi
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# My Garden
|
|
2
|
+
|
|
3
|
+
This repository holds only your writing and site configuration: `content/` and `site.config.yaml`. The page engine comes from [Garden](https://github.com/hedon954/garden).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install
|
|
7
|
+
make new TITLE="Hello, World" SLUG=hello
|
|
8
|
+
make dev
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Running `npx garden dev` for the first time fills in any missing `Makefile` or skills and creates `.env.local`. Cursor and Codex read `.agents/skills/` automatically when opening this repository. Run `npx garden skill` anytime to update the bundled skills.
|
|
12
|
+
|
|
13
|
+
Comments are disabled by default. Before publishing, open [Giscus](https://giscus.app), write the repository, repository ID, category, and category ID into `.env.local`, and set the matching `GISCUS_REPO`, `GISCUS_REPO_ID`, `GISCUS_CATEGORY`, and `GISCUS_CATEGORY_ID` under GitHub Actions Variables. Leaving them empty fails the publish workflow. To publish without comments, set `GISCUS_DISABLED` to `1`.
|
|
14
|
+
|
|
15
|
+
To upgrade the engine, update `hd-garden` in `package.json` to the new version and run `npm install`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Garden
|
|
2
|
+
tagline: Writing · Notes · Staying curious
|
|
3
|
+
description: Posts, short notes, and columns.
|
|
4
|
+
locale: en_US
|
|
5
|
+
# Public address of the site. Full URLs pointing here inside posts render as internal links.
|
|
6
|
+
# url: https://blog.example.com
|
|
7
|
+
# Site icons; falls back to Garden's built-in icons when omitted.
|
|
8
|
+
# brand:
|
|
9
|
+
# logo: brand/logo.svg
|
|
10
|
+
# favicon: /favicon.svg
|
|
11
|
+
# appleTouchIcon: /apple-touch-icon.png
|
|
12
|
+
|
|
13
|
+
author:
|
|
14
|
+
name: Your Name
|
|
15
|
+
github: your-github-id
|
|
16
|
+
githubBio: A short bio.
|
|
17
|
+
githubPinned: []
|
|
18
|
+
|
|
19
|
+
pages:
|
|
20
|
+
home:
|
|
21
|
+
title: |-
|
|
22
|
+
Notes on what I am building,
|
|
23
|
+
and questions worth thinking through.
|
|
24
|
+
subtitle: Software, reading notes, and occasional work logs.
|
|
25
|
+
blog:
|
|
26
|
+
title: Posts
|
|
27
|
+
subtitle: Finished long-form essays.
|
|
28
|
+
thoughts:
|
|
29
|
+
title: Thoughts
|
|
30
|
+
subtitle: Short notes, images, and links.
|
|
31
|
+
# How many items to show per screen; the next screen loads automatically as you scroll. Default: 10.
|
|
32
|
+
# pageSize: 10
|
|
33
|
+
columns:
|
|
34
|
+
title: Columns
|
|
35
|
+
subtitle: Series of posts meant to be read in order.
|
|
36
|
+
about:
|
|
37
|
+
title: Hi, I am Your Name.
|
|
38
|
+
subtitle: Essays and notes live here.
|
|
39
|
+
heading: About
|
|
40
|
+
paragraphs:
|
|
41
|
+
- A software engineer writing code and keeping notes on things figured out along the way.
|
|
42
|
+
- This site holds long-form posts, short thoughts, and columns.
|
|
43
|
+
quote: ""
|
|
44
|
+
focus:
|
|
45
|
+
- Software
|
|
46
|
+
- Notes
|
|
47
|
+
- Daily
|
|
48
|
+
cta:
|
|
49
|
+
href: /blog
|
|
50
|
+
label: Browse posts
|
|
51
|
+
garden:
|
|
52
|
+
title: "Write in Markdown.\nPush to update the site."
|
|
53
|
+
subtitle: ""
|
|
54
|
+
|
|
55
|
+
footer: Keep writing.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: garden-interactive-chart
|
|
3
|
+
description: >-
|
|
4
|
+
Writes Garden interactive charts as self-contained HTML in a same-name
|
|
5
|
+
sidecar directory and embeds them with a short garden html fence. Use when adding
|
|
6
|
+
or editing Garden widgets, steppers, inspect grids, param benches,
|
|
7
|
+
compare views, traces, KV cache charts, or any interactive chart / same-name directory HTML.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Garden interactive charts
|
|
11
|
+
|
|
12
|
+
Complex interaction does not go into Markdown. Each post has a same-name directory, and every `.html` file in it is one chart that can be opened on its own. The post body only embeds a short fence.
|
|
13
|
+
|
|
14
|
+
Background: [Interactive components](https://github.com/hedon954/garden/blob/main/docs/interactive-blog-components.md). Finished examples to copy: [widget-kind-examples](https://github.com/hedon954/garden/tree/main/docs/widget-kind-examples).
|
|
15
|
+
|
|
16
|
+
## When to draw, when to stop
|
|
17
|
+
|
|
18
|
+
Ask first: is the reader stuck because a process was frozen into a photo, a parameter is a fixed example, two dimensions were flattened, a comparison needs scrolling back and forth, or a "what if" cannot be tried? If not, write prose or Mermaid.
|
|
19
|
+
|
|
20
|
+
Five primitives. Pick one before you draw:
|
|
21
|
+
|
|
22
|
+
| What the reader should do | Primitive | Copy from |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| Stop at a given beat | Stepper | [stepper.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/stepper.html), [waste.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/waste.html) |
|
|
25
|
+
| Look at cells and talk about them | Inspect grid | [inspect-grid.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/inspect-grid.html) |
|
|
26
|
+
| Turn a knob, watch numbers or curves | Param bench | [param-bench.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/param-bench.html) |
|
|
27
|
+
| See both sides at the same moment | Compare view | [compare.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/compare.html) |
|
|
28
|
+
| See the current node and current edge | Trace | [trace.html](https://github.com/hedon954/garden/blob/main/docs/widget-kind-examples/trace.html) |
|
|
29
|
+
|
|
30
|
+
If it fits, change the data instead of adding a new type. Past about 9 nodes or 12 steps, split it into two charts.
|
|
31
|
+
|
|
32
|
+
## File conventions
|
|
33
|
+
|
|
34
|
+
When the Markdown is at `content/posts/<dir>/<slug>.md`, charts can only live in `content/posts/<dir>/<slug>/`:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
content/posts/windowsill-herbs.md
|
|
38
|
+
content/posts/windowsill-herbs/layers.html
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Body:
|
|
42
|
+
|
|
43
|
+
````md
|
|
44
|
+
```garden html
|
|
45
|
+
src: ./windowsill-herbs/layers.html
|
|
46
|
+
title: Water takes about a minute to seep from the soil surface to the drain hole.
|
|
47
|
+
```
|
|
48
|
+
````
|
|
49
|
+
|
|
50
|
+
Rules:
|
|
51
|
+
|
|
52
|
+
- `src` must be a local `.html` or `.pdf` in that same-name directory. It cannot point to `assets/`, another post, or `https://`
|
|
53
|
+
- HTML that reports its height with `garden-chart` is a chart and is shown inline. Plain HTML and PDF are folded into a preview entry by default and load when the reader opens them
|
|
54
|
+
- `title` must be a claim, not "see the chart below"
|
|
55
|
+
- One chart per HTML file. Use short kebab-case names such as `layers.html` or `diagram.html`
|
|
56
|
+
- The HTML is self-contained: CSS, SVG and scripts all live in that one file
|
|
57
|
+
- Write the HTML first, then the fence. Do not spell out step arrays or sample points in the fence
|
|
58
|
+
|
|
59
|
+
The build checks the path and copies the file to `/media/...`. A wrong path fails the build.
|
|
60
|
+
|
|
61
|
+
## Writing the HTML
|
|
62
|
+
|
|
63
|
+
1. Copy [references/shell.html](references/shell.html), or start from the closest example.
|
|
64
|
+
2. Use only the color and font tokens in [references/style.md](references/style.md). Do not invent a dark cinematic style.
|
|
65
|
+
3. Call the boot script in the shell: it reads `?theme=light|dark` and uses `ResizeObserver` to send `{ source: "garden-chart", height }` to the parent page.
|
|
66
|
+
4. Respect `prefers-reduced-motion: reduce`: autoplay stops at the first step or the final state.
|
|
67
|
+
5. Use native `button` and `input` controls. Bind sliders to fixed inputs. Never `eval` reader input, and never make network requests.
|
|
68
|
+
6. Open the HTML in a browser and walk the main path and the empty state. Then run `CONTENT_INCLUDE_DRAFTS=1 make dev` to check the embed.
|
|
69
|
+
|
|
70
|
+
## Don't
|
|
71
|
+
|
|
72
|
+
- Put steps, grids or formula sample points in Markdown
|
|
73
|
+
- Use remote scripts, remote iframes or any CDN (the system monospace stack is enough)
|
|
74
|
+
- Read or write the parent page's DOM or cookies, or depend on `allow-same-origin`
|
|
75
|
+
- Use MDX, JSX, or post-specific type names (for example `autoregressive-waste`)
|
|
76
|
+
- Make the interaction the only explanation: the surrounding prose must stand on its own
|
|
77
|
+
|
|
78
|
+
## Before you hand it in
|
|
79
|
+
|
|
80
|
+
- [ ] The file is in the same-name directory, and the fence has only `src` + `title`
|
|
81
|
+
- [ ] The HTML works when opened on its own, and the iframe grows to its height
|
|
82
|
+
- [ ] Light and dark themes both follow the Garden page
|
|
83
|
+
- [ ] No autoplay loop when reduced motion is on
|
|
84
|
+
- [ ] `title` gives the conclusion to readers without JS
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Chart title</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--paper: #f8f8f6; --paper-deep: #eeeeeb; --ink: #202124; --ink-soft: #34383d;
|
|
10
|
+
--muted: #65707d; --faint: #a1a6ad; --line: #deded9; --accent: #d82d48;
|
|
11
|
+
--accent-soft: #f9e8eb; --new: #1f7a4d; --new-soft: #e5f4ec;
|
|
12
|
+
}
|
|
13
|
+
:root[data-theme="dark"] {
|
|
14
|
+
--paper: #171819; --paper-deep: #202224; --ink: #ededeb; --ink-soft: #d8d8d4;
|
|
15
|
+
--muted: #a6adb6; --faint: #747b84; --line: #35383b; --accent: #ff5d76;
|
|
16
|
+
--accent-soft: #3b242a; --new: #5ee0a0; --new-soft: #1a3a2c;
|
|
17
|
+
}
|
|
18
|
+
html, body { margin: 0; background: var(--paper); color: var(--ink); }
|
|
19
|
+
body {
|
|
20
|
+
font: 13px/1.5 ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
|
21
|
+
padding: 16px;
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<!-- Chart body -->
|
|
27
|
+
<script>
|
|
28
|
+
function gardenChartBoot() {
|
|
29
|
+
const theme = new URLSearchParams(location.search).get("theme");
|
|
30
|
+
if (theme === "dark" || theme === "light") {
|
|
31
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
32
|
+
}
|
|
33
|
+
const report = () => {
|
|
34
|
+
if (window.parent === window) return;
|
|
35
|
+
const height = Math.ceil(Math.max(
|
|
36
|
+
document.body.scrollHeight,
|
|
37
|
+
document.body.offsetHeight,
|
|
38
|
+
document.documentElement.scrollHeight,
|
|
39
|
+
));
|
|
40
|
+
window.parent.postMessage({ source: "garden-chart", height }, "*");
|
|
41
|
+
};
|
|
42
|
+
new ResizeObserver(report).observe(document.body);
|
|
43
|
+
window.addEventListener("load", report);
|
|
44
|
+
requestAnimationFrame(() => requestAnimationFrame(report));
|
|
45
|
+
}
|
|
46
|
+
gardenChartBoot();
|
|
47
|
+
</script>
|
|
48
|
+
</body>
|
|
49
|
+
</html>
|