hd-garden 0.4.0 → 0.5.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/.agents/skills/garden-interactive-chart/SKILL.md +10 -10
- package/.agents/skills/garden-ui/SKILL.md +4 -4
- 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 +2 -13
- package/app/blog/page.tsx +2 -2
- package/app/columns/[column]/[...path]/page.tsx +2 -4
- package/app/columns/page.tsx +2 -2
- package/app/components/AdminDashboard.tsx +3 -3
- package/app/components/ArticleMeasure.tsx +114 -42
- package/app/components/ArticleTags.tsx +17 -0
- package/app/components/BlogArchive.tsx +85 -33
- package/app/components/ContentEmptyState.tsx +6 -6
- 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/MarkdownImage.tsx +4 -2
- package/app/components/SiteFooter.tsx +5 -5
- package/app/components/SiteHeader.tsx +1 -5
- package/app/components/TagRedirect.tsx +17 -0
- package/app/garden/page.tsx +6 -6
- package/app/garden-modules.d.ts +0 -1
- package/app/globals.css +19 -101
- package/app/lib/a2-meta.ts +10 -0
- package/app/lib/a2-preview.ts +0 -2
- package/app/lib/content.ts +14 -8
- package/app/lib/fallback-site-config.ts +15 -15
- package/app/lib/generated-content.ts +58 -47
- package/app/lib/tag-filter.ts +114 -0
- package/app/not-found.tsx +13 -0
- package/app/page.tsx +10 -27
- package/app/styles/a2.css +96 -15
- package/app/styles/shell.css +56 -61
- package/app/styles/tokens.css +35 -22
- package/app/tags/page.tsx +20 -9
- package/package.json +1 -1
- package/scripts/build-content.mjs +3 -3
- package/scripts/check-post.mjs +0 -3
- 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/garden/check.mjs +2 -4
- package/ui/garden/enhance.mjs +57 -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 +6 -6
- package/ui/preview/dist/garden-ui.iife.js +7 -7
- package/ui/react/components/media.tsx +2 -2
- package/ui/react/ui.css +11 -11
- package/app/components/TagIndex.tsx +0 -97
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import { useSearchParams } from "next/navigation";
|
|
5
|
-
import { useState } from "react";
|
|
6
|
-
import { columns, postHref, posts
|
|
4
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
5
|
+
import { useMemo, useState } from "react";
|
|
6
|
+
import { columnIndexHref, columnOfPost, columns, postHref, posts } from "../lib/content";
|
|
7
|
+
import {
|
|
8
|
+
TAG_PARAM,
|
|
9
|
+
filterByTags,
|
|
10
|
+
selectedTags,
|
|
11
|
+
tagFilterHref,
|
|
12
|
+
tagOptions,
|
|
13
|
+
tagSummary,
|
|
14
|
+
toggleTag,
|
|
15
|
+
visibleTagOptions,
|
|
16
|
+
} from "../lib/tag-filter";
|
|
7
17
|
import { ContentEmptyState } from "./ContentEmptyState";
|
|
8
18
|
|
|
9
|
-
export const archiveHref = (topic?: string) => {
|
|
10
|
-
const params = new URLSearchParams();
|
|
11
|
-
if (topic) params.set("topic", topic);
|
|
12
|
-
const query = params.toString();
|
|
13
|
-
return query ? `/blog?${query}` : "/blog";
|
|
14
|
-
};
|
|
15
|
-
|
|
16
19
|
function day(value: string) {
|
|
17
20
|
const date = new Date(value);
|
|
18
21
|
if (Number.isNaN(date.getTime())) return value.slice(0, 10);
|
|
@@ -24,12 +27,37 @@ function day(value: string) {
|
|
|
24
27
|
}).format(date);
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
function ColumnLabel({ path }: { path: string }) {
|
|
31
|
+
const column = columnOfPost({ path });
|
|
32
|
+
return column ? <Link className="cat" href={columnIndexHref(column.slug)}>{column.title}</Link> : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const NO_TAGS: string[] = [];
|
|
36
|
+
|
|
37
|
+
/** Reads the selected tags from the URL; needs a Suspense boundary. */
|
|
27
38
|
export function BlogArchive() {
|
|
39
|
+
const router = useRouter();
|
|
40
|
+
const pathname = usePathname() || "/blog";
|
|
28
41
|
const searchParams = useSearchParams();
|
|
42
|
+
const paramKey = searchParams.getAll(TAG_PARAM).join("\u0000");
|
|
43
|
+
const selected = useMemo(() => selectedTags(paramKey ? paramKey.split("\u0000") : []), [paramKey]);
|
|
44
|
+
return <ArchiveView selected={selected} go={(next) => router.push(tagFilterHref(next, pathname), { scroll: false })} />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The full, unfiltered list. Rendered into the static page as the Suspense
|
|
49
|
+
* fallback, so the list is there before scripts run (or without them).
|
|
50
|
+
*/
|
|
51
|
+
export function BlogArchiveFallback() {
|
|
52
|
+
return <ArchiveView selected={NO_TAGS} go={(next) => window.location.assign(tagFilterHref(next))} />;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ArchiveView({ selected, go }: { selected: string[]; go: (next: string[]) => void }) {
|
|
29
56
|
const [compact, setCompact] = useState(false);
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
57
|
+
const [expanded, setExpanded] = useState(false);
|
|
58
|
+
const options = useMemo(() => tagOptions(posts, selected), [selected]);
|
|
59
|
+
const { shown, hidden } = visibleTagOptions(options, expanded);
|
|
60
|
+
const filtered = filterByTags(posts, selected)
|
|
33
61
|
.slice()
|
|
34
62
|
.sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
|
|
35
63
|
const years = new Map<string, typeof filtered>();
|
|
@@ -53,23 +81,37 @@ export function BlogArchive() {
|
|
|
53
81
|
return (
|
|
54
82
|
<div className="sh-listgrid">
|
|
55
83
|
<div>
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
{options.length ? (
|
|
85
|
+
<div className="sh-tagfilter">
|
|
86
|
+
<ul className="sh-tagchips" aria-label="按标签筛选">
|
|
87
|
+
{shown.map((option) => (
|
|
88
|
+
<li key={option.tag}>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
className="sh-tagchip"
|
|
92
|
+
aria-pressed={option.selected}
|
|
93
|
+
disabled={option.disabled}
|
|
94
|
+
onClick={() => go(toggleTag(selected, option.tag))}
|
|
95
|
+
>
|
|
96
|
+
<span className="label">#{option.tag}</span>
|
|
97
|
+
<span className="n num">{option.next}</span>
|
|
98
|
+
{option.selected ? <span className="x" aria-hidden="true">×</span> : null}
|
|
99
|
+
</button>
|
|
100
|
+
</li>
|
|
101
|
+
))}
|
|
102
|
+
{hidden > 0 || expanded ? (
|
|
103
|
+
<li>
|
|
104
|
+
<button type="button" className="sh-tagmore" aria-expanded={expanded} onClick={() => setExpanded(!expanded)}>
|
|
105
|
+
{expanded ? "收起" : `展开全部 ${options.length} 个标签`}
|
|
106
|
+
</button>
|
|
107
|
+
</li>
|
|
108
|
+
) : null}
|
|
109
|
+
</ul>
|
|
110
|
+
</div>
|
|
111
|
+
) : null}
|
|
70
112
|
<div className="sh-status">
|
|
71
|
-
<span aria-live="polite">{
|
|
72
|
-
{
|
|
113
|
+
<span aria-live="polite">{tagSummary(selected.length, filtered.length)}</span>
|
|
114
|
+
{selected.length ? <button type="button" onClick={() => go([])}>清除</button> : null}
|
|
73
115
|
<div className="sh-view" role="group" aria-label="列表密度">
|
|
74
116
|
<button type="button" aria-pressed={!compact} onClick={() => setCompact(false)}>摘要</button>
|
|
75
117
|
<button type="button" aria-pressed={compact} onClick={() => setCompact(true)}>紧凑</button>
|
|
@@ -94,16 +136,26 @@ export function BlogArchive() {
|
|
|
94
136
|
</h3>
|
|
95
137
|
{post.description ? <p>{post.description}</p> : null}
|
|
96
138
|
</div>
|
|
97
|
-
|
|
139
|
+
<ColumnLabel path={post.path} />
|
|
98
140
|
</li>
|
|
99
141
|
))}
|
|
100
142
|
</ol>
|
|
101
143
|
</section>
|
|
102
|
-
)) :
|
|
144
|
+
)) : selected.length ? (
|
|
145
|
+
<div className="sh-tagempty">
|
|
146
|
+
<p>没有同时带这些标签的文章。</p>
|
|
147
|
+
<div className="acts">
|
|
148
|
+
{selected.length > 1 ? (
|
|
149
|
+
<button type="button" onClick={() => go(selected.slice(0, -1))}>去掉「{selected[selected.length - 1]}」</button>
|
|
150
|
+
) : null}
|
|
151
|
+
<button type="button" onClick={() => go([])}>看全部文章</button>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
) : <ContentEmptyState kind="posts" />}
|
|
103
155
|
</div>
|
|
104
156
|
</div>
|
|
105
157
|
<aside className="sh-rail" aria-label="按年份跳转">
|
|
106
|
-
<h2>按年份</h2>
|
|
158
|
+
{years.size ? <h2>按年份</h2> : null}
|
|
107
159
|
<ol>
|
|
108
160
|
{[...years.keys()].map((year) => (
|
|
109
161
|
<li key={year}>
|
|
@@ -117,7 +169,7 @@ export function BlogArchive() {
|
|
|
117
169
|
<ol>
|
|
118
170
|
{series.map(([slug, entries]) => (
|
|
119
171
|
<li key={slug}>
|
|
120
|
-
<Link href={
|
|
172
|
+
<Link href={columnIndexHref(slug)}><span>{entries[0]?.columnTitle ?? slug}</span><span className="n num">{entries.length}</span></Link>
|
|
121
173
|
</li>
|
|
122
174
|
))}
|
|
123
175
|
</ol>
|
|
@@ -6,25 +6,25 @@ import { ArrowRight, Article, Books, Quotes } from "@phosphor-icons/react/ssr";
|
|
|
6
6
|
const emptyContent = {
|
|
7
7
|
posts: {
|
|
8
8
|
icon: Article,
|
|
9
|
-
eyebrow: "
|
|
9
|
+
eyebrow: "暂无博文",
|
|
10
10
|
title: "暂无博文。",
|
|
11
|
-
description: "
|
|
11
|
+
description: "文章会按时间排在这里。",
|
|
12
12
|
actionHref: "/",
|
|
13
13
|
actionLabel: "返回首页",
|
|
14
14
|
},
|
|
15
15
|
thoughts: {
|
|
16
16
|
icon: Quotes,
|
|
17
|
-
eyebrow: "
|
|
17
|
+
eyebrow: "暂无随想",
|
|
18
18
|
title: "暂无随想。",
|
|
19
|
-
description: "
|
|
19
|
+
description: "短笔记和链接会在这里。",
|
|
20
20
|
actionHref: "/blog",
|
|
21
21
|
actionLabel: "先读博文",
|
|
22
22
|
},
|
|
23
23
|
columns: {
|
|
24
24
|
icon: Books,
|
|
25
|
-
eyebrow: "
|
|
25
|
+
eyebrow: "暂无专栏",
|
|
26
26
|
title: "暂无专栏。",
|
|
27
|
-
description: "
|
|
27
|
+
description: "一组文章会按顺序列在这里。",
|
|
28
28
|
actionHref: "/blog",
|
|
29
29
|
actionLabel: "浏览全部博文",
|
|
30
30
|
},
|
|
@@ -23,7 +23,6 @@ const postSource = `---
|
|
|
23
23
|
title: 我的第一篇文章
|
|
24
24
|
date: 2026-08-03 09:00:00
|
|
25
25
|
description: 这篇文章记录我为什么重新开始写博客。
|
|
26
|
-
topic: 写作
|
|
27
26
|
tags: [Markdown, 博客]
|
|
28
27
|
pinned: false
|
|
29
28
|
cover: writing/first-post.jpg
|
|
@@ -100,7 +99,7 @@ const guides: Guide[] = [
|
|
|
100
99
|
<>文件名就是地址。标题和日期写在开头。先存着就标成草稿。</>,
|
|
101
100
|
<>图片、音频、视频和链接按顺序放进 <code>media</code>。</>,
|
|
102
101
|
],
|
|
103
|
-
result: "
|
|
102
|
+
result: "它会出现在随想页,并且有自己的链接。",
|
|
104
103
|
path: "content/thoughts/walk-after-rain.md",
|
|
105
104
|
source: thoughtSource,
|
|
106
105
|
language: "markdown",
|
|
@@ -5,7 +5,7 @@ const embedTypes = [
|
|
|
5
5
|
},
|
|
6
6
|
{
|
|
7
7
|
title: "X / Twitter",
|
|
8
|
-
description: "
|
|
8
|
+
description: "显示成一张带出处的卡片。",
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
title: "微信公众号",
|
|
@@ -13,7 +13,7 @@ const embedTypes = [
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
title: "其他网站",
|
|
16
|
-
description: "
|
|
16
|
+
description: "自己写标题、摘要和封面。",
|
|
17
17
|
},
|
|
18
18
|
];
|
|
19
19
|
|
|
@@ -53,7 +53,7 @@ export function GardenMediaGuide() {
|
|
|
53
53
|
</ul>
|
|
54
54
|
|
|
55
55
|
<p className="garden-media-boundary">
|
|
56
|
-
|
|
56
|
+
摘要写在文章里。原站打不开,文章还在。需要时用 <code>make embed URL=...</code> 拉一次,看过再贴进去。
|
|
57
57
|
</p>
|
|
58
58
|
</section>
|
|
59
59
|
);
|
|
@@ -253,7 +253,7 @@ export async function GithubProfile() {
|
|
|
253
253
|
<header>
|
|
254
254
|
<div>
|
|
255
255
|
<p className="eyebrow">PINNED / 置顶仓库</p>
|
|
256
|
-
<h3 id="github-pinned-title"
|
|
256
|
+
<h3 id="github-pinned-title">正在做的项目</h3>
|
|
257
257
|
</div>
|
|
258
258
|
<PushPin size={20} aria-hidden="true" />
|
|
259
259
|
</header>
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, type ComponentPropsWithoutRef } from "react";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// react-markdown also hands over its syntax-tree `node`; it must not reach the DOM.
|
|
6
|
+
type MarkdownImageProps = ComponentPropsWithoutRef<"img"> & { node?: unknown };
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
9
|
+
export function MarkdownImage({ alt = "", src, node, ...props }: MarkdownImageProps) {
|
|
8
10
|
const [open, setOpen] = useState(false);
|
|
9
11
|
const description = alt || "查看原图";
|
|
10
12
|
|
|
@@ -9,13 +9,13 @@ export function SiteFooter() {
|
|
|
9
9
|
© {new Date().getFullYear()} {siteConfig.author.name} · {siteConfig.footer}
|
|
10
10
|
<span className="footer-framework"> · 基于 <a href="https://github.com/hedon954/garden" target="_blank" rel="noreferrer">Garden</a> 构建</span>
|
|
11
11
|
</p>
|
|
12
|
-
<div>
|
|
13
|
-
<a href={githubUrl}
|
|
14
|
-
<GithubLogo size={
|
|
12
|
+
<div className="sh-foot-links">
|
|
13
|
+
<a href={githubUrl} rel="me">
|
|
14
|
+
<GithubLogo size={16} aria-hidden="true" />
|
|
15
15
|
GitHub
|
|
16
16
|
</a>
|
|
17
|
-
<a href="/rss.xml"
|
|
18
|
-
<Rss size={
|
|
17
|
+
<a href="/rss.xml">
|
|
18
|
+
<Rss size={16} aria-hidden="true" />
|
|
19
19
|
RSS
|
|
20
20
|
</a>
|
|
21
21
|
</div>
|
|
@@ -197,7 +197,6 @@ export function SiteHeader() {
|
|
|
197
197
|
{ name: "title", weight: 0.5 },
|
|
198
198
|
{ name: "description", weight: 0.3 },
|
|
199
199
|
{ name: "content", weight: 0.15 },
|
|
200
|
-
{ name: "topic", weight: 0.05 },
|
|
201
200
|
],
|
|
202
201
|
threshold: 0.36,
|
|
203
202
|
ignoreLocation: true,
|
|
@@ -401,10 +400,7 @@ export function SiteHeader() {
|
|
|
401
400
|
className={`search-result${activeResult === index ? " active" : ""}`}
|
|
402
401
|
>
|
|
403
402
|
<span>
|
|
404
|
-
|
|
405
|
-
text={result.item.topic}
|
|
406
|
-
ranges={rangesFor(result.matches, "topic")}
|
|
407
|
-
/>
|
|
403
|
+
{result.item.section}
|
|
408
404
|
</span>
|
|
409
405
|
<strong>
|
|
410
406
|
<HighlightedText
|
|
@@ -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
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: #
|
|
15
|
+
--accent: #30302e;
|
|
16
|
+
--accent-soft: #f0eee6;
|
|
17
|
+
--selection: #e8e5db;
|
|
18
18
|
--font-mono: var(--font-geist-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;
|
|
@@ -2879,25 +2824,10 @@ button {
|
|
|
2879
2824
|
color: var(--accent);
|
|
2880
2825
|
}
|
|
2881
2826
|
|
|
2882
|
-
.article-references + .article-tags,
|
|
2883
2827
|
.article-references + .next-entry {
|
|
2884
2828
|
margin-top: 36px;
|
|
2885
2829
|
}
|
|
2886
2830
|
|
|
2887
|
-
.article-tags {
|
|
2888
|
-
display: flex;
|
|
2889
|
-
flex-wrap: wrap;
|
|
2890
|
-
gap: 8px;
|
|
2891
|
-
margin: 68px 0 24px;
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
.article-tags span {
|
|
2895
|
-
padding: 4px 8px;
|
|
2896
|
-
background: var(--paper-deep);
|
|
2897
|
-
color: var(--muted);
|
|
2898
|
-
font-size: 10px;
|
|
2899
|
-
}
|
|
2900
|
-
|
|
2901
2831
|
.webmentions,
|
|
2902
2832
|
.comments {
|
|
2903
2833
|
margin-top: 78px;
|
|
@@ -3078,7 +3008,7 @@ button {
|
|
|
3078
3008
|
margin: 0 auto;
|
|
3079
3009
|
padding: 58px 0 120px;
|
|
3080
3010
|
display: grid;
|
|
3081
|
-
grid-template-columns: var(--column-sidebar-width) minmax(0,
|
|
3011
|
+
grid-template-columns: var(--column-sidebar-width) minmax(0, var(--content)) var(--column-sidebar-width);
|
|
3082
3012
|
gap: 54px;
|
|
3083
3013
|
justify-content: center;
|
|
3084
3014
|
}
|
|
@@ -3697,12 +3627,6 @@ button {
|
|
|
3697
3627
|
color: var(--accent);
|
|
3698
3628
|
}
|
|
3699
3629
|
|
|
3700
|
-
.footer-framework a {
|
|
3701
|
-
text-decoration: underline;
|
|
3702
|
-
text-decoration-color: var(--line-strong);
|
|
3703
|
-
text-underline-offset: 3px;
|
|
3704
|
-
}
|
|
3705
|
-
|
|
3706
3630
|
@media (max-width: 1080px) {
|
|
3707
3631
|
.post-reading-page {
|
|
3708
3632
|
--toc-width: 210px;
|
|
@@ -3917,7 +3841,7 @@ button {
|
|
|
3917
3841
|
}
|
|
3918
3842
|
|
|
3919
3843
|
.column-reading-page {
|
|
3920
|
-
grid-template-columns: minmax(0,
|
|
3844
|
+
grid-template-columns: minmax(0, var(--content));
|
|
3921
3845
|
}
|
|
3922
3846
|
|
|
3923
3847
|
.series-nav {
|
|
@@ -4202,8 +4126,7 @@ button {
|
|
|
4202
4126
|
gap: 4px 9px;
|
|
4203
4127
|
}
|
|
4204
4128
|
|
|
4205
|
-
.archive-meta time
|
|
4206
|
-
.archive-topic {
|
|
4129
|
+
.archive-meta time {
|
|
4207
4130
|
font-size: 9px;
|
|
4208
4131
|
}
|
|
4209
4132
|
|
|
@@ -4274,11 +4197,6 @@ button {
|
|
|
4274
4197
|
font-size: 17px;
|
|
4275
4198
|
}
|
|
4276
4199
|
|
|
4277
|
-
.markdown-body table {
|
|
4278
|
-
display: block;
|
|
4279
|
-
overflow-x: auto;
|
|
4280
|
-
}
|
|
4281
|
-
|
|
4282
4200
|
.markdown-body .external-embed-card,
|
|
4283
4201
|
.markdown-body .external-embed-card:has(> img) {
|
|
4284
4202
|
grid-template-columns: 1fr 22px;
|
package/app/lib/a2-meta.ts
CHANGED
|
@@ -13,6 +13,16 @@ export function demoDateLabel(value: string) {
|
|
|
13
13
|
return `${date.getUTCFullYear()} 年 ${date.getUTCMonth() + 1} 月 ${date.getUTCDate()} 日`;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** Home list label. The current Shanghai year drops the year so the column stays narrow. */
|
|
17
|
+
export function homeListDate(value: string, now = new Date()) {
|
|
18
|
+
const date = shanghaiDate(value);
|
|
19
|
+
if (!date) return value;
|
|
20
|
+
const today = shanghaiDate(now.toISOString());
|
|
21
|
+
const monthDay = `${date.getUTCMonth() + 1} 月 ${date.getUTCDate()} 日`;
|
|
22
|
+
if (today && date.getUTCFullYear() === today.getUTCFullYear()) return monthDay;
|
|
23
|
+
return `${date.getUTCFullYear()} 年 ${monthDay}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export function demoDateTime(value: string) {
|
|
17
27
|
const date = shanghaiDate(value);
|
|
18
28
|
if (!date) return value.slice(0, 10);
|
package/app/lib/a2-preview.ts
CHANGED
|
@@ -17,7 +17,6 @@ export type A2Article = {
|
|
|
17
17
|
title: string;
|
|
18
18
|
description: string;
|
|
19
19
|
date: string;
|
|
20
|
-
topic: string;
|
|
21
20
|
cover: string;
|
|
22
21
|
original: string;
|
|
23
22
|
content: string;
|
|
@@ -42,7 +41,6 @@ export function readA2Article(slug: string): A2Article | null {
|
|
|
42
41
|
title: String(data.title ?? slug),
|
|
43
42
|
description: String(data.description ?? ""),
|
|
44
43
|
date,
|
|
45
|
-
topic: String(data.topic ?? ""),
|
|
46
44
|
cover: cover && !/^https?:/iu.test(cover) ? `https://hedon.top/media/covers/${cover}` : cover,
|
|
47
45
|
original: originals[slug] ?? "",
|
|
48
46
|
content: parsed.content,
|