hd-garden 0.3.13 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/garden-interactive-chart/SKILL.md +6 -6
- package/.agents/skills/garden-ui/SKILL.md +4 -4
- package/app/a2/[slug]/page.tsx +61 -0
- package/app/a2/page.tsx +27 -0
- package/app/blog/[...path]/page.tsx +32 -22
- package/app/blog/page.tsx +6 -7
- package/app/columns/[column]/[...path]/page.tsx +32 -25
- package/app/columns/page.tsx +38 -52
- package/app/components/ArticleMeasure.tsx +95 -0
- package/app/components/ArticleTitle.tsx +20 -0
- package/app/components/BlogArchive.tsx +106 -102
- package/app/components/MarkdownArticle.tsx +145 -66
- package/app/components/MermaidDiagram.tsx +44 -71
- package/app/components/SiteFooter.tsx +3 -7
- package/app/components/SiteHeader.tsx +55 -41
- package/app/components/TableOfContents.tsx +3 -2
- package/app/components/TagIndex.tsx +97 -0
- package/app/components/garden/GardenBlock.tsx +45 -0
- package/app/components/garden/blocks.tsx +530 -0
- package/app/components/garden/chrome.tsx +350 -0
- package/app/components/garden/sandbox-message.ts +16 -0
- package/app/garden-modules.d.ts +2 -0
- package/app/globals.css +17 -12
- package/app/layout.tsx +9 -0
- package/app/lib/a2-meta.ts +22 -0
- package/app/lib/a2-preview.ts +50 -0
- package/app/lib/content.ts +52 -6
- package/app/lib/markdown-plugins.ts +2 -0
- package/app/lib/remark-a2.mjs +208 -0
- package/app/lib/remark-garden.mjs +88 -0
- package/app/page.tsx +122 -93
- package/app/styles/a2.css +1383 -0
- package/app/styles/shell.css +438 -0
- package/app/styles/tokens.css +111 -0
- package/app/tags/page.tsx +28 -0
- package/app/ui-parity/page.tsx +3 -2
- package/package.json +1 -1
- package/scripts/build-content.mjs +11 -1
- package/scripts/check-post.mjs +18 -4
- package/scripts/new-post.mjs +3 -0
- package/site.config.yaml +1 -1
- package/templates/garden-blocks.md +128 -0
- package/templates/garden-snippets.plist +121 -0
- package/ui/garden/catalog.mjs +196 -0
- package/ui/garden/check.mjs +103 -0
- package/ui/garden/enhance.mjs +89 -0
- package/ui/garden/parse.mjs +198 -0
- package/ui/garden/snippets.mjs +109 -0
- package/ui/garden/title.mjs +37 -0
- package/ui/garden/word-count.mjs +266 -0
- package/ui/host.mjs +45 -28
- package/ui/parity-fixtures.mjs +1 -1
- package/ui/preview/dist/garden-ui.css +81 -77
- package/ui/preview/dist/garden-ui.esm.js +124 -89
- package/ui/preview/dist/garden-ui.iife.js +124 -89
- package/ui/prompt/rules.md +17 -1
- package/ui/react/CatalogPage.tsx +12 -0
- package/ui/react/components/display.tsx +1 -1
- package/ui/react/components/media.tsx +2 -2
- package/ui/react/ui.css +81 -77
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: garden-interactive-chart
|
|
3
3
|
description: >-
|
|
4
4
|
Writes Garden interactive charts as self-contained HTML in a same-name
|
|
5
|
-
sidecar directory and embeds them with a short
|
|
5
|
+
sidecar directory and embeds them with a short garden html fence. Use when adding
|
|
6
6
|
or editing Garden widgets, steppers, inspect grids, param benches,
|
|
7
7
|
compare views, traces, KV cache charts, or any 可交互图表 / 同名目录 HTML.
|
|
8
8
|
---
|
|
@@ -41,9 +41,9 @@ content/posts/understand-kv-cache/waste.html
|
|
|
41
41
|
正文:
|
|
42
42
|
|
|
43
43
|
````md
|
|
44
|
-
```
|
|
44
|
+
```garden html
|
|
45
45
|
src: ./understand-kv-cache/waste.html
|
|
46
|
-
|
|
46
|
+
title: 朴素 decode 每步重算全部过去 token;6 个 token 共 21 次计算,其中 15 次是浪费。
|
|
47
47
|
```
|
|
48
48
|
````
|
|
49
49
|
|
|
@@ -51,7 +51,7 @@ caption: 朴素 decode 每步重算全部过去 token;6 个 token 共 21 次
|
|
|
51
51
|
|
|
52
52
|
- `src` 必须是该同名目录里的本地 `.html` 或 `.pdf`,不能指到 `assets/`、别的文章或 `https://`
|
|
53
53
|
- 带 `garden-chart` 高度回报的 HTML 是图表,直接铺开;普通 HTML 和 PDF 默认收成示意入口,读者展开后再加载
|
|
54
|
-
- `
|
|
54
|
+
- `title` 必须是判断,不能是「见下图」
|
|
55
55
|
- 一份 HTML 一张图;文件名用 `waste.html`、`stepper.html` 这种短 kebab
|
|
56
56
|
- HTML 自包含:CSS、SVG、脚本都在这一个文件里
|
|
57
57
|
- 先写 HTML,再写围栏;围栏里不要再摊步进数组或采样点
|
|
@@ -77,8 +77,8 @@ caption: 朴素 decode 每步重算全部过去 token;6 个 token 共 21 次
|
|
|
77
77
|
|
|
78
78
|
## 交稿前
|
|
79
79
|
|
|
80
|
-
- [ ] 文件在同名目录内,围栏只有 `src` + `
|
|
80
|
+
- [ ] 文件在同名目录内,围栏只有 `src` + `title`
|
|
81
81
|
- [ ] 单独打开 HTML 就能用;iframe 能撑开高度
|
|
82
82
|
- [ ] 浅色 / 深色主题都跟 Garden 纸面
|
|
83
83
|
- [ ] 减少动态时不自动循环
|
|
84
|
-
- [ ] `
|
|
84
|
+
- [ ] `title` 让关 JS 的人也能读到结论
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: garden-ui
|
|
3
3
|
description: >-
|
|
4
|
-
Compiles the intent blocks in one Garden post into ui blocks (or prose, or
|
|
5
|
-
|
|
4
|
+
Compiles the intent blocks in one Garden post into ui blocks (or prose, or
|
|
5
|
+
garden html), recompiles stale or broken ui blocks, and suggests where a post
|
|
6
6
|
could use interaction. Use when the user says 编译这篇 / 编译意图块 / 重编译
|
|
7
7
|
<id> / 这篇哪里适合交互, or pastes a garden ui check fix prompt.
|
|
8
8
|
---
|
|
@@ -43,7 +43,7 @@ description: >-
|
|
|
43
43
|
3. 每块读原话和所在小节,三选一:
|
|
44
44
|
- **写回正文**:一句话、一张表或一张 Mermaid 就能说清时。
|
|
45
45
|
- **写 `ui`**:读者需要拖、点、翻才能看懂时。
|
|
46
|
-
- **写 `
|
|
46
|
+
- **写 `garden html`**:目录表达不了的画布动画、音频、物理模拟等。按 [garden-interactive-chart](../garden-interactive-chart/SKILL.md) 在同名目录写 HTML,用 `title` 写下判断。
|
|
47
47
|
|
|
48
48
|
4. 写 `ui` 时:
|
|
49
49
|
- 原话原样放进块头的 `intent`,一个字都不改。`compiled` 不用写,`apply` 会写。
|
|
@@ -69,7 +69,7 @@ description: >-
|
|
|
69
69
|
````
|
|
70
70
|
|
|
71
71
|
- 没写 id 的 `intent` 块用 `--intent-hash <intentHash>` 定位;原话重复时改用 `--at <startLine>`。
|
|
72
|
-
- 新内容可以是一个 `ui` 围栏、一个 `
|
|
72
|
+
- 新内容可以是一个 `ui` 围栏、一个 `garden html` 围栏,或一段普通 Markdown(写回正文时)。
|
|
73
73
|
- 成功时 stdout 是 `{"ok":true,...}`。失败时 stderr 是 JSON,按 `code` 处理:
|
|
74
74
|
- `apply-invalid`:新内容没通过校验,按 `diagnostics` 改了再写。
|
|
75
75
|
- `apply-base`:块在扫描之后被改过。重新 `scan`,用新的 `hash`。
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { notFound } from "next/navigation";
|
|
3
|
+
import { ArticleMeasure } from "../../components/ArticleMeasure";
|
|
4
|
+
import { ArticleTitle } from "../../components/ArticleTitle";
|
|
5
|
+
import { MarkdownArticle } from "../../components/MarkdownArticle";
|
|
6
|
+
import { TableOfContents } from "../../components/TableOfContents";
|
|
7
|
+
import { a2WordCounts } from "@garden/generated-content";
|
|
8
|
+
import { demoDateLabel, demoDateTime } from "../../lib/a2-meta";
|
|
9
|
+
import { extractHeadings } from "../../lib/content";
|
|
10
|
+
import { a2Slugs, readA2Article } from "../../lib/a2-preview";
|
|
11
|
+
|
|
12
|
+
export function generateStaticParams() {
|
|
13
|
+
return a2Slugs().map((slug) => ({ slug }));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
|
17
|
+
const { slug } = await params;
|
|
18
|
+
const article = readA2Article(slug);
|
|
19
|
+
if (!article) return { robots: { index: false, follow: false } };
|
|
20
|
+
return { title: article.title, description: article.description, robots: { index: false, follow: false } };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default async function A2ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
|
|
24
|
+
const { slug } = await params;
|
|
25
|
+
const article = readA2Article(slug);
|
|
26
|
+
if (!article) notFound();
|
|
27
|
+
const headings = extractHeadings(article.content);
|
|
28
|
+
const wordCount = a2WordCounts[slug] ?? 0;
|
|
29
|
+
return (
|
|
30
|
+
<main className="reading-page post-reading-page">
|
|
31
|
+
<article className="post-article">
|
|
32
|
+
<header className="article-header article-summary-row hero">
|
|
33
|
+
<ArticleTitle title={article.title.replace(/(互动版)$/u, "")} />
|
|
34
|
+
<ul className="meta article-meta">
|
|
35
|
+
<li>Hedon</li>
|
|
36
|
+
{article.date ? <li><time dateTime={demoDateTime(article.date)}>{demoDateLabel(article.date)}</time></li> : null}
|
|
37
|
+
<li>约 {wordCount.toLocaleString("en-US")} 字</li>
|
|
38
|
+
{article.topic ? <li><span className="cat">{article.topic}</span></li> : null}
|
|
39
|
+
{article.original ? <li><a className="orig ext" href={article.original} target="_blank" rel="noopener noreferrer">原文 ↗</a></li> : null}
|
|
40
|
+
</ul>
|
|
41
|
+
{article.cover ? (
|
|
42
|
+
<figure className="article-hero article-cover cover-card cover-img">
|
|
43
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
44
|
+
<img src={article.cover} alt="" />
|
|
45
|
+
</figure>
|
|
46
|
+
) : null}
|
|
47
|
+
</header>
|
|
48
|
+
<div className="article-reading-layout">
|
|
49
|
+
<div className="article-reading-column">
|
|
50
|
+
<ArticleMeasure />
|
|
51
|
+
<div className="e-content">
|
|
52
|
+
{article.description ? <p className="lead quote-plain">{article.description}</p> : null}
|
|
53
|
+
<MarkdownArticle content={article.content} />
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<TableOfContents headings={headings} />
|
|
57
|
+
</div>
|
|
58
|
+
</article>
|
|
59
|
+
</main>
|
|
60
|
+
);
|
|
61
|
+
}
|
package/app/a2/page.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { a2Slugs, readA2Article } from "../lib/a2-preview";
|
|
3
|
+
|
|
4
|
+
export const metadata = {
|
|
5
|
+
title: "A2 对照",
|
|
6
|
+
robots: { index: false, follow: false },
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function A2Index() {
|
|
10
|
+
const articles = a2Slugs().map((slug) => readA2Article(slug)).filter((item) => item !== null);
|
|
11
|
+
return (
|
|
12
|
+
<main className="page-shell index-page">
|
|
13
|
+
<header className="sh-head">
|
|
14
|
+
<h1>A2 对照</h1>
|
|
15
|
+
<p className="sh-deck">用这套组件渲染参考文章,对照线上演示。正文没有改字。</p>
|
|
16
|
+
</header>
|
|
17
|
+
<ul className="sh-rows">
|
|
18
|
+
{articles.map((article) => (
|
|
19
|
+
<li className="sh-row" key={article.slug}>
|
|
20
|
+
<h3><Link href={`/a2/${article.slug}`}>{article.title}</Link></h3>
|
|
21
|
+
<p>{article.description}</p>
|
|
22
|
+
</li>
|
|
23
|
+
))}
|
|
24
|
+
</ul>
|
|
25
|
+
</main>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
postHref,
|
|
10
10
|
posts,
|
|
11
11
|
} from "../../lib/content";
|
|
12
|
+
import { ArticleMeasure } from "../../components/ArticleMeasure";
|
|
12
13
|
import { ArticleReferences } from "../../components/ArticleReferences";
|
|
14
|
+
import { ArticleTitle, originalUrl } from "../../components/ArticleTitle";
|
|
13
15
|
import { MarkdownArticle } from "../../components/MarkdownArticle";
|
|
14
16
|
import { TableOfContents } from "../../components/TableOfContents";
|
|
15
17
|
import { Comments } from "../../components/Comments";
|
|
@@ -87,32 +89,40 @@ export default async function PostPage({
|
|
|
87
89
|
>
|
|
88
90
|
{siteConfig.author.name}
|
|
89
91
|
</a>
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
</
|
|
92
|
+
<header className="article-header article-summary-row hero">
|
|
93
|
+
<ArticleTitle title={post.title} />
|
|
94
|
+
<ul className="meta article-meta">
|
|
95
|
+
<li>{siteConfig.author.name}</li>
|
|
96
|
+
<li>
|
|
97
|
+
<time className="dt-published" dateTime={post.date}>
|
|
98
|
+
{formatDate(post.date)}
|
|
99
|
+
</time>
|
|
100
|
+
</li>
|
|
101
|
+
{post.updated ? <li>更新于 {formatDate(post.updated)}</li> : null}
|
|
102
|
+
<li>约 {wordCount.toLocaleString("en-US")} 字</li>
|
|
103
|
+
{post.topic ? (
|
|
104
|
+
<li>
|
|
105
|
+
<a className="cat" href={`/blog?topic=${encodeURIComponent(post.topic)}`}>{post.topic}</a>
|
|
106
|
+
</li>
|
|
107
|
+
) : null}
|
|
108
|
+
{originalUrl(post) ? (
|
|
109
|
+
<li>
|
|
110
|
+
<a className="orig ext" href={originalUrl(post)} target="_blank" rel="noopener noreferrer">原文 ↗</a>
|
|
111
|
+
</li>
|
|
112
|
+
) : null}
|
|
113
|
+
</ul>
|
|
114
|
+
{post.cover && (
|
|
115
|
+
<figure className="article-hero article-cover cover-card cover-img">
|
|
116
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
117
|
+
<img className="u-photo" src={post.cover} alt={`${post.title} 封面`} />
|
|
118
|
+
</figure>
|
|
119
|
+
)}
|
|
112
120
|
</header>
|
|
113
121
|
<div className="article-reading-layout">
|
|
114
122
|
<div className="article-reading-column">
|
|
123
|
+
<ArticleMeasure />
|
|
115
124
|
<div className="e-content">
|
|
125
|
+
{post.description ? <p className="lead quote-plain p-summary">{post.description}</p> : null}
|
|
116
126
|
<MarkdownArticle content={post.content} />
|
|
117
127
|
</div>
|
|
118
128
|
<ArticleReferences references={post.references} />
|
package/app/blog/page.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Suspense } from "react";
|
|
2
2
|
import { siteConfig, siteDocumentTitle } from "../site.config";
|
|
3
3
|
import { BlogArchive } from "../components/BlogArchive";
|
|
4
|
-
import {
|
|
4
|
+
import { posts } from "../lib/content";
|
|
5
5
|
|
|
6
6
|
export const metadata = {
|
|
7
7
|
title: siteDocumentTitle,
|
|
@@ -10,12 +10,11 @@ export const metadata = {
|
|
|
10
10
|
|
|
11
11
|
export default function BlogIndex() {
|
|
12
12
|
return (
|
|
13
|
-
<main className="
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/>
|
|
13
|
+
<main className="sh-main" id="content">
|
|
14
|
+
<div className="sh-head">
|
|
15
|
+
<h1 id="sh-title">文章</h1>
|
|
16
|
+
<p className="sh-deck">阶段性的完整沉淀,关于工程、架构与技术思考。共 {posts.length} 篇,按年份归档。</p>
|
|
17
|
+
</div>
|
|
19
18
|
<Suspense>
|
|
20
19
|
<BlogArchive />
|
|
21
20
|
</Suspense>
|
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
columnHref,
|
|
11
11
|
columnEntryPath,
|
|
12
12
|
} from "../../../lib/content";
|
|
13
|
+
import { ArticleMeasure } from "../../../components/ArticleMeasure";
|
|
13
14
|
import { ArticleReferences } from "../../../components/ArticleReferences";
|
|
15
|
+
import { ArticleTitle, originalUrl } from "../../../components/ArticleTitle";
|
|
14
16
|
import { MarkdownArticle } from "../../../components/MarkdownArticle";
|
|
15
17
|
import { TableOfContents } from "../../../components/TableOfContents";
|
|
16
18
|
import { Comments } from "../../../components/Comments";
|
|
@@ -73,6 +75,7 @@ export default async function ColumnEntryPage({
|
|
|
73
75
|
const entry = findColumnEntry(column, path.join("/"));
|
|
74
76
|
if (!entry) notFound();
|
|
75
77
|
const series = columns.filter((item) => item.column === column);
|
|
78
|
+
const listed = [...series].sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
|
|
76
79
|
const currentIndex = series.findIndex((item) => item.path === entry.path);
|
|
77
80
|
const next = series[currentIndex + 1];
|
|
78
81
|
const headings = extractHeadings(entry.content);
|
|
@@ -90,7 +93,7 @@ export default async function ColumnEntryPage({
|
|
|
90
93
|
<p>主题专栏</p>
|
|
91
94
|
<h2>{entry.columnTitle}</h2>
|
|
92
95
|
<ol>
|
|
93
|
-
{
|
|
96
|
+
{listed.map((item) => (
|
|
94
97
|
<li key={item.path} className={item.path === entry.path ? "active" : ""}>
|
|
95
98
|
<Link href={columnHref(item)}>{item.title}</Link>
|
|
96
99
|
</li>
|
|
@@ -110,32 +113,36 @@ export default async function ColumnEntryPage({
|
|
|
110
113
|
>
|
|
111
114
|
{siteConfig.author.name}
|
|
112
115
|
</a>
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
{entry
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
116
|
+
<header className="article-header hero">
|
|
117
|
+
<ArticleTitle title={entry.title.replace(/^\d+\s*·\s*/u, "")} />
|
|
118
|
+
<ul className="meta article-meta">
|
|
119
|
+
<li>{siteConfig.author.name}</li>
|
|
120
|
+
<li>
|
|
121
|
+
<time className="dt-published" dateTime={entry.date}>
|
|
122
|
+
{formatDate(entry.date)}
|
|
123
|
+
</time>
|
|
124
|
+
</li>
|
|
125
|
+
{entry.topic && entry.topic !== entry.columnTitle ? (
|
|
126
|
+
<li><a className="cat" href={`/blog?topic=${encodeURIComponent(entry.topic)}`}>{entry.topic}</a></li>
|
|
127
|
+
) : null}
|
|
128
|
+
{originalUrl(entry) ? (
|
|
129
|
+
<li><a className="orig ext" href={originalUrl(entry)} target="_blank" rel="noopener noreferrer">原文 ↗</a></li>
|
|
130
|
+
) : null}
|
|
131
|
+
</ul>
|
|
132
|
+
{entry.cover && (
|
|
133
|
+
<figure className="article-hero article-cover cover-img">
|
|
134
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
135
|
+
<img
|
|
136
|
+
className="u-photo"
|
|
137
|
+
src={entry.cover}
|
|
138
|
+
alt={`${entry.title} 封面`}
|
|
139
|
+
/>
|
|
140
|
+
</figure>
|
|
141
|
+
)}
|
|
137
142
|
</header>
|
|
143
|
+
<ArticleMeasure />
|
|
138
144
|
<div className="e-content">
|
|
145
|
+
{entry.description ? <p className="lead p-summary">{entry.description}</p> : null}
|
|
139
146
|
<MarkdownArticle content={entry.content} />
|
|
140
147
|
</div>
|
|
141
148
|
<ArticleReferences references={entry.references} />
|
package/app/columns/page.tsx
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
/* eslint-disable @next/next/no-img-element -- static GitHub Pages assets must keep their exact paths. */
|
|
2
2
|
import Link from "next/link";
|
|
3
|
-
import { ArrowRight } from "@phosphor-icons/react/ssr";
|
|
4
3
|
import { columnHref, columns } from "../lib/content";
|
|
5
|
-
import { PageIntro } from "../components/PageIntro";
|
|
6
4
|
import { ContentEmptyState } from "../components/ContentEmptyState";
|
|
7
5
|
import { siteConfig, siteDocumentTitle } from "../site.config";
|
|
8
6
|
|
|
9
7
|
export const metadata = {
|
|
10
8
|
title: siteDocumentTitle,
|
|
11
|
-
description:
|
|
9
|
+
description: "专栏是按主题整理的阅读清单,与文章分类相互独立;清单内按发布时间从新到旧排列。",
|
|
12
10
|
};
|
|
13
11
|
|
|
12
|
+
function day(value: string) {
|
|
13
|
+
const date = new Date(value);
|
|
14
|
+
if (Number.isNaN(date.getTime())) return value.slice(0, 10);
|
|
15
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
16
|
+
timeZone: "Asia/Shanghai",
|
|
17
|
+
year: "numeric",
|
|
18
|
+
month: "2-digit",
|
|
19
|
+
day: "2-digit",
|
|
20
|
+
}).format(date);
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
export default function ColumnsPage() {
|
|
15
24
|
const grouped = Array.from(
|
|
16
25
|
columns.reduce((result, entry) => {
|
|
@@ -21,59 +30,36 @@ export default function ColumnsPage() {
|
|
|
21
30
|
}, new Map<string, typeof columns>()),
|
|
22
31
|
).map(([slug, entries]) => ({
|
|
23
32
|
slug,
|
|
24
|
-
entries,
|
|
25
33
|
first: entries[0],
|
|
34
|
+
entries: [...entries].sort((left, right) => Date.parse(right.date) - Date.parse(left.date)),
|
|
26
35
|
}));
|
|
27
36
|
|
|
28
37
|
return (
|
|
29
|
-
<main className="
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{first.columnDescription ??
|
|
55
|
-
first.description ??
|
|
56
|
-
"按顺序组织的专题文章系列。"}
|
|
57
|
-
</p>
|
|
58
|
-
<strong>
|
|
59
|
-
从第一篇开始
|
|
60
|
-
<ArrowRight size={17} />
|
|
61
|
-
</strong>
|
|
62
|
-
</div>
|
|
63
|
-
<div className="column-feature-index">
|
|
64
|
-
{cover && (
|
|
65
|
-
<img className="column-feature-cover" src={cover.src} alt={cover.alt} />
|
|
66
|
-
)}
|
|
67
|
-
<div className="column-feature-index-label">
|
|
68
|
-
<span>主题专栏</span>
|
|
69
|
-
<strong>{String(index + 1).padStart(2, "0")}</strong>
|
|
70
|
-
<small>{slug.replaceAll("-", " ").toUpperCase()}</small>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
</Link>
|
|
74
|
-
);
|
|
75
|
-
})}
|
|
76
|
-
</div> : <ContentEmptyState kind="columns" />}
|
|
38
|
+
<main className="sh-main" id="content">
|
|
39
|
+
<div className="sh-head">
|
|
40
|
+
<h1>{siteConfig.pages.columns.title || "专栏"}</h1>
|
|
41
|
+
<p className="sh-deck">专栏是按主题整理的阅读清单,与文章分类相互独立;清单内按发布时间从新到旧排列。</p>
|
|
42
|
+
</div>
|
|
43
|
+
{grouped.length ? grouped.map(({ slug, first, entries }) => (
|
|
44
|
+
<section className="sh-series" aria-labelledby={slug} key={slug}>
|
|
45
|
+
<div className="cover" style={{ background: "var(--well)" }}>
|
|
46
|
+
{first.columnCover ? <img className="column-feature-cover" src={first.columnCover} alt={first.columnCoverAlt ?? `${first.columnTitle} 封面`} /> : null}
|
|
47
|
+
</div>
|
|
48
|
+
<div className="body">
|
|
49
|
+
<h2 id={slug}>{first.columnTitle}</h2>
|
|
50
|
+
<p className="count">阅读清单,{entries.length} 篇,{first.columnStatus ?? "持续更新"}</p>
|
|
51
|
+
<p className="desc">{first.columnDescription ?? first.description ?? "按顺序组织的专题文章系列。"}</p>
|
|
52
|
+
<ol aria-label={`${first.columnTitle},从新到旧`}>
|
|
53
|
+
{entries.map((entry) => (
|
|
54
|
+
<li key={entry.path}>
|
|
55
|
+
<Link href={columnHref(entry)}>{entry.title}</Link>
|
|
56
|
+
<time dateTime={day(entry.date)}>{day(entry.date)}</time>
|
|
57
|
+
</li>
|
|
58
|
+
))}
|
|
59
|
+
</ol>
|
|
60
|
+
</div>
|
|
61
|
+
</section>
|
|
62
|
+
)) : <ContentEmptyState kind="columns" />}
|
|
77
63
|
</main>
|
|
78
64
|
);
|
|
79
65
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { tableBreakout, tocShift } from "../../ui/garden/enhance.mjs";
|
|
5
|
+
|
|
6
|
+
function translatedX(element: Element) {
|
|
7
|
+
const value = new DOMMatrix(getComputedStyle(element).transform).m41;
|
|
8
|
+
return Number.isFinite(value) ? value : 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ArticleMeasure() {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const article = document.querySelector("article");
|
|
14
|
+
const toc = document.querySelector<HTMLElement>(".toc");
|
|
15
|
+
if (!article) return undefined;
|
|
16
|
+
const root = document.documentElement;
|
|
17
|
+
let frame = 0;
|
|
18
|
+
|
|
19
|
+
const layout = () => {
|
|
20
|
+
const articleBox = article.getBoundingClientRect();
|
|
21
|
+
const viewport = root.clientWidth;
|
|
22
|
+
article.querySelectorAll<HTMLElement>(".table-wrap").forEach((wrap) => {
|
|
23
|
+
const table = wrap.querySelector("table");
|
|
24
|
+
wrap.classList.remove("ux-wide");
|
|
25
|
+
wrap.style.removeProperty("--wl");
|
|
26
|
+
wrap.style.removeProperty("--wr");
|
|
27
|
+
if (!table) return;
|
|
28
|
+
const margin = tableBreakout({
|
|
29
|
+
viewport,
|
|
30
|
+
articleLeft: articleBox.left,
|
|
31
|
+
articleRight: articleBox.right,
|
|
32
|
+
tableClient: wrap.clientWidth,
|
|
33
|
+
tableScroll: table.scrollWidth,
|
|
34
|
+
});
|
|
35
|
+
if (margin.left > 0) {
|
|
36
|
+
wrap.style.setProperty("--wl", `${margin.left}px`);
|
|
37
|
+
wrap.style.setProperty("--wr", `${margin.right}px`);
|
|
38
|
+
wrap.classList.add("ux-wide");
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!toc) {
|
|
43
|
+
root.removeAttribute("data-toc-yield");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const box = toc.getBoundingClientRect();
|
|
47
|
+
const dx = translatedX(toc);
|
|
48
|
+
const originalLeft = box.left - dx;
|
|
49
|
+
const overlapRights: number[] = [];
|
|
50
|
+
article.querySelectorAll<HTMLElement>(".ux-wide").forEach((element) => {
|
|
51
|
+
const wide = element.getBoundingClientRect();
|
|
52
|
+
const overlaps = wide.bottom > box.top - 12 && wide.top < box.bottom + 12 && wide.right > originalLeft - 12;
|
|
53
|
+
if (overlaps) overlapRights.push(wide.right);
|
|
54
|
+
});
|
|
55
|
+
const shift = tocShift({
|
|
56
|
+
tocLeft: originalLeft,
|
|
57
|
+
tocWidth: box.width,
|
|
58
|
+
viewport,
|
|
59
|
+
overlapRights,
|
|
60
|
+
});
|
|
61
|
+
if (shift.shift > 0) {
|
|
62
|
+
root.style.setProperty("--tocshift", `${shift.shift}px`);
|
|
63
|
+
root.toggleAttribute("data-toc-hide", shift.hide);
|
|
64
|
+
root.setAttribute("data-toc-yield", "");
|
|
65
|
+
} else {
|
|
66
|
+
root.removeAttribute("data-toc-yield");
|
|
67
|
+
root.removeAttribute("data-toc-hide");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const schedule = () => {
|
|
72
|
+
if (frame) return;
|
|
73
|
+
frame = window.requestAnimationFrame(() => {
|
|
74
|
+
frame = 0;
|
|
75
|
+
layout();
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
layout();
|
|
80
|
+
window.addEventListener("resize", schedule);
|
|
81
|
+
window.addEventListener("scroll", schedule, { passive: true });
|
|
82
|
+
window.addEventListener("load", schedule);
|
|
83
|
+
const observer = new ResizeObserver(schedule);
|
|
84
|
+
observer.observe(article);
|
|
85
|
+
article.querySelectorAll("img").forEach((image) => image.addEventListener("load", schedule));
|
|
86
|
+
return () => {
|
|
87
|
+
window.removeEventListener("resize", schedule);
|
|
88
|
+
window.removeEventListener("scroll", schedule);
|
|
89
|
+
window.removeEventListener("load", schedule);
|
|
90
|
+
observer.disconnect();
|
|
91
|
+
if (frame) window.cancelAnimationFrame(frame);
|
|
92
|
+
};
|
|
93
|
+
}, []);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { splitTitle, titleLineScale } from "../../ui/garden/title.mjs";
|
|
2
|
+
|
|
3
|
+
export function ArticleTitle({ title, className = "" }: { title: string; className?: string }) {
|
|
4
|
+
const split = splitTitle(title);
|
|
5
|
+
if (!split.split) {
|
|
6
|
+
return <h1 className={`p-name${className ? ` ${className}` : ""}`}>{title}</h1>;
|
|
7
|
+
}
|
|
8
|
+
const scale = titleLineScale(split.lines);
|
|
9
|
+
return (
|
|
10
|
+
<h1 className={`p-name h1-br${className ? ` ${className}` : ""}`} style={{ ["--l" as string]: scale }}>
|
|
11
|
+
<span className="h1-line">{split.lines[0]}</span>
|
|
12
|
+
<span className="vh">{split.separator}</span>
|
|
13
|
+
<span className="h1-line">{split.lines[1]}</span>
|
|
14
|
+
</h1>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function originalUrl(entry: { original?: unknown }) {
|
|
19
|
+
return typeof entry.original === "string" && /^https?:\/\//iu.test(entry.original) ? entry.original : "";
|
|
20
|
+
}
|