hd-garden 0.4.1 → 0.5.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 +5 -5
- package/Makefile +2 -3
- package/app/a2/[slug]/page.tsx +0 -1
- package/app/a2/page.tsx +1 -1
- package/app/about/page.tsx +8 -12
- package/app/admin/page.tsx +2 -2
- package/app/blog/[...path]/page.tsx +4 -14
- package/app/blog/page.tsx +6 -3
- package/app/columns/[column]/[...path]/page.tsx +4 -9
- package/app/columns/page.tsx +2 -2
- package/app/components/AdminDashboard.tsx +3 -3
- package/app/components/ArticleMeasure.tsx +114 -42
- package/app/components/ArticleReferences.tsx +31 -31
- package/app/components/ArticleTags.tsx +17 -0
- package/app/components/BlogArchive.tsx +94 -50
- package/app/components/ContentEmptyState.tsx +6 -6
- package/app/components/CoverImage.tsx +34 -0
- package/app/components/GardenAdminGuide.tsx +1 -1
- package/app/components/GardenContentGuide.tsx +1 -2
- package/app/components/GardenMediaGuide.tsx +3 -3
- package/app/components/GithubProfile.tsx +1 -1
- package/app/components/LinkCardRefresh.tsx +44 -0
- package/app/components/MarkdownArticle.tsx +87 -19
- package/app/components/MarkdownImage.tsx +4 -2
- package/app/components/SiteFooter.tsx +5 -5
- package/app/components/SiteHeader.tsx +105 -33
- package/app/components/TagRedirect.tsx +17 -0
- package/app/garden/page.tsx +6 -6
- package/app/garden-modules.d.ts +17 -2
- package/app/globals.css +92 -136
- package/app/layout.tsx +1 -7
- package/app/lib/a2-meta.ts +10 -0
- package/app/lib/a2-preview.ts +0 -2
- package/app/lib/archive.ts +43 -0
- package/app/lib/content.ts +12 -43
- package/app/lib/fallback-site-config.ts +15 -15
- package/app/lib/generated-content.ts +58 -47
- package/app/lib/heading-number.ts +9 -0
- package/app/lib/remark-link-card.mjs +83 -0
- package/app/lib/tag-filter.ts +151 -0
- package/app/not-found.tsx +13 -0
- package/app/page.tsx +12 -29
- package/app/rss.xml/route.ts +2 -1
- package/app/styles/a2.css +159 -27
- package/app/styles/shell.css +54 -59
- package/app/styles/tokens.css +35 -22
- package/app/tags/page.tsx +20 -9
- package/bin/garden.mjs +6 -0
- package/package.json +1 -1
- package/scripts/build-content.mjs +50 -9
- package/scripts/check-post.mjs +0 -3
- package/scripts/cover-images.mjs +69 -0
- package/scripts/link-assets.mjs +271 -0
- package/scripts/link-meta-core.mjs +321 -0
- package/scripts/new-post.mjs +1 -3
- package/site.config.yaml +15 -15
- package/templates/site/Makefile +2 -3
- package/templates/site/site.config.yaml +15 -15
- package/ui/core/transform.mjs +4 -3
- package/ui/garden/card-refresh.mjs +92 -0
- package/ui/garden/check.mjs +2 -4
- package/ui/garden/enhance.mjs +57 -0
- package/ui/garden/markdown-links.mjs +84 -0
- package/ui/garden/references.mjs +21 -0
- package/ui/garden/search-full.mjs +131 -0
- package/ui/garden/search-index.mjs +102 -0
- package/ui/garden/search-match.mjs +96 -0
- package/ui/garden/title.mjs +5 -8
- package/ui/host.mjs +13 -13
- package/ui/preview/dist/garden-ui.css +11 -11
- package/ui/preview/dist/garden-ui.esm.js +8 -8
- package/ui/preview/dist/garden-ui.iife.js +8 -8
- package/ui/react/components/media.tsx +2 -2
- package/ui/react/ui.css +11 -11
- package/worker/og/README.md +59 -0
- package/worker/og/handler.mjs +147 -0
- package/worker/og/index.mjs +61 -0
- package/worker/og/wrangler.toml +18 -0
- package/app/components/TagIndex.tsx +0 -97
|
@@ -10,11 +10,79 @@ import {
|
|
|
10
10
|
useState,
|
|
11
11
|
type KeyboardEvent,
|
|
12
12
|
} from "react";
|
|
13
|
-
import
|
|
13
|
+
import type Fuse from "fuse.js";
|
|
14
14
|
import { MagnifyingGlass, X } from "@phosphor-icons/react";
|
|
15
|
-
import {
|
|
15
|
+
import { searchFull, searchIndexUrl } from "@garden/site-config";
|
|
16
|
+
import { createSearch } from "../../ui/garden/search-match.mjs";
|
|
17
|
+
import { createIdbStore, loadFullIndex, saveDataOn, whenIdleAfterLoad } from "../../ui/garden/search-full.mjs";
|
|
16
18
|
import { siteConfig } from "../site.config";
|
|
17
19
|
|
|
20
|
+
/** One entry of /search-index.json: titles, tags, dates and a short excerpt, never the full body. */
|
|
21
|
+
type SearchRecord = {
|
|
22
|
+
title: string;
|
|
23
|
+
description: string;
|
|
24
|
+
section: string;
|
|
25
|
+
path: string;
|
|
26
|
+
content: string;
|
|
27
|
+
tags?: string[];
|
|
28
|
+
date?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type SearchEngine = { records: SearchRecord[]; fuse: Fuse<SearchRecord> };
|
|
32
|
+
let engineLoad: Promise<SearchEngine> | null = null;
|
|
33
|
+
|
|
34
|
+
type MatchRange = readonly [number, number];
|
|
35
|
+
type SearchMatch = { key?: string; indices: readonly MatchRange[] };
|
|
36
|
+
type SearchHit = { item: SearchRecord; matches?: readonly SearchMatch[] };
|
|
37
|
+
|
|
38
|
+
let fullLoad: Promise<Map<string, string>> | null = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Every body as plain text. Started in the background after the page has
|
|
42
|
+
* loaded, or when search opens first; read from IndexedDB when this version
|
|
43
|
+
* was fetched before.
|
|
44
|
+
*/
|
|
45
|
+
function loadFullText() {
|
|
46
|
+
const scope = searchFull.url.slice(0, searchFull.url.lastIndexOf("/"));
|
|
47
|
+
fullLoad ??= loadFullIndex({ url: searchFull.url, hash: searchFull.hash, store: createIdbStore(scope) })
|
|
48
|
+
.then((docs) => new Map(docs))
|
|
49
|
+
.catch((error: unknown) => {
|
|
50
|
+
fullLoad = null;
|
|
51
|
+
throw error;
|
|
52
|
+
});
|
|
53
|
+
return fullLoad;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Index and Fuse are fetched together, the first time search is about to open. */
|
|
57
|
+
function loadSearchEngine() {
|
|
58
|
+
engineLoad ??= Promise.all([
|
|
59
|
+
fetch(searchIndexUrl).then((response) => {
|
|
60
|
+
if (!response.ok) throw new Error(`search index ${response.status}`);
|
|
61
|
+
return response.json() as Promise<SearchRecord[]>;
|
|
62
|
+
}),
|
|
63
|
+
import("fuse.js"),
|
|
64
|
+
])
|
|
65
|
+
.then(([records, { default: FuseClass }]) => ({
|
|
66
|
+
records,
|
|
67
|
+
fuse: new FuseClass(records, {
|
|
68
|
+
keys: [
|
|
69
|
+
{ name: "title", weight: 0.5 },
|
|
70
|
+
{ name: "description", weight: 0.3 },
|
|
71
|
+
{ name: "tags", weight: 0.2 },
|
|
72
|
+
{ name: "content", weight: 0.15 },
|
|
73
|
+
],
|
|
74
|
+
threshold: 0.36,
|
|
75
|
+
ignoreLocation: true,
|
|
76
|
+
includeMatches: true,
|
|
77
|
+
}),
|
|
78
|
+
}))
|
|
79
|
+
.catch((error) => {
|
|
80
|
+
engineLoad = null;
|
|
81
|
+
throw error;
|
|
82
|
+
});
|
|
83
|
+
return engineLoad;
|
|
84
|
+
}
|
|
85
|
+
|
|
18
86
|
const nav = [
|
|
19
87
|
{ href: "/blog", label: "文章" },
|
|
20
88
|
{ href: "/thoughts", label: "随想" },
|
|
@@ -23,10 +91,8 @@ const nav = [
|
|
|
23
91
|
{ href: "/about", label: "关于" },
|
|
24
92
|
];
|
|
25
93
|
|
|
26
|
-
type MatchRange = readonly [number, number];
|
|
27
|
-
|
|
28
94
|
function rangesFor(
|
|
29
|
-
matches: readonly
|
|
95
|
+
matches: readonly SearchMatch[] | undefined,
|
|
30
96
|
key: string,
|
|
31
97
|
): readonly MatchRange[] {
|
|
32
98
|
return matches?.find((match) => match.key === key)?.indices ?? [];
|
|
@@ -119,8 +185,8 @@ function cropMatchedText(text: string, ranges: readonly MatchRange[]) {
|
|
|
119
185
|
}
|
|
120
186
|
|
|
121
187
|
function searchPreview(
|
|
122
|
-
item:
|
|
123
|
-
matches: readonly
|
|
188
|
+
item: SearchRecord,
|
|
189
|
+
matches: readonly SearchMatch[] | undefined,
|
|
124
190
|
): { text: string; ranges: readonly MatchRange[] } {
|
|
125
191
|
const descriptionRanges = rangesFor(matches, "description");
|
|
126
192
|
if (descriptionRanges.length) {
|
|
@@ -147,7 +213,25 @@ export function SiteHeader() {
|
|
|
147
213
|
const resultRefs = useRef<Array<HTMLAnchorElement | null>>([]);
|
|
148
214
|
const previousFocusRef = useRef<HTMLElement | null>(null);
|
|
149
215
|
|
|
216
|
+
const [engine, setEngine] = useState<SearchEngine | null>(null);
|
|
217
|
+
const [engineFailed, setEngineFailed] = useState(false);
|
|
218
|
+
const [fullText, setFullText] = useState<Map<string, string> | null>(null);
|
|
219
|
+
const prefetchSearch = useCallback(() => {
|
|
220
|
+
loadSearchEngine().then(setEngine, () => setEngineFailed(true));
|
|
221
|
+
}, []);
|
|
222
|
+
|
|
223
|
+
// Full text arrives quietly once the page is done; search upgrades when it lands.
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (saveDataOn()) return;
|
|
226
|
+
return whenIdleAfterLoad(() => {
|
|
227
|
+
loadFullText().then(setFullText, () => {});
|
|
228
|
+
});
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
150
231
|
const openSearch = useCallback(() => {
|
|
232
|
+
setEngineFailed(false);
|
|
233
|
+
loadSearchEngine().then(setEngine, () => setEngineFailed(true));
|
|
234
|
+
loadFullText().then(setFullText, () => {});
|
|
151
235
|
previousFocusRef.current =
|
|
152
236
|
document.activeElement instanceof HTMLElement
|
|
153
237
|
? document.activeElement
|
|
@@ -190,25 +274,14 @@ export function SiteHeader() {
|
|
|
190
274
|
return () => window.removeEventListener("keydown", onKey);
|
|
191
275
|
}, [closeSearch, openSearch, searchOpen]);
|
|
192
276
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
],
|
|
202
|
-
threshold: 0.36,
|
|
203
|
-
ignoreLocation: true,
|
|
204
|
-
includeMatches: true,
|
|
205
|
-
}),
|
|
206
|
-
[],
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
const results = query.trim()
|
|
210
|
-
? fuse.search(query).slice(0, 6)
|
|
211
|
-
: searchRecords.slice(0, 5).map((item) => ({ item, matches: [] }));
|
|
277
|
+
// Excerpts first, full text once it is here. Typos fall back to Fuse's fuzzy match.
|
|
278
|
+
const matcher = useMemo(() => (engine ? createSearch(engine.records, fullText) : null), [engine, fullText]);
|
|
279
|
+
const results: SearchHit[] = useMemo(() => {
|
|
280
|
+
if (!engine || !matcher) return [];
|
|
281
|
+
if (!query.trim()) return engine.records.slice(0, 5).map((item) => ({ item, matches: [] }));
|
|
282
|
+
const exact = matcher.search(query, 6) as unknown as SearchHit[];
|
|
283
|
+
return exact.length ? exact : engine.fuse.search(query).slice(0, 6);
|
|
284
|
+
}, [engine, matcher, query]);
|
|
212
285
|
|
|
213
286
|
useEffect(() => {
|
|
214
287
|
if (!searchOpen) return;
|
|
@@ -294,7 +367,7 @@ export function SiteHeader() {
|
|
|
294
367
|
))}
|
|
295
368
|
</nav>
|
|
296
369
|
<div className="sh-tools">
|
|
297
|
-
<button ref={searchTriggerRef} className="sh-search" type="button" onClick={openSearch} aria-label="搜索文章" aria-expanded={searchOpen} aria-controls="site-search-dialog">
|
|
370
|
+
<button ref={searchTriggerRef} className="sh-search" type="button" onClick={openSearch} onPointerEnter={prefetchSearch} onFocus={prefetchSearch} aria-label="搜索文章" aria-expanded={searchOpen} aria-controls="site-search-dialog">
|
|
298
371
|
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
299
372
|
<circle cx="9" cy="9" r="5.8" />
|
|
300
373
|
<path d="M13.3 13.3 17 17" />
|
|
@@ -302,7 +375,7 @@ export function SiteHeader() {
|
|
|
302
375
|
<span className="lbl">搜索文章</span>
|
|
303
376
|
<kbd><span className="kbd-mod kbd-mac">⌘</span><span className="kbd-mod kbd-other">Ctrl</span>{" "}K</kbd>
|
|
304
377
|
</button>
|
|
305
|
-
<button className="sh-icon sh-search-m" type="button" onClick={openSearch} aria-label="搜索文章">
|
|
378
|
+
<button className="sh-icon sh-search-m" type="button" onClick={openSearch} onPointerDown={prefetchSearch} aria-label="搜索文章">
|
|
306
379
|
<svg viewBox="0 0 20 20" aria-hidden="true">
|
|
307
380
|
<circle cx="9" cy="9" r="5.8" />
|
|
308
381
|
<path d="M13.3 13.3 17 17" />
|
|
@@ -401,10 +474,7 @@ export function SiteHeader() {
|
|
|
401
474
|
className={`search-result${activeResult === index ? " active" : ""}`}
|
|
402
475
|
>
|
|
403
476
|
<span>
|
|
404
|
-
|
|
405
|
-
text={result.item.topic}
|
|
406
|
-
ranges={rangesFor(result.matches, "topic")}
|
|
407
|
-
/>
|
|
477
|
+
{result.item.section}
|
|
408
478
|
</span>
|
|
409
479
|
<strong>
|
|
410
480
|
<HighlightedText
|
|
@@ -419,7 +489,9 @@ export function SiteHeader() {
|
|
|
419
489
|
);
|
|
420
490
|
})}
|
|
421
491
|
{results.length === 0 && (
|
|
422
|
-
<p className="empty-state"
|
|
492
|
+
<p className="empty-state">
|
|
493
|
+
{engine ? "没有找到,换个更宽松的关键词试试。" : engineFailed ? "搜索暂时打不开,请稍后再试。" : "正在载入…"}
|
|
494
|
+
</p>
|
|
423
495
|
)}
|
|
424
496
|
</div>
|
|
425
497
|
<footer className="search-footer">
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import { TAG_PARAM, selectedTags, tagFilterHref } from "../lib/tag-filter";
|
|
6
|
+
|
|
7
|
+
/** Old `/tags?t=X` links open the same filter on `/blog?t=X`. */
|
|
8
|
+
export function TagRedirect() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const params = useSearchParams();
|
|
11
|
+
const tags = selectedTags(params.getAll(TAG_PARAM));
|
|
12
|
+
const target = tags.length ? tagFilterHref(tags) : "";
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (target) router.replace(target);
|
|
15
|
+
}, [router, target]);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
package/app/garden/page.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { siteConfig } from "../site.config";
|
|
|
7
7
|
|
|
8
8
|
export const metadata = {
|
|
9
9
|
title: "Garden",
|
|
10
|
-
description: "用 Markdown
|
|
10
|
+
description: "用 Markdown 写文章。推送之后,页面、搜索和 RSS 一起更新。",
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const jumps = [
|
|
@@ -45,7 +45,7 @@ const setupSteps = [
|
|
|
45
45
|
{
|
|
46
46
|
title: "发布",
|
|
47
47
|
command: "make update",
|
|
48
|
-
body: "先检查,再提交并推送。main 更新后,网站、搜索和 RSS
|
|
48
|
+
body: "先检查,再提交并推送。main 更新后,网站、搜索和 RSS 会一起更新。",
|
|
49
49
|
},
|
|
50
50
|
];
|
|
51
51
|
|
|
@@ -54,7 +54,7 @@ export default function GardenPage() {
|
|
|
54
54
|
<main className="page-shell garden-page">
|
|
55
55
|
<PageIntro title={siteConfig.pages.garden.title} subtitle={siteConfig.pages.garden.subtitle}>
|
|
56
56
|
<p className="garden-lede">
|
|
57
|
-
|
|
57
|
+
文章放在你自己的仓库里。你改文件,网站跟着更新。
|
|
58
58
|
</p>
|
|
59
59
|
<div className="garden-actions">
|
|
60
60
|
<a
|
|
@@ -133,10 +133,10 @@ export default function GardenPage() {
|
|
|
133
133
|
<header className="garden-section-heading">
|
|
134
134
|
<h2 id="garden-after-title">推送之后</h2>
|
|
135
135
|
</header>
|
|
136
|
-
<p className="garden-after-lead"
|
|
136
|
+
<p className="garden-after-lead">推送之后,网站公开更新。</p>
|
|
137
137
|
<div className="garden-after-copy">
|
|
138
|
-
<p
|
|
139
|
-
<p
|
|
138
|
+
<p>文章在你自己的仓库里。读者打开的是已经发布的页面。</p>
|
|
139
|
+
<p>一次更新会带上正文、目录、前后篇、搜索、RSS 和 sitemap。后台和评论都可以以后再加。</p>
|
|
140
140
|
</div>
|
|
141
141
|
</section>
|
|
142
142
|
|
package/app/garden-modules.d.ts
CHANGED
|
@@ -18,9 +18,8 @@ declare module "@garden/generated-content" {
|
|
|
18
18
|
updated?: string;
|
|
19
19
|
publishAt?: string;
|
|
20
20
|
draft?: boolean;
|
|
21
|
-
topic?: string;
|
|
22
21
|
tags?: string[];
|
|
23
|
-
references?: { title: string; url?: string }[];
|
|
22
|
+
references?: { title: string; url?: string; domain?: string }[];
|
|
24
23
|
pinned?: boolean;
|
|
25
24
|
readingTime?: string;
|
|
26
25
|
kind: "post" | "column" | "thought";
|
|
@@ -34,6 +33,8 @@ declare module "@garden/generated-content" {
|
|
|
34
33
|
columnCoverAlt?: string;
|
|
35
34
|
order?: number;
|
|
36
35
|
cover?: string;
|
|
36
|
+
/** Resized WebP copies of a local cover, when the build had sharp. */
|
|
37
|
+
coverImage?: { src: string; srcset: string; width: number; height: number };
|
|
37
38
|
original?: string;
|
|
38
39
|
mediaType?: "image" | "audio" | "video" | "link" | "text";
|
|
39
40
|
media?: string | MediaItem[];
|
|
@@ -48,9 +49,22 @@ declare module "@garden/generated-content" {
|
|
|
48
49
|
export const columns: ContentEntry[];
|
|
49
50
|
export const thoughts: ContentEntry[];
|
|
50
51
|
export const a2WordCounts: Record<string, number>;
|
|
52
|
+
export type LinkCardMeta = {
|
|
53
|
+
title: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
siteName?: string;
|
|
56
|
+
author?: string;
|
|
57
|
+
publishedTime?: string;
|
|
58
|
+
image?: string;
|
|
59
|
+
};
|
|
60
|
+
export type LinkAssets = { favicons: Record<string, string>; cards: Record<string, LinkCardMeta> };
|
|
61
|
+
export const linkAssets: LinkAssets;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
declare module "@garden/site-config" {
|
|
65
|
+
export const searchIndexUrl: string;
|
|
66
|
+
/** The full-text index file and the hash its contents are cached under. */
|
|
67
|
+
export const searchFull: { url: string; hash: string };
|
|
54
68
|
export const siteConfig: {
|
|
55
69
|
name: string;
|
|
56
70
|
tagline: string;
|
|
@@ -59,6 +73,7 @@ declare module "@garden/site-config" {
|
|
|
59
73
|
author: { name: string; github: string; githubBio: string; githubPinned?: string[] };
|
|
60
74
|
pages: Record<string, Record<string, unknown>>;
|
|
61
75
|
footer: string;
|
|
76
|
+
linkPreview?: { worker?: string };
|
|
62
77
|
};
|
|
63
78
|
export const githubUrl: string;
|
|
64
79
|
}
|
package/app/globals.css
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
@import "./admin-ui.css";
|
|
4
4
|
|
|
5
5
|
:root {
|
|
6
|
-
--paper: #
|
|
7
|
-
--paper-deep: #
|
|
6
|
+
--paper: #faf9f5;
|
|
7
|
+
--paper-deep: #f0eee6;
|
|
8
8
|
--card: #ffffff;
|
|
9
9
|
--ink: #202124;
|
|
10
10
|
--ink-soft: #34383d;
|
|
@@ -12,28 +12,28 @@
|
|
|
12
12
|
--faint: #a1a6ad;
|
|
13
13
|
--line: #deded9;
|
|
14
14
|
--line-strong: #c9c9c2;
|
|
15
|
-
--accent: #
|
|
16
|
-
--accent-soft: #
|
|
17
|
-
--selection: #
|
|
18
|
-
--font-mono:
|
|
15
|
+
--accent: #30302e;
|
|
16
|
+
--accent-soft: #f0eee6;
|
|
17
|
+
--selection: #e8e5db;
|
|
18
|
+
--font-mono: "SFMono-Regular", Consolas,
|
|
19
19
|
"Liberation Mono", "Noto Sans Mono CJK SC", monospace;
|
|
20
|
-
--content:
|
|
20
|
+
--content: 740px;
|
|
21
21
|
--wide: 1040px;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
:root[data-theme="dark"] {
|
|
25
|
-
--paper: #
|
|
26
|
-
--paper-deep: #
|
|
27
|
-
--card: #
|
|
25
|
+
--paper: #1f1e1d;
|
|
26
|
+
--paper-deep: #262624;
|
|
27
|
+
--card: #262624;
|
|
28
28
|
--ink: #ededeb;
|
|
29
29
|
--ink-soft: #d8d8d4;
|
|
30
30
|
--muted: #a6adb6;
|
|
31
31
|
--faint: #747b84;
|
|
32
32
|
--line: #35383b;
|
|
33
33
|
--line-strong: #4b4f54;
|
|
34
|
-
--accent: #
|
|
35
|
-
--accent-soft: #
|
|
36
|
-
--selection:
|
|
34
|
+
--accent: #eeece7;
|
|
35
|
+
--accent-soft: #30302e;
|
|
36
|
+
--selection: rgb(238 236 231 / 18%);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
* {
|
|
@@ -252,11 +252,6 @@ button {
|
|
|
252
252
|
margin-top: 72px;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
.home-thought-list {
|
|
256
|
-
display: grid;
|
|
257
|
-
gap: 24px;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
255
|
.section-heading {
|
|
261
256
|
min-height: 40px;
|
|
262
257
|
margin-bottom: 21px;
|
|
@@ -1423,32 +1418,6 @@ button {
|
|
|
1423
1418
|
color: var(--accent);
|
|
1424
1419
|
}
|
|
1425
1420
|
|
|
1426
|
-
.topic-strip {
|
|
1427
|
-
display: flex;
|
|
1428
|
-
flex-wrap: wrap;
|
|
1429
|
-
gap: 8px;
|
|
1430
|
-
margin: 8px 0 38px;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
.topic-strip a {
|
|
1434
|
-
padding: 6px 10px;
|
|
1435
|
-
background: var(--paper-deep);
|
|
1436
|
-
color: var(--muted);
|
|
1437
|
-
font-size: 11px;
|
|
1438
|
-
border: 1px solid transparent;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
.topic-strip a:hover,
|
|
1442
|
-
.topic-strip a:focus-visible {
|
|
1443
|
-
color: var(--ink);
|
|
1444
|
-
border-color: var(--line-strong);
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
.topic-strip a.active {
|
|
1448
|
-
background: var(--ink);
|
|
1449
|
-
color: var(--paper);
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
1421
|
.archive-list {
|
|
1453
1422
|
border-top: 1px solid var(--line-strong);
|
|
1454
1423
|
}
|
|
@@ -1496,13 +1465,6 @@ button {
|
|
|
1496
1465
|
border: 1px solid var(--line);
|
|
1497
1466
|
}
|
|
1498
1467
|
|
|
1499
|
-
.archive-topic {
|
|
1500
|
-
color: var(--accent);
|
|
1501
|
-
font-size: 10px;
|
|
1502
|
-
font-weight: 700;
|
|
1503
|
-
letter-spacing: 0.05em;
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
1468
|
.pinned-badge {
|
|
1507
1469
|
margin-left: 9px;
|
|
1508
1470
|
display: inline-flex;
|
|
@@ -2089,12 +2051,14 @@ button {
|
|
|
2089
2051
|
|
|
2090
2052
|
.markdown-body a {
|
|
2091
2053
|
text-decoration: underline;
|
|
2092
|
-
text-decoration-color: var(--
|
|
2054
|
+
text-decoration-color: color-mix(in srgb, var(--ink) 35%, transparent);
|
|
2093
2055
|
text-underline-offset: 4px;
|
|
2094
2056
|
}
|
|
2095
2057
|
|
|
2058
|
+
/* Links stay ink; hover darkens and thickens the underline instead. */
|
|
2096
2059
|
.markdown-body a:hover {
|
|
2097
|
-
color: var(--
|
|
2060
|
+
text-decoration-color: var(--ink);
|
|
2061
|
+
text-decoration-thickness: 2px;
|
|
2098
2062
|
}
|
|
2099
2063
|
|
|
2100
2064
|
/* A per-line border stays continuous across inline code and descenders. */
|
|
@@ -2186,25 +2150,6 @@ button {
|
|
|
2186
2150
|
color: var(--accent);
|
|
2187
2151
|
}
|
|
2188
2152
|
|
|
2189
|
-
.markdown-body table {
|
|
2190
|
-
width: 100%;
|
|
2191
|
-
margin: 34px 0;
|
|
2192
|
-
border-collapse: collapse;
|
|
2193
|
-
font-size: 12px;
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
.markdown-body th,
|
|
2197
|
-
.markdown-body td {
|
|
2198
|
-
padding: 11px 13px;
|
|
2199
|
-
border: 1px solid var(--line);
|
|
2200
|
-
text-align: left;
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
.markdown-body th {
|
|
2204
|
-
background: var(--paper-deep);
|
|
2205
|
-
color: var(--ink);
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
2153
|
.markdown-body pre {
|
|
2209
2154
|
position: relative;
|
|
2210
2155
|
margin: 32px 0;
|
|
@@ -2818,84 +2763,107 @@ button {
|
|
|
2818
2763
|
color: #7f8790;
|
|
2819
2764
|
}
|
|
2820
2765
|
|
|
2766
|
+
/* References: one quiet warm-grey card after the article. */
|
|
2821
2767
|
.article-references {
|
|
2822
|
-
margin-top:
|
|
2768
|
+
margin-top: 56px;
|
|
2769
|
+
padding: 22px 24px 16px;
|
|
2770
|
+
background: var(--card-hover);
|
|
2771
|
+
border: 1px solid var(--rule);
|
|
2772
|
+
border-radius: var(--r);
|
|
2823
2773
|
}
|
|
2824
2774
|
|
|
2825
|
-
.article-references
|
|
2775
|
+
:root[data-theme="dark"] .article-references,
|
|
2776
|
+
html[data-theme="dark"] .article-references {
|
|
2777
|
+
background: var(--card);
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
.article-references > h2 {
|
|
2826
2781
|
margin: 0 0 14px;
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
font
|
|
2782
|
+
padding: 0;
|
|
2783
|
+
border: 0;
|
|
2784
|
+
font: 600 17px / 1.4 var(--sans);
|
|
2785
|
+
color: var(--ink);
|
|
2830
2786
|
}
|
|
2831
2787
|
|
|
2832
|
-
.article-references
|
|
2833
|
-
|
|
2834
|
-
border-collapse: collapse;
|
|
2835
|
-
font-size: 12px;
|
|
2788
|
+
.article-references > h2::before {
|
|
2789
|
+
content: none;
|
|
2836
2790
|
}
|
|
2837
2791
|
|
|
2838
|
-
.article-references
|
|
2792
|
+
.article-references ol {
|
|
2793
|
+
list-style: none;
|
|
2794
|
+
margin: 0;
|
|
2839
2795
|
padding: 0;
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2796
|
+
counter-reset: ref;
|
|
2797
|
+
font-size: 14px;
|
|
2798
|
+
line-height: 1.6;
|
|
2799
|
+
color: var(--soft);
|
|
2844
2800
|
}
|
|
2845
2801
|
|
|
2846
|
-
.article-references
|
|
2847
|
-
|
|
2848
|
-
|
|
2802
|
+
.article-references li {
|
|
2803
|
+
position: relative;
|
|
2804
|
+
margin: 0 0 6px;
|
|
2805
|
+
padding-left: 2.1em;
|
|
2806
|
+
counter-increment: ref;
|
|
2807
|
+
break-inside: avoid;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
.article-references li::before {
|
|
2811
|
+
content: counter(ref);
|
|
2812
|
+
position: absolute;
|
|
2813
|
+
top: 0.15em;
|
|
2814
|
+
left: 0;
|
|
2815
|
+
width: 1.5em;
|
|
2816
|
+
color: var(--muted);
|
|
2817
|
+
font-size: 12px;
|
|
2849
2818
|
font-variant-numeric: tabular-nums;
|
|
2850
2819
|
text-align: right;
|
|
2851
|
-
white-space: nowrap;
|
|
2852
2820
|
}
|
|
2853
2821
|
|
|
2854
|
-
.article-references
|
|
2855
|
-
.article-references
|
|
2856
|
-
|
|
2857
|
-
padding: 11px 13px;
|
|
2822
|
+
.article-references a,
|
|
2823
|
+
.article-references .ref-title {
|
|
2824
|
+
color: var(--text);
|
|
2858
2825
|
}
|
|
2859
2826
|
|
|
2860
|
-
.article-references
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
text-decoration:
|
|
2827
|
+
.article-references a {
|
|
2828
|
+
text-decoration: underline;
|
|
2829
|
+
text-decoration-thickness: 1px;
|
|
2830
|
+
text-underline-offset: 4px;
|
|
2831
|
+
text-decoration-color: color-mix(in srgb, var(--ink) 35%, transparent);
|
|
2832
|
+
transition: text-decoration-color 0.15s ease;
|
|
2865
2833
|
}
|
|
2866
2834
|
|
|
2867
|
-
.article-references
|
|
2868
|
-
color:
|
|
2869
|
-
text-decoration:
|
|
2835
|
+
.article-references a:hover {
|
|
2836
|
+
text-decoration-color: var(--ink);
|
|
2837
|
+
text-decoration-thickness: 2px;
|
|
2870
2838
|
}
|
|
2871
2839
|
|
|
2872
|
-
.article-references
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2840
|
+
.article-references .ref-domain {
|
|
2841
|
+
font-size: 12px;
|
|
2842
|
+
color: var(--muted);
|
|
2843
|
+
white-space: nowrap;
|
|
2876
2844
|
}
|
|
2877
2845
|
|
|
2878
|
-
|
|
2879
|
-
|
|
2846
|
+
@media (min-width: 900px) {
|
|
2847
|
+
.article-references.is-split ol {
|
|
2848
|
+
columns: 2;
|
|
2849
|
+
column-gap: 32px;
|
|
2850
|
+
}
|
|
2880
2851
|
}
|
|
2881
2852
|
|
|
2882
|
-
|
|
2883
|
-
.article-references
|
|
2884
|
-
|
|
2853
|
+
@media (max-width: 640px) {
|
|
2854
|
+
.article-references {
|
|
2855
|
+
padding: 18px 16px 12px;
|
|
2856
|
+
}
|
|
2885
2857
|
}
|
|
2886
2858
|
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
margin: 68px 0 24px;
|
|
2859
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2860
|
+
.article-references a {
|
|
2861
|
+
transition: none;
|
|
2862
|
+
}
|
|
2892
2863
|
}
|
|
2893
2864
|
|
|
2894
|
-
.article-
|
|
2895
|
-
|
|
2896
|
-
background: var(--paper-deep);
|
|
2897
|
-
color: var(--muted);
|
|
2898
|
-
font-size: 10px;
|
|
2865
|
+
.article-references + .next-entry {
|
|
2866
|
+
margin-top: 36px;
|
|
2899
2867
|
}
|
|
2900
2868
|
|
|
2901
2869
|
.webmentions,
|
|
@@ -3078,7 +3046,7 @@ button {
|
|
|
3078
3046
|
margin: 0 auto;
|
|
3079
3047
|
padding: 58px 0 120px;
|
|
3080
3048
|
display: grid;
|
|
3081
|
-
grid-template-columns: var(--column-sidebar-width) minmax(0,
|
|
3049
|
+
grid-template-columns: var(--column-sidebar-width) minmax(0, var(--content)) var(--column-sidebar-width);
|
|
3082
3050
|
gap: 54px;
|
|
3083
3051
|
justify-content: center;
|
|
3084
3052
|
}
|
|
@@ -3697,12 +3665,6 @@ button {
|
|
|
3697
3665
|
color: var(--accent);
|
|
3698
3666
|
}
|
|
3699
3667
|
|
|
3700
|
-
.footer-framework a {
|
|
3701
|
-
text-decoration: underline;
|
|
3702
|
-
text-decoration-color: var(--line-strong);
|
|
3703
|
-
text-underline-offset: 3px;
|
|
3704
|
-
}
|
|
3705
|
-
|
|
3706
3668
|
@media (max-width: 1080px) {
|
|
3707
3669
|
.post-reading-page {
|
|
3708
3670
|
--toc-width: 210px;
|
|
@@ -3917,7 +3879,7 @@ button {
|
|
|
3917
3879
|
}
|
|
3918
3880
|
|
|
3919
3881
|
.column-reading-page {
|
|
3920
|
-
grid-template-columns: minmax(0,
|
|
3882
|
+
grid-template-columns: minmax(0, var(--content));
|
|
3921
3883
|
}
|
|
3922
3884
|
|
|
3923
3885
|
.series-nav {
|
|
@@ -4202,8 +4164,7 @@ button {
|
|
|
4202
4164
|
gap: 4px 9px;
|
|
4203
4165
|
}
|
|
4204
4166
|
|
|
4205
|
-
.archive-meta time
|
|
4206
|
-
.archive-topic {
|
|
4167
|
+
.archive-meta time {
|
|
4207
4168
|
font-size: 9px;
|
|
4208
4169
|
}
|
|
4209
4170
|
|
|
@@ -4274,11 +4235,6 @@ button {
|
|
|
4274
4235
|
font-size: 17px;
|
|
4275
4236
|
}
|
|
4276
4237
|
|
|
4277
|
-
.markdown-body table {
|
|
4278
|
-
display: block;
|
|
4279
|
-
overflow-x: auto;
|
|
4280
|
-
}
|
|
4281
|
-
|
|
4282
4238
|
.markdown-body .external-embed-card,
|
|
4283
4239
|
.markdown-body .external-embed-card:has(> img) {
|
|
4284
4240
|
grid-template-columns: 1fr 22px;
|