hd-garden 0.3.13 → 0.4.0
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/app/a2/[slug]/page.tsx +61 -0
- package/app/a2/page.tsx +27 -0
- package/app/blog/[...path]/page.tsx +33 -22
- package/app/blog/page.tsx +6 -7
- package/app/columns/[column]/[...path]/page.tsx +33 -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 +48 -5
- package/app/lib/markdown-plugins.ts +2 -0
- package/app/lib/remark-a2.mjs +208 -0
- package/app/lib/remark-garden.mjs +88 -0
- package/app/page.tsx +122 -93
- package/app/styles/a2.css +1383 -0
- package/app/styles/shell.css +438 -0
- package/app/styles/tokens.css +111 -0
- package/app/tags/page.tsx +28 -0
- package/app/ui-parity/page.tsx +3 -2
- package/package.json +1 -1
- package/scripts/build-content.mjs +11 -1
- package/scripts/check-post.mjs +18 -4
- package/scripts/new-post.mjs +3 -0
- package/site.config.yaml +1 -1
- package/templates/garden-blocks.md +128 -0
- package/templates/garden-snippets.plist +121 -0
- package/ui/garden/catalog.mjs +196 -0
- package/ui/garden/check.mjs +103 -0
- package/ui/garden/enhance.mjs +89 -0
- package/ui/garden/parse.mjs +198 -0
- package/ui/garden/snippets.mjs +109 -0
- package/ui/garden/title.mjs +37 -0
- package/ui/garden/word-count.mjs +266 -0
- package/ui/host.mjs +45 -28
- package/ui/parity-fixtures.mjs +1 -1
- package/ui/preview/dist/garden-ui.css +81 -77
- package/ui/preview/dist/garden-ui.esm.js +124 -89
- package/ui/preview/dist/garden-ui.iife.js +124 -89
- package/ui/prompt/rules.md +17 -1
- package/ui/react/CatalogPage.tsx +12 -0
- package/ui/react/components/display.tsx +1 -1
- package/ui/react/components/media.tsx +2 -2
- package/ui/react/ui.css +81 -77
|
@@ -0,0 +1,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,41 @@ 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>{post.readingTime}</li>
|
|
103
|
+
<li>约 {wordCount.toLocaleString("en-US")} 字</li>
|
|
104
|
+
{post.topic ? (
|
|
105
|
+
<li>
|
|
106
|
+
<a className="cat" href={`/blog?topic=${encodeURIComponent(post.topic)}`}>{post.topic}</a>
|
|
107
|
+
</li>
|
|
108
|
+
) : null}
|
|
109
|
+
{originalUrl(post) ? (
|
|
110
|
+
<li>
|
|
111
|
+
<a className="orig ext" href={originalUrl(post)} target="_blank" rel="noopener noreferrer">原文 ↗</a>
|
|
112
|
+
</li>
|
|
113
|
+
) : null}
|
|
114
|
+
</ul>
|
|
115
|
+
{post.cover && (
|
|
116
|
+
<figure className="article-hero article-cover cover-card cover-img">
|
|
117
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
118
|
+
<img className="u-photo" src={post.cover} alt={`${post.title} 封面`} />
|
|
119
|
+
</figure>
|
|
120
|
+
)}
|
|
112
121
|
</header>
|
|
113
122
|
<div className="article-reading-layout">
|
|
114
123
|
<div className="article-reading-column">
|
|
124
|
+
<ArticleMeasure />
|
|
115
125
|
<div className="e-content">
|
|
126
|
+
{post.description ? <p className="lead quote-plain p-summary">{post.description}</p> : null}
|
|
116
127
|
<MarkdownArticle content={post.content} />
|
|
117
128
|
</div>
|
|
118
129
|
<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,37 @@ 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
|
-
|
|
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
|
+
<li>{entry.readingTime}</li>
|
|
126
|
+
{entry.topic && entry.topic !== entry.columnTitle ? (
|
|
127
|
+
<li><a className="cat" href={`/blog?topic=${encodeURIComponent(entry.topic)}`}>{entry.topic}</a></li>
|
|
128
|
+
) : null}
|
|
129
|
+
{originalUrl(entry) ? (
|
|
130
|
+
<li><a className="orig ext" href={originalUrl(entry)} target="_blank" rel="noopener noreferrer">原文 ↗</a></li>
|
|
131
|
+
) : null}
|
|
132
|
+
</ul>
|
|
133
|
+
{entry.cover && (
|
|
134
|
+
<figure className="article-hero article-cover cover-img">
|
|
135
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
136
|
+
<img
|
|
137
|
+
className="u-photo"
|
|
138
|
+
src={entry.cover}
|
|
139
|
+
alt={`${entry.title} 封面`}
|
|
140
|
+
/>
|
|
141
|
+
</figure>
|
|
142
|
+
)}
|
|
137
143
|
</header>
|
|
144
|
+
<ArticleMeasure />
|
|
138
145
|
<div className="e-content">
|
|
146
|
+
{entry.description ? <p className="lead p-summary">{entry.description}</p> : null}
|
|
139
147
|
<MarkdownArticle content={entry.content} />
|
|
140
148
|
</div>
|
|
141
149
|
<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
|
+
}
|