hd-garden 0.10.0 → 0.10.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/app/about/page.tsx +1 -1
- package/app/blog/[...path]/page.tsx +9 -78
- package/app/blog/page.tsx +5 -4
- package/app/columns/[column]/[...path]/page.tsx +29 -91
- package/app/columns/page.tsx +6 -4
- package/app/components/PageIntro.tsx +9 -4
- package/app/components/PostReadingView.tsx +133 -0
- package/app/components/SeriesEdgeNav.tsx +119 -0
- package/app/lib/i18n.ts +6 -2
- package/app/page.tsx +3 -1
- package/app/styles/a2.css +204 -0
- package/app/thoughts/page/[n]/page.tsx +1 -1
- package/app/thoughts/page.tsx +1 -1
- package/package.json +1 -1
- package/ui/garden/site-nav.mjs +3 -1
package/app/about/page.tsx
CHANGED
|
@@ -1,34 +1,18 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { CoverImage } from "../../components/CoverImage";
|
|
3
1
|
import type { Metadata } from "next";
|
|
4
2
|
import { FEED_ALTERNATES } from "../../lib/page-alternates";
|
|
5
3
|
import { notFound } from "next/navigation";
|
|
6
4
|
import {
|
|
7
|
-
extractHeadings,
|
|
8
|
-
estimateWordCount,
|
|
9
5
|
findPost,
|
|
10
|
-
formatDate,
|
|
11
6
|
postHref,
|
|
12
7
|
posts,
|
|
13
8
|
} from "../../lib/content";
|
|
14
|
-
import {
|
|
15
|
-
import { ArticleReferences } from "../../components/ArticleReferences";
|
|
16
|
-
import { ArticleTitle, originalUrl } from "../../components/ArticleTitle";
|
|
17
|
-
import { MarkdownArticle } from "../../components/MarkdownArticle";
|
|
18
|
-
import { TableOfContents } from "../../components/TableOfContents";
|
|
19
|
-
import { Comments } from "../../components/Comments";
|
|
20
|
-
import { Webmentions } from "../../components/Webmentions";
|
|
21
|
-
import { ArticleTags } from "../../components/ArticleTags";
|
|
22
|
-
import { ArticlePager } from "../../components/ArticlePager";
|
|
9
|
+
import { PostReadingView } from "../../components/PostReadingView";
|
|
23
10
|
import { adjacentByDate } from "../../../ui/garden/adjacent.mjs";
|
|
24
11
|
import {
|
|
25
12
|
absoluteUrl,
|
|
26
13
|
getSiteUrl,
|
|
27
|
-
webmentionApiEndpoint,
|
|
28
14
|
} from "../../lib/site";
|
|
29
|
-
import { githubUrl, siteConfig } from "../../site.config";
|
|
30
15
|
import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../ui/garden/static-params.mjs";
|
|
31
|
-
import { siteLang, ui } from "../../lib/i18n";
|
|
32
16
|
|
|
33
17
|
export function generateStaticParams() {
|
|
34
18
|
return staticParams(posts.map((post) => ({ path: post.path.split("/") })), { path: [STATIC_PLACEHOLDER] });
|
|
@@ -80,72 +64,19 @@ export default async function PostPage({
|
|
|
80
64
|
if (isStaticPlaceholder(path)) return null;
|
|
81
65
|
notFound();
|
|
82
66
|
}
|
|
83
|
-
const headings = extractHeadings(post.content, { numbering: post.numbering });
|
|
84
67
|
const baseUrl = await getSiteUrl();
|
|
85
68
|
const pathname = postHref(post);
|
|
86
69
|
const canonicalUrl = absoluteUrl(pathname, baseUrl);
|
|
87
|
-
const wordCount = estimateWordCount(post.content);
|
|
88
70
|
const { prev, next } = adjacentByDate(posts, (item: { path: string }) => item.path === post.path);
|
|
89
|
-
const t = ui();
|
|
90
71
|
|
|
91
72
|
return (
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
rel="author"
|
|
101
|
-
>
|
|
102
|
-
{siteConfig.author.name}
|
|
103
|
-
</a>
|
|
104
|
-
<header className="article-header article-summary-row hero">
|
|
105
|
-
<ArticleTitle title={post.title} />
|
|
106
|
-
<ul className="meta article-meta">
|
|
107
|
-
<li>{siteConfig.author.name}</li>
|
|
108
|
-
<li>
|
|
109
|
-
<time className="dt-published" dateTime={post.date}>
|
|
110
|
-
{formatDate(post.date)}
|
|
111
|
-
</time>
|
|
112
|
-
</li>
|
|
113
|
-
{post.updated ? <li>{siteLang === "en" ? t.updatedOn(formatDate(post.updated)) : <>更新于 {formatDate(post.updated)}</>}</li> : null}
|
|
114
|
-
<li>{t.wordCount(wordCount)}</li>
|
|
115
|
-
{originalUrl(post) ? (
|
|
116
|
-
<li>
|
|
117
|
-
<a className="orig ext" href={originalUrl(post)} target="_blank" rel="noopener noreferrer">{t.originalLink}</a>
|
|
118
|
-
</li>
|
|
119
|
-
) : null}
|
|
120
|
-
</ul>
|
|
121
|
-
<ArticleTags tags={post.tags} />
|
|
122
|
-
{post.cover && (
|
|
123
|
-
<figure className="article-hero article-cover cover-card cover-img">
|
|
124
|
-
<CoverImage className="u-photo" src={post.cover} image={post.coverImage} alt={t.coverLabel(post.title)} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
|
|
125
|
-
</figure>
|
|
126
|
-
)}
|
|
127
|
-
</header>
|
|
128
|
-
<div className="article-reading-layout">
|
|
129
|
-
<div className="article-reading-column">
|
|
130
|
-
<ArticleMeasure />
|
|
131
|
-
<div className="e-content">
|
|
132
|
-
{post.description ? <p className="lead quote-plain p-summary">{post.description}</p> : null}
|
|
133
|
-
<MarkdownArticle content={post.content} numbering={post.numbering} />
|
|
134
|
-
</div>
|
|
135
|
-
<ArticleReferences references={post.references} />
|
|
136
|
-
<Webmentions
|
|
137
|
-
target={canonicalUrl}
|
|
138
|
-
endpoint={webmentionApiEndpoint()}
|
|
139
|
-
/>
|
|
140
|
-
<ArticlePager
|
|
141
|
-
prev={prev ? { href: postHref(prev), title: prev.title } : null}
|
|
142
|
-
next={next ? { href: postHref(next), title: next.title } : null}
|
|
143
|
-
/>
|
|
144
|
-
<Comments slug={post.path} />
|
|
145
|
-
</div>
|
|
146
|
-
<TableOfContents headings={headings} />
|
|
147
|
-
</div>
|
|
148
|
-
</article>
|
|
149
|
-
</main>
|
|
73
|
+
<PostReadingView
|
|
74
|
+
post={post}
|
|
75
|
+
pathname={pathname}
|
|
76
|
+
canonicalUrl={canonicalUrl}
|
|
77
|
+
commentsSlug={post.path}
|
|
78
|
+
prev={prev ? { href: postHref(prev), title: prev.title } : null}
|
|
79
|
+
next={next ? { href: postHref(next), title: next.title } : null}
|
|
80
|
+
/>
|
|
150
81
|
);
|
|
151
82
|
}
|
package/app/blog/page.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Suspense } from "react";
|
|
2
2
|
import { siteConfig, siteDocumentTitle } from "../site.config";
|
|
3
3
|
import { BlogArchive, BlogArchiveFallback } from "../components/BlogArchive";
|
|
4
|
+
import { PageIntro } from "../components/PageIntro";
|
|
4
5
|
import { posts } from "../lib/content";
|
|
5
6
|
import { archivePosts, archiveSeries } from "../lib/archive";
|
|
6
7
|
import { pageAlternates } from "../lib/page-alternates";
|
|
@@ -16,12 +17,12 @@ export default function BlogIndex() {
|
|
|
16
17
|
const t = ui();
|
|
17
18
|
const list = archivePosts();
|
|
18
19
|
const series = archiveSeries();
|
|
20
|
+
const deck =
|
|
21
|
+
siteConfig.pages.blog.subtitle?.trim() ||
|
|
22
|
+
(siteLang === "en" ? t.blogDeck(posts.length) : `阶段性的完整沉淀,关于工程、架构与技术思考。共 ${posts.length} 篇,按年份归档。`);
|
|
19
23
|
return (
|
|
20
24
|
<main className="sh-main" id="content">
|
|
21
|
-
<
|
|
22
|
-
<h1 id="sh-title">{t.blogHeading}</h1>
|
|
23
|
-
<p className="sh-deck">{siteLang === "en" ? t.blogDeck(posts.length) : <>阶段性的完整沉淀,关于工程、架构与技术思考。共 {posts.length} 篇,按年份归档。</>}</p>
|
|
24
|
-
</div>
|
|
25
|
+
<PageIntro eyebrow={t.blogEyebrow} title={t.blogHeading} subtitle={deck} />
|
|
25
26
|
<Suspense fallback={<BlogArchiveFallback posts={list} series={series} />}>
|
|
26
27
|
<BlogArchive posts={list} series={series} />
|
|
27
28
|
</Suspense>
|
|
@@ -1,35 +1,21 @@
|
|
|
1
|
-
import Link from "next/link";
|
|
2
|
-
import { ArticlePager } from "../../../components/ArticlePager";
|
|
3
|
-
import { CoverImage } from "../../../components/CoverImage";
|
|
4
1
|
import type { Metadata } from "next";
|
|
5
2
|
import { FEED_ALTERNATES } from "../../../lib/page-alternates";
|
|
6
3
|
import { notFound } from "next/navigation";
|
|
7
|
-
import { ArrowLeft } from "@phosphor-icons/react/ssr";
|
|
8
4
|
import {
|
|
9
5
|
columns,
|
|
10
|
-
extractHeadings,
|
|
11
6
|
findColumnEntry,
|
|
12
|
-
formatDate,
|
|
13
7
|
columnHref,
|
|
14
8
|
columnEntryPath,
|
|
15
9
|
} from "../../../lib/content";
|
|
16
|
-
import {
|
|
17
|
-
import { ArticleReferences } from "../../../components/ArticleReferences";
|
|
18
|
-
import { ArticleTitle, originalUrl } from "../../../components/ArticleTitle";
|
|
19
|
-
import { MarkdownArticle } from "../../../components/MarkdownArticle";
|
|
20
|
-
import { TableOfContents } from "../../../components/TableOfContents";
|
|
21
|
-
import { Comments } from "../../../components/Comments";
|
|
22
|
-
import { Webmentions } from "../../../components/Webmentions";
|
|
23
|
-
import { ArticleTags } from "../../../components/ArticleTags";
|
|
10
|
+
import { PostReadingView } from "../../../components/PostReadingView";
|
|
24
11
|
import {
|
|
25
12
|
absoluteUrl,
|
|
26
13
|
getSiteUrl,
|
|
27
|
-
webmentionApiEndpoint,
|
|
28
14
|
} from "../../../lib/site";
|
|
29
|
-
import { githubUrl, siteConfig } from "../../../site.config";
|
|
30
|
-
import { siteHref } from "../../../lib/site-href";
|
|
31
15
|
import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../../ui/garden/static-params.mjs";
|
|
32
|
-
import { columnStatusLabel,
|
|
16
|
+
import { columnStatusLabel, ui } from "../../../lib/i18n";
|
|
17
|
+
|
|
18
|
+
const stripColumnIndex = (title: string) => title.replace(/^\d+\s*·\s*/u, "");
|
|
33
19
|
|
|
34
20
|
export function generateStaticParams() {
|
|
35
21
|
return staticParams(
|
|
@@ -51,14 +37,15 @@ export async function generateMetadata({
|
|
|
51
37
|
if (!entry) return {};
|
|
52
38
|
const baseUrl = await getSiteUrl();
|
|
53
39
|
const pathname = columnHref(entry);
|
|
40
|
+
const title = stripColumnIndex(entry.title);
|
|
54
41
|
return {
|
|
55
|
-
title
|
|
42
|
+
title,
|
|
56
43
|
description: entry.description,
|
|
57
44
|
alternates: { canonical: pathname, types: FEED_ALTERNATES },
|
|
58
45
|
openGraph: {
|
|
59
46
|
type: "article",
|
|
60
47
|
url: absoluteUrl(pathname, baseUrl),
|
|
61
|
-
title
|
|
48
|
+
title,
|
|
62
49
|
description: entry.description,
|
|
63
50
|
publishedTime: new Date(entry.date).toISOString(),
|
|
64
51
|
modifiedTime: entry.updated
|
|
@@ -69,7 +56,7 @@ export async function generateMetadata({
|
|
|
69
56
|
},
|
|
70
57
|
twitter: {
|
|
71
58
|
card: "summary_large_image",
|
|
72
|
-
title
|
|
59
|
+
title,
|
|
73
60
|
description: entry.description,
|
|
74
61
|
images: entry.cover ? [entry.cover] : ["/og.png"],
|
|
75
62
|
},
|
|
@@ -92,81 +79,32 @@ export default async function ColumnEntryPage({
|
|
|
92
79
|
const currentIndex = series.findIndex((item) => item.path === entry.path);
|
|
93
80
|
const prev = series[currentIndex - 1] ?? null;
|
|
94
81
|
const next = series[currentIndex + 1] ?? null;
|
|
95
|
-
const headings = extractHeadings(entry.content, { numbering: entry.numbering });
|
|
96
82
|
const baseUrl = await getSiteUrl();
|
|
97
83
|
const pathname = columnHref(entry);
|
|
98
84
|
const canonicalUrl = absoluteUrl(pathname, baseUrl);
|
|
99
85
|
const t = ui();
|
|
86
|
+
const displayTitle = stripColumnIndex(entry.title);
|
|
100
87
|
|
|
101
88
|
return (
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{siteLang === "en" ? t.permalinkLabel(entry.title) : <>{entry.title} 永久链接</>}
|
|
123
|
-
</Link>
|
|
124
|
-
<a
|
|
125
|
-
className="p-author h-card sr-only"
|
|
126
|
-
href={githubUrl}
|
|
127
|
-
rel="author"
|
|
128
|
-
>
|
|
129
|
-
{siteConfig.author.name}
|
|
130
|
-
</a>
|
|
131
|
-
<header className="article-header hero">
|
|
132
|
-
<ArticleTitle title={entry.title.replace(/^\d+\s*·\s*/u, "")} />
|
|
133
|
-
<ul className="meta article-meta">
|
|
134
|
-
<li>{siteConfig.author.name}</li>
|
|
135
|
-
<li>
|
|
136
|
-
<time className="dt-published" dateTime={entry.date}>
|
|
137
|
-
{formatDate(entry.date)}
|
|
138
|
-
</time>
|
|
139
|
-
</li>
|
|
140
|
-
{originalUrl(entry) ? (
|
|
141
|
-
<li><a className="orig ext" href={originalUrl(entry)} target="_blank" rel="noopener noreferrer">{t.originalLink}</a></li>
|
|
142
|
-
) : null}
|
|
143
|
-
</ul>
|
|
144
|
-
<ArticleTags tags={entry.tags} />
|
|
145
|
-
{entry.cover && (
|
|
146
|
-
<figure className="article-hero article-cover cover-img">
|
|
147
|
-
<CoverImage className="u-photo" src={entry.cover} image={entry.coverImage} alt={siteLang === "en" ? t.coverLabel(entry.title) : `${entry.title} 封面`} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
|
|
148
|
-
</figure>
|
|
149
|
-
)}
|
|
150
|
-
</header>
|
|
151
|
-
<ArticleMeasure />
|
|
152
|
-
<div className="e-content">
|
|
153
|
-
{entry.description ? <p className="lead p-summary">{entry.description}</p> : null}
|
|
154
|
-
<MarkdownArticle content={entry.content} numbering={entry.numbering} />
|
|
155
|
-
</div>
|
|
156
|
-
<ArticleReferences references={entry.references} />
|
|
157
|
-
<Webmentions
|
|
158
|
-
target={canonicalUrl}
|
|
159
|
-
endpoint={webmentionApiEndpoint()}
|
|
160
|
-
/>
|
|
161
|
-
<ArticlePager
|
|
162
|
-
label={ui().columnPagerLabel}
|
|
163
|
-
prev={prev ? { href: columnHref(prev), title: prev.title } : null}
|
|
164
|
-
next={next ? { href: columnHref(next), title: next.title } : null}
|
|
165
|
-
/>
|
|
166
|
-
<Comments slug={`${column}:${entry.path}`} />
|
|
167
|
-
</article>
|
|
168
|
-
|
|
169
|
-
<TableOfContents headings={headings} />
|
|
170
|
-
</main>
|
|
89
|
+
<PostReadingView
|
|
90
|
+
post={entry}
|
|
91
|
+
pathname={pathname}
|
|
92
|
+
canonicalUrl={canonicalUrl}
|
|
93
|
+
title={displayTitle}
|
|
94
|
+
commentsSlug={`${column}:${entry.path}`}
|
|
95
|
+
pagerLabel={t.columnPagerLabel}
|
|
96
|
+
prev={prev ? { href: columnHref(prev), title: stripColumnIndex(prev.title) } : null}
|
|
97
|
+
next={next ? { href: columnHref(next), title: stripColumnIndex(next.title) } : null}
|
|
98
|
+
series={{
|
|
99
|
+
columnTitle: entry.columnTitle ?? column,
|
|
100
|
+
statusLabel: t.columnSideCount(series.length, columnStatusLabel(entry.columnStatus)),
|
|
101
|
+
items: listed.map((item) => ({
|
|
102
|
+
path: item.path,
|
|
103
|
+
href: columnHref(item),
|
|
104
|
+
title: stripColumnIndex(item.title),
|
|
105
|
+
current: item.path === entry.path,
|
|
106
|
+
})),
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
171
109
|
);
|
|
172
110
|
}
|
package/app/columns/page.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { columnHref, columns } from "../lib/content";
|
|
4
4
|
import { ContentEmptyState } from "../components/ContentEmptyState";
|
|
5
|
+
import { PageIntro } from "../components/PageIntro";
|
|
5
6
|
import { siteConfig, siteDocumentTitle } from "../site.config";
|
|
6
7
|
import { pageAlternates } from "../lib/page-alternates";
|
|
7
8
|
import { columnStatusLabel, ui } from "../lib/i18n";
|
|
@@ -40,10 +41,11 @@ export default function ColumnsPage() {
|
|
|
40
41
|
|
|
41
42
|
return (
|
|
42
43
|
<main className="sh-main" id="content">
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
{siteConfig.pages.columns.
|
|
46
|
-
|
|
44
|
+
<PageIntro
|
|
45
|
+
eyebrow={t.columnsEyebrow}
|
|
46
|
+
title={siteConfig.pages.columns.title || t.homeColumns}
|
|
47
|
+
subtitle={siteConfig.pages.columns.subtitle}
|
|
48
|
+
/>
|
|
47
49
|
{grouped.length ? grouped.map(({ slug, first, entries }) => (
|
|
48
50
|
<section className={first.columnCover ? "sh-series" : "sh-series no-cover"} aria-labelledby={slug} key={slug}>
|
|
49
51
|
{first.columnCover ? (
|
|
@@ -7,15 +7,20 @@ type PageIntroProps = {
|
|
|
7
7
|
children?: ReactNode;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Shared heading for top-level list pages (blog / thoughts / columns).
|
|
12
|
+
* Same eyebrow + h1 + deck typography and padding so tab switches do not jump.
|
|
13
|
+
* Empty subtitle reserves no space.
|
|
14
|
+
*/
|
|
11
15
|
export function PageIntro({
|
|
12
16
|
eyebrow,
|
|
13
17
|
title,
|
|
14
18
|
subtitle,
|
|
15
19
|
children,
|
|
16
20
|
}: PageIntroProps) {
|
|
21
|
+
const deck = subtitle?.trim();
|
|
17
22
|
return (
|
|
18
|
-
<header className="page-intro">
|
|
23
|
+
<header className="page-intro sh-head">
|
|
19
24
|
{eyebrow ? <p className="eyebrow">{eyebrow}</p> : null}
|
|
20
25
|
<h1>
|
|
21
26
|
{title.split("\n").map((line, index) => (
|
|
@@ -25,8 +30,8 @@ export function PageIntro({
|
|
|
25
30
|
</span>
|
|
26
31
|
))}
|
|
27
32
|
</h1>
|
|
28
|
-
{
|
|
29
|
-
{children
|
|
33
|
+
{deck ? <p className="sh-deck">{deck}</p> : null}
|
|
34
|
+
{children ? <div className="page-intro-actions">{children}</div> : null}
|
|
30
35
|
</header>
|
|
31
36
|
);
|
|
32
37
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { CoverImage } from "./CoverImage";
|
|
3
|
+
import { ArticleMeasure } from "./ArticleMeasure";
|
|
4
|
+
import { ArticleReferences } from "./ArticleReferences";
|
|
5
|
+
import { ArticleTitle, originalUrl } from "./ArticleTitle";
|
|
6
|
+
import { MarkdownArticle } from "./MarkdownArticle";
|
|
7
|
+
import { TableOfContents } from "./TableOfContents";
|
|
8
|
+
import { Comments } from "./Comments";
|
|
9
|
+
import { Webmentions } from "./Webmentions";
|
|
10
|
+
import { ArticleTags } from "./ArticleTags";
|
|
11
|
+
import { ArticlePager } from "./ArticlePager";
|
|
12
|
+
import { SeriesEdgeNav, type SeriesEdgeItem } from "./SeriesEdgeNav";
|
|
13
|
+
import { formatDate, estimateWordCount, extractHeadings } from "../lib/content";
|
|
14
|
+
import { githubUrl, siteConfig } from "../site.config";
|
|
15
|
+
import { siteLang, ui } from "../lib/i18n";
|
|
16
|
+
import { webmentionApiEndpoint } from "../lib/site";
|
|
17
|
+
import type { ContentEntry } from "../lib/content";
|
|
18
|
+
|
|
19
|
+
export type PostReadingPagerLink = { href: string; title: string } | null;
|
|
20
|
+
|
|
21
|
+
export type PostReadingSeries = {
|
|
22
|
+
columnTitle: string;
|
|
23
|
+
statusLabel?: string;
|
|
24
|
+
items: SeriesEdgeItem[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Shared article body for /blog/... and /columns/... .
|
|
29
|
+
* Column routes only differ by mounting SeriesEdgeNav on the left.
|
|
30
|
+
*/
|
|
31
|
+
export function PostReadingView({
|
|
32
|
+
post,
|
|
33
|
+
pathname,
|
|
34
|
+
canonicalUrl,
|
|
35
|
+
title,
|
|
36
|
+
commentsSlug,
|
|
37
|
+
prev,
|
|
38
|
+
next,
|
|
39
|
+
pagerLabel,
|
|
40
|
+
series,
|
|
41
|
+
}: {
|
|
42
|
+
post: ContentEntry;
|
|
43
|
+
pathname: string;
|
|
44
|
+
canonicalUrl: string;
|
|
45
|
+
/** Display title; defaults to post.title */
|
|
46
|
+
title?: string;
|
|
47
|
+
commentsSlug: string;
|
|
48
|
+
prev?: PostReadingPagerLink;
|
|
49
|
+
next?: PostReadingPagerLink;
|
|
50
|
+
pagerLabel?: string;
|
|
51
|
+
series?: PostReadingSeries;
|
|
52
|
+
}) {
|
|
53
|
+
const displayTitle = title ?? post.title;
|
|
54
|
+
const numbering = (post as ContentEntry & { numbering?: boolean }).numbering;
|
|
55
|
+
const headings = extractHeadings(post.content, { numbering });
|
|
56
|
+
const wordCount = estimateWordCount(post.content);
|
|
57
|
+
const t = ui();
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<main className="reading-page post-reading-page">
|
|
61
|
+
{series ? (
|
|
62
|
+
<SeriesEdgeNav
|
|
63
|
+
columnTitle={series.columnTitle}
|
|
64
|
+
statusLabel={series.statusLabel}
|
|
65
|
+
items={series.items}
|
|
66
|
+
/>
|
|
67
|
+
) : null}
|
|
68
|
+
<article className="post-article h-entry">
|
|
69
|
+
<Link className="u-url sr-only" href={pathname}>
|
|
70
|
+
{siteLang === "en" ? t.permalinkLabel(displayTitle) : <>{displayTitle} 永久链接</>}
|
|
71
|
+
</Link>
|
|
72
|
+
<a className="p-author h-card sr-only" href={githubUrl} rel="author">
|
|
73
|
+
{siteConfig.author.name}
|
|
74
|
+
</a>
|
|
75
|
+
<header className="article-header article-summary-row hero">
|
|
76
|
+
<ArticleTitle title={displayTitle} />
|
|
77
|
+
<ul className="meta article-meta">
|
|
78
|
+
<li>{siteConfig.author.name}</li>
|
|
79
|
+
<li>
|
|
80
|
+
<time className="dt-published" dateTime={post.date}>
|
|
81
|
+
{formatDate(post.date)}
|
|
82
|
+
</time>
|
|
83
|
+
</li>
|
|
84
|
+
{post.updated ? (
|
|
85
|
+
<li>
|
|
86
|
+
{siteLang === "en" ? t.updatedOn(formatDate(post.updated)) : <>更新于 {formatDate(post.updated)}</>}
|
|
87
|
+
</li>
|
|
88
|
+
) : null}
|
|
89
|
+
<li>{t.wordCount(wordCount)}</li>
|
|
90
|
+
{originalUrl(post) ? (
|
|
91
|
+
<li>
|
|
92
|
+
<a className="orig ext" href={originalUrl(post)} target="_blank" rel="noopener noreferrer">
|
|
93
|
+
{t.originalLink}
|
|
94
|
+
</a>
|
|
95
|
+
</li>
|
|
96
|
+
) : null}
|
|
97
|
+
</ul>
|
|
98
|
+
<ArticleTags tags={post.tags} />
|
|
99
|
+
{post.cover ? (
|
|
100
|
+
<figure className="article-hero article-cover cover-card cover-img">
|
|
101
|
+
<CoverImage
|
|
102
|
+
className="u-photo"
|
|
103
|
+
src={post.cover}
|
|
104
|
+
image={post.coverImage}
|
|
105
|
+
alt={t.coverLabel(displayTitle)}
|
|
106
|
+
sizes="(max-width: 852px) calc(100vw - 40px), 812px"
|
|
107
|
+
priority
|
|
108
|
+
/>
|
|
109
|
+
</figure>
|
|
110
|
+
) : null}
|
|
111
|
+
</header>
|
|
112
|
+
<div className="article-reading-layout">
|
|
113
|
+
<div className="article-reading-column">
|
|
114
|
+
<ArticleMeasure />
|
|
115
|
+
<div className="e-content">
|
|
116
|
+
{post.description ? <p className="lead quote-plain p-summary">{post.description}</p> : null}
|
|
117
|
+
<MarkdownArticle content={post.content} numbering={numbering} />
|
|
118
|
+
</div>
|
|
119
|
+
<ArticleReferences references={post.references} />
|
|
120
|
+
<Webmentions target={canonicalUrl} endpoint={webmentionApiEndpoint()} />
|
|
121
|
+
<ArticlePager
|
|
122
|
+
label={pagerLabel}
|
|
123
|
+
prev={prev ?? null}
|
|
124
|
+
next={next ?? null}
|
|
125
|
+
/>
|
|
126
|
+
<Comments slug={commentsSlug} />
|
|
127
|
+
</div>
|
|
128
|
+
<TableOfContents headings={headings} />
|
|
129
|
+
</div>
|
|
130
|
+
</article>
|
|
131
|
+
</main>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import {
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useId,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
export type SeriesEdgeItem = {
|
|
14
|
+
path: string;
|
|
15
|
+
href: string;
|
|
16
|
+
title: string;
|
|
17
|
+
current?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Left-edge Raschka ticks for column articles.
|
|
22
|
+
* Flyout is the article list only (no column chrome).
|
|
23
|
+
* Opens on hover or click; stays open until click-outside or Escape.
|
|
24
|
+
*/
|
|
25
|
+
export function SeriesEdgeNav({
|
|
26
|
+
columnTitle,
|
|
27
|
+
items,
|
|
28
|
+
}: {
|
|
29
|
+
columnTitle: string;
|
|
30
|
+
items: SeriesEdgeItem[];
|
|
31
|
+
/** @deprecated kept optional so callers can pass it; not shown in the flyout */
|
|
32
|
+
statusLabel?: string;
|
|
33
|
+
lang?: string;
|
|
34
|
+
}) {
|
|
35
|
+
const panelId = useId();
|
|
36
|
+
const rootRef = useRef<HTMLElement>(null);
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
const currentIndex = Math.max(0, items.findIndex((item) => item.current));
|
|
39
|
+
|
|
40
|
+
const close = useCallback(() => setOpen(false), []);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!open) return;
|
|
44
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
45
|
+
const root = rootRef.current;
|
|
46
|
+
if (!root) return;
|
|
47
|
+
if (event.target instanceof Node && root.contains(event.target)) return;
|
|
48
|
+
close();
|
|
49
|
+
};
|
|
50
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
51
|
+
if (event.key === "Escape") close();
|
|
52
|
+
};
|
|
53
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
54
|
+
document.addEventListener("keydown", onKeyDown);
|
|
55
|
+
return () => {
|
|
56
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
57
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
58
|
+
};
|
|
59
|
+
}, [close, open]);
|
|
60
|
+
|
|
61
|
+
const onTicksKeyDown = (event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
|
62
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
setOpen((value) => !value);
|
|
65
|
+
} else if (event.key === "Escape") {
|
|
66
|
+
close();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<nav
|
|
72
|
+
ref={rootRef}
|
|
73
|
+
className="series-edge"
|
|
74
|
+
aria-label={columnTitle}
|
|
75
|
+
data-open={open ? "true" : "false"}
|
|
76
|
+
>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
className="series-edge-ticks"
|
|
80
|
+
aria-controls={panelId}
|
|
81
|
+
aria-expanded={open}
|
|
82
|
+
aria-label={columnTitle}
|
|
83
|
+
onPointerEnter={() => setOpen(true)}
|
|
84
|
+
onFocus={() => setOpen(true)}
|
|
85
|
+
onClick={() => setOpen(true)}
|
|
86
|
+
onKeyDown={onTicksKeyDown}
|
|
87
|
+
>
|
|
88
|
+
{items.map((item, index) => (
|
|
89
|
+
<i
|
|
90
|
+
key={item.path}
|
|
91
|
+
className={index === currentIndex ? "hot" : undefined}
|
|
92
|
+
aria-hidden="true"
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
</button>
|
|
96
|
+
{/* Hit bridge so the pointer can cross ticks → panel without a dead zone. */}
|
|
97
|
+
<div
|
|
98
|
+
className="series-edge-bridge"
|
|
99
|
+
aria-hidden="true"
|
|
100
|
+
onPointerEnter={() => setOpen(true)}
|
|
101
|
+
/>
|
|
102
|
+
<div
|
|
103
|
+
className="series-edge-fly"
|
|
104
|
+
id={panelId}
|
|
105
|
+
onPointerEnter={() => setOpen(true)}
|
|
106
|
+
>
|
|
107
|
+
<ol className="series-edge-list">
|
|
108
|
+
{items.map((item) => (
|
|
109
|
+
<li key={item.path} className={item.current ? "cur" : undefined}>
|
|
110
|
+
<Link href={item.href} aria-current={item.current ? "page" : undefined}>
|
|
111
|
+
{item.title}
|
|
112
|
+
</Link>
|
|
113
|
+
</li>
|
|
114
|
+
))}
|
|
115
|
+
</ol>
|
|
116
|
+
</div>
|
|
117
|
+
</nav>
|
|
118
|
+
);
|
|
119
|
+
}
|
package/app/lib/i18n.ts
CHANGED
|
@@ -141,6 +141,7 @@ const zh = {
|
|
|
141
141
|
homeColumnCountStatus: (count: number, status: string) => `${count} 篇,${status}`,
|
|
142
142
|
columnDefaultStatus: "持续更新",
|
|
143
143
|
// Blog archive
|
|
144
|
+
blogEyebrow: "BLOG / 文章",
|
|
144
145
|
blogHeading: "文章",
|
|
145
146
|
blogDeck: (n: number) => `阶段性的完整沉淀,关于工程、架构与技术思考。共 ${n} 篇,按年份归档。`,
|
|
146
147
|
unknownYear: "未知",
|
|
@@ -170,6 +171,7 @@ const zh = {
|
|
|
170
171
|
seriesEyebrow: "主题专栏",
|
|
171
172
|
seriesCountStatus: (count: number, status: string) => `${count} 篇 · ${status}`,
|
|
172
173
|
// Columns index
|
|
174
|
+
columnsEyebrow: "COLUMNS / 专栏",
|
|
173
175
|
columnsFallbackTitle: "专栏",
|
|
174
176
|
columnsMeta: (count: number, status: string) => `阅读清单,${count} 篇,${status}`,
|
|
175
177
|
columnsFallbackDesc: "按顺序组织的专题文章系列。",
|
|
@@ -195,7 +197,7 @@ const zh = {
|
|
|
195
197
|
audioFallback: "你的浏览器不支持音频播放。",
|
|
196
198
|
thoughtPermalinkShort: "永久链接",
|
|
197
199
|
// About & GitHub profile
|
|
198
|
-
aboutEyebrow: "关于",
|
|
200
|
+
aboutEyebrow: "ABOUT / 关于",
|
|
199
201
|
aboutFallbackHeading: "关于",
|
|
200
202
|
aboutFallbackParagraphs: [
|
|
201
203
|
"一名工程师。平时写代码,也把想清楚的问题记下来。",
|
|
@@ -424,6 +426,7 @@ const en: typeof zh = {
|
|
|
424
426
|
homeColumnCountStatus: (count: number, status: string) => `${count} ${count === 1 ? "post" : "posts"}, ${status}`,
|
|
425
427
|
columnDefaultStatus: "Ongoing",
|
|
426
428
|
// Blog archive
|
|
429
|
+
blogEyebrow: "BLOG",
|
|
427
430
|
blogHeading: "Posts",
|
|
428
431
|
blogDeck: (n: number) => `Essays on engineering, architecture, and systems thinking. ${n} ${n === 1 ? "post" : "posts"}, archived by year.`,
|
|
429
432
|
unknownYear: "Unknown",
|
|
@@ -453,6 +456,7 @@ const en: typeof zh = {
|
|
|
453
456
|
seriesEyebrow: "Column",
|
|
454
457
|
seriesCountStatus: (count: number, status: string) => `${count} ${count === 1 ? "post" : "posts"} · ${status}`,
|
|
455
458
|
// Columns index
|
|
459
|
+
columnsEyebrow: "COLUMNS",
|
|
456
460
|
columnsFallbackTitle: "Columns",
|
|
457
461
|
columnsMeta: (count: number, status: string) => `Reading list · ${count} ${count === 1 ? "post" : "posts"} · ${status}`,
|
|
458
462
|
columnsFallbackDesc: "A series of posts organized in reading order.",
|
|
@@ -478,7 +482,7 @@ const en: typeof zh = {
|
|
|
478
482
|
audioFallback: "Your browser does not support audio playback.",
|
|
479
483
|
thoughtPermalinkShort: "Permalink",
|
|
480
484
|
// About & GitHub profile
|
|
481
|
-
aboutEyebrow: "
|
|
485
|
+
aboutEyebrow: "ABOUT",
|
|
482
486
|
aboutFallbackHeading: "About",
|
|
483
487
|
aboutFallbackParagraphs: [
|
|
484
488
|
"An engineer who writes code and writes down problems once they are clear.",
|
package/app/page.tsx
CHANGED
|
@@ -43,7 +43,9 @@ export default function Home() {
|
|
|
43
43
|
<main className="sh-main" id="content">
|
|
44
44
|
<section className="sh-intro">
|
|
45
45
|
<h1>{siteConfig.pages.home.title}</h1>
|
|
46
|
-
|
|
46
|
+
{siteConfig.pages.home.subtitle?.trim() ? (
|
|
47
|
+
<p>{siteConfig.pages.home.subtitle}</p>
|
|
48
|
+
) : null}
|
|
47
49
|
<div className="links">
|
|
48
50
|
<Link href={siteHref("/about")}>{t.homeAbout}</Link>
|
|
49
51
|
<a href={githubUrl} rel="me">GitHub</a>
|
package/app/styles/a2.css
CHANGED
|
@@ -2636,3 +2636,207 @@ html[data-theme="dark"] .ref-demo { --rd-s: #262624; --rd-p: #1f1e1d; --rd-i: #e
|
|
|
2636
2636
|
.ref-demo[data-name="figure"] .ref-pane[data-p="v"] { grid-column: 1; grid-row: 1; }
|
|
2637
2637
|
.ref-demo[data-name="figure"] .ref-pane[data-p="c"] { grid-column: 1; grid-row: 2; background: var(--rd-s); border-top: 1px solid var(--rd-r); }
|
|
2638
2638
|
}
|
|
2639
|
+
|
|
2640
|
+
/* ===== v0.10.1 tip keep (after git checkout of a2.css from 9a80f7e5) ===== */
|
|
2641
|
+
|
|
2642
|
+
/* List pages: PageIntro rides on .sh-head; keep eyebrow in the same vertical rhythm. */
|
|
2643
|
+
.page-intro.sh-head {
|
|
2644
|
+
padding: 72px 0 36px;
|
|
2645
|
+
max-width: var(--list);
|
|
2646
|
+
width: 100%;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
.page-intro.sh-head .eyebrow {
|
|
2650
|
+
margin: 0 0 14px;
|
|
2651
|
+
color: var(--muted);
|
|
2652
|
+
font: 600 11px / 1.4 var(--sans);
|
|
2653
|
+
letter-spacing: 0.12em;
|
|
2654
|
+
text-transform: uppercase;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
.page-intro.sh-head h1 {
|
|
2658
|
+
margin: 0;
|
|
2659
|
+
font: 700 var(--fs-display) / 1.2 var(--sans);
|
|
2660
|
+
color: var(--ink);
|
|
2661
|
+
letter-spacing: 0;
|
|
2662
|
+
text-wrap: balance;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
.page-intro.sh-head .sh-deck {
|
|
2666
|
+
margin: 14px 0 0;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
.page-intro.sh-head .page-intro-actions {
|
|
2670
|
+
margin-top: 22px;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
.thoughts-page,
|
|
2674
|
+
.about-page {
|
|
2675
|
+
width: auto;
|
|
2676
|
+
max-width: none;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
.about-page {
|
|
2680
|
+
padding-bottom: 120px;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
/* Keep PageIntro at --list; focus card sticks to the right edge. */
|
|
2684
|
+
.about-page .about-grid {
|
|
2685
|
+
width: 100%;
|
|
2686
|
+
max-width: none;
|
|
2687
|
+
display: grid;
|
|
2688
|
+
grid-template-columns: minmax(0, var(--list)) minmax(12rem, 17rem);
|
|
2689
|
+
column-gap: 48px;
|
|
2690
|
+
row-gap: 36px;
|
|
2691
|
+
justify-content: space-between;
|
|
2692
|
+
align-items: start;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
@media (max-width: 900px) {
|
|
2696
|
+
.about-page .about-grid {
|
|
2697
|
+
grid-template-columns: 1fr;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
/* SeriesEdgeNav: fixed to viewport left on column posts that share PostReadingView.
|
|
2702
|
+
post-reading-page already lays out article + TOC; ticks are position:fixed so
|
|
2703
|
+
they do not need a grid track. */
|
|
2704
|
+
.post-reading-page > .series-edge {
|
|
2705
|
+
--edge-window-inset: 12px;
|
|
2706
|
+
position: fixed;
|
|
2707
|
+
top: calc(var(--hdr) + 12px);
|
|
2708
|
+
bottom: 16px;
|
|
2709
|
+
left: max(var(--edge-window-inset), env(safe-area-inset-left, 0px));
|
|
2710
|
+
z-index: 15;
|
|
2711
|
+
display: flex;
|
|
2712
|
+
align-items: center;
|
|
2713
|
+
pointer-events: none;
|
|
2714
|
+
transform: none;
|
|
2715
|
+
margin: 0;
|
|
2716
|
+
padding: 0;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
.series-edge-ticks {
|
|
2720
|
+
pointer-events: auto;
|
|
2721
|
+
appearance: none;
|
|
2722
|
+
border: 0;
|
|
2723
|
+
background: transparent;
|
|
2724
|
+
box-shadow: none;
|
|
2725
|
+
display: flex;
|
|
2726
|
+
flex-direction: column;
|
|
2727
|
+
justify-content: center;
|
|
2728
|
+
align-items: center;
|
|
2729
|
+
gap: 8px;
|
|
2730
|
+
padding: 12px 6px;
|
|
2731
|
+
margin: 0;
|
|
2732
|
+
cursor: pointer;
|
|
2733
|
+
color: inherit;
|
|
2734
|
+
font: inherit;
|
|
2735
|
+
border-radius: 0;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
.series-edge-ticks i {
|
|
2739
|
+
display: block;
|
|
2740
|
+
width: 11px;
|
|
2741
|
+
height: 1.5px;
|
|
2742
|
+
border-radius: 1px;
|
|
2743
|
+
background: color-mix(in srgb, var(--ink) 32%, transparent);
|
|
2744
|
+
transition: background 0.15s ease, width 0.15s ease;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
.series-edge-ticks i.hot {
|
|
2748
|
+
width: 15px;
|
|
2749
|
+
background: color-mix(in srgb, var(--ink) 70%, transparent);
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
.series-edge[data-open="true"] .series-edge-ticks i,
|
|
2753
|
+
.series-edge:focus-within .series-edge-ticks i {
|
|
2754
|
+
background: color-mix(in srgb, var(--ink) 52%, transparent);
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
.series-edge[data-open="true"] .series-edge-ticks i.hot,
|
|
2758
|
+
.series-edge:focus-within .series-edge-ticks i.hot {
|
|
2759
|
+
background: var(--ink);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
.series-edge-bridge {
|
|
2763
|
+
pointer-events: auto;
|
|
2764
|
+
position: absolute;
|
|
2765
|
+
top: 0;
|
|
2766
|
+
bottom: 0;
|
|
2767
|
+
left: 100%;
|
|
2768
|
+
width: 28px;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
.series-edge-fly {
|
|
2772
|
+
pointer-events: none;
|
|
2773
|
+
position: absolute;
|
|
2774
|
+
top: 50%;
|
|
2775
|
+
left: calc(100% + 4px);
|
|
2776
|
+
transform: translate(0, -50%);
|
|
2777
|
+
width: 16rem;
|
|
2778
|
+
max-height: min(72vh, calc(100vh - var(--hdr) - 40px));
|
|
2779
|
+
margin: 0;
|
|
2780
|
+
padding: 0.7rem 0.85rem 0.75rem;
|
|
2781
|
+
border: 0;
|
|
2782
|
+
border-radius: 10px;
|
|
2783
|
+
background: var(--paper);
|
|
2784
|
+
box-shadow:
|
|
2785
|
+
0 1px 2px color-mix(in srgb, var(--ink) 6%, transparent),
|
|
2786
|
+
0 10px 28px color-mix(in srgb, var(--ink) 10%, transparent);
|
|
2787
|
+
opacity: 0;
|
|
2788
|
+
visibility: hidden;
|
|
2789
|
+
overflow: auto;
|
|
2790
|
+
transition: opacity 0.16s ease, visibility 0.16s ease;
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
.series-edge[data-open="true"],
|
|
2794
|
+
.series-edge:focus-within {
|
|
2795
|
+
z-index: 28;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
.series-edge[data-open="true"] .series-edge-fly,
|
|
2799
|
+
.series-edge:focus-within .series-edge-fly {
|
|
2800
|
+
pointer-events: auto;
|
|
2801
|
+
opacity: 1;
|
|
2802
|
+
visibility: visible;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
.series-edge-list {
|
|
2806
|
+
list-style: none;
|
|
2807
|
+
margin: 0;
|
|
2808
|
+
padding: 0;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
.series-edge-list a {
|
|
2812
|
+
display: block;
|
|
2813
|
+
padding: 0.4rem 0.35rem;
|
|
2814
|
+
margin: 0 -0.35rem;
|
|
2815
|
+
border-radius: 7px;
|
|
2816
|
+
color: var(--soft);
|
|
2817
|
+
font-size: 13px;
|
|
2818
|
+
line-height: 1.35;
|
|
2819
|
+
text-decoration: none;
|
|
2820
|
+
white-space: nowrap;
|
|
2821
|
+
overflow: hidden;
|
|
2822
|
+
text-overflow: ellipsis;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
.series-edge-list a:hover {
|
|
2826
|
+
background: color-mix(in srgb, var(--ink) 4%, transparent);
|
|
2827
|
+
color: var(--ink);
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
.series-edge-list li.cur a {
|
|
2831
|
+
background: color-mix(in srgb, var(--ink) 6%, transparent);
|
|
2832
|
+
color: var(--ink);
|
|
2833
|
+
font-weight: 600;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
@media (max-width: 900px) {
|
|
2837
|
+
/* The desktop rule is `.post-reading-page > .series-edge { display: flex }`.
|
|
2838
|
+
Match that specificity so the rail stays off small screens. */
|
|
2839
|
+
.post-reading-page > .series-edge {
|
|
2840
|
+
display: none;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
@@ -36,7 +36,7 @@ export default async function ThoughtsPage({ params }: { params: Promise<{ n: st
|
|
|
36
36
|
notFound();
|
|
37
37
|
}
|
|
38
38
|
return (
|
|
39
|
-
<main className="
|
|
39
|
+
<main className="sh-main thoughts-page" id="content">
|
|
40
40
|
<PageIntro eyebrow={ui().thoughtsEyebrow} title={siteConfig.pages.thoughts.title} subtitle={siteConfig.pages.thoughts.subtitle} />
|
|
41
41
|
<ThoughtFeed page={page} />
|
|
42
42
|
</main>
|
package/app/thoughts/page.tsx
CHANGED
|
@@ -16,7 +16,7 @@ export default function ThoughtsPage() {
|
|
|
16
16
|
const thoughts = getPublishedThoughts();
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<main className="
|
|
19
|
+
<main className="sh-main thoughts-page" id="content">
|
|
20
20
|
<PageIntro
|
|
21
21
|
eyebrow={ui().thoughtsEyebrow}
|
|
22
22
|
title={siteConfig.pages.thoughts.title}
|
package/package.json
CHANGED
package/ui/garden/site-nav.mjs
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* The header's site navigation. Hrefs carry the trailing slash: a static
|
|
3
3
|
* export serves these pages at /blog/ and so on, and the host answers /blog
|
|
4
4
|
* with a 301 first (new tab, no JavaScript, crawlers).
|
|
5
|
+
*
|
|
6
|
+
* /tags/ stays as a deep route (article tag chips still link there) but is
|
|
7
|
+
* not listed in the primary nav.
|
|
5
8
|
*/
|
|
6
9
|
export const SITE_NAV = [
|
|
7
10
|
{ href: "/blog/", label: "文章" },
|
|
8
11
|
{ href: "/thoughts/", label: "随想" },
|
|
9
12
|
{ href: "/columns/", label: "专栏" },
|
|
10
|
-
{ href: "/tags/", label: "标签" },
|
|
11
13
|
{ href: "/about/", label: "关于" },
|
|
12
14
|
];
|
|
13
15
|
|