hd-garden 0.3.12 → 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/admin/page.tsx +11 -15
- package/app/admin/ui/page.tsx +1 -1
- 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/AdminDashboard.tsx +24 -34
- package/app/components/AdminUiPanels.tsx +5 -5
- package/app/components/AdminUiShell.tsx +7 -7
- package/app/components/ArticleMeasure.tsx +95 -0
- package/app/components/ArticleTitle.tsx +20 -0
- package/app/components/BlogArchive.tsx +106 -102
- package/app/components/GardenAdminGuide.tsx +38 -45
- package/app/components/GardenContentGuide.tsx +22 -106
- package/app/components/GardenMediaGuide.tsx +24 -45
- package/app/components/MarkdownArticle.tsx +145 -66
- package/app/components/MermaidDiagram.tsx +44 -71
- package/app/components/PageIntro.tsx +2 -2
- 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/UiStudio.tsx +2 -2
- 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/page.tsx +85 -223
- package/app/garden-modules.d.ts +2 -0
- package/app/globals.css +947 -88
- 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/fallback-site-config.ts +1 -1
- 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 +2 -2
- package/public/images/garden/admin-studio.png +0 -0
- 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 +2 -2
- package/templates/garden-blocks.md +128 -0
- package/templates/garden-snippets.plist +121 -0
- package/templates/site/site.config.yaml +1 -1
- 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
|
+
}
|
package/app/admin/page.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
|
-
import { ArrowSquareOut, GithubLogo
|
|
2
|
+
import { ArrowSquareOut, GithubLogo } from "@phosphor-icons/react/ssr";
|
|
3
3
|
import { AdminDashboard } from "../components/AdminDashboard";
|
|
4
4
|
import { getAdminUser, isGitHubLoginAvailable } from "../lib/admin-auth";
|
|
5
5
|
import { columns, posts, thoughts } from "../lib/content";
|
|
@@ -27,17 +27,15 @@ export default async function AdminPage({
|
|
|
27
27
|
return (
|
|
28
28
|
<main className="admin-access-page">
|
|
29
29
|
<div className="admin-access-card">
|
|
30
|
-
<
|
|
31
|
-
<p
|
|
32
|
-
<h1>内容后台独立运行。</h1>
|
|
33
|
-
<p>公开博客由 GitHub Pages 静态托管;发布和分发请前往独立后台。</p>
|
|
30
|
+
<h1>后台在另一个地址。</h1>
|
|
31
|
+
<p>这个页面是公开博客。写随想、上传和转发,去后台。</p>
|
|
34
32
|
<div className="admin-access-actions">
|
|
35
33
|
{adminUrl ? (
|
|
36
34
|
<a className="admin-github-login" href={adminUrl}>
|
|
37
|
-
|
|
35
|
+
打开后台 <ArrowSquareOut size={15} />
|
|
38
36
|
</a>
|
|
39
37
|
) : (
|
|
40
|
-
<p
|
|
38
|
+
<p>还没有填写后台地址。</p>
|
|
41
39
|
)}
|
|
42
40
|
<Link href="/">返回公开博客</Link>
|
|
43
41
|
</div>
|
|
@@ -52,22 +50,20 @@ export default async function AdminPage({
|
|
|
52
50
|
return (
|
|
53
51
|
<main className="admin-access-page">
|
|
54
52
|
<div className="admin-access-card">
|
|
55
|
-
<
|
|
56
|
-
<p className="eyebrow">{siteConfig.name.toUpperCase()} / ADMIN</p>
|
|
57
|
-
<h1>用 GitHub 身份进入内容管理后台。</h1>
|
|
53
|
+
<h1>登录后台</h1>
|
|
58
54
|
<p>
|
|
59
55
|
{query.auth === "denied"
|
|
60
|
-
?
|
|
56
|
+
? `@${query.account ?? "这个账号"} 不在允许名单里。`
|
|
61
57
|
: oauthReady
|
|
62
|
-
?
|
|
58
|
+
? `用 GitHub 登录。只有 ${siteConfig.name} 允许的账号能进来。`
|
|
63
59
|
: process.env.GARDEN_DEV === "1"
|
|
64
|
-
? "
|
|
65
|
-
: "GitHub
|
|
60
|
+
? "这台机器还没登录 GitHub。在终端运行 gh auth login,然后重新启动。"
|
|
61
|
+
: "还没配好 GitHub 登录。先填后台的环境变量。"}
|
|
66
62
|
</p>
|
|
67
63
|
<div className="admin-access-actions">
|
|
68
64
|
{oauthReady && (
|
|
69
65
|
<Link className="admin-github-login" href="/api/auth/github">
|
|
70
|
-
<GithubLogo size={18} weight="fill" />
|
|
66
|
+
<GithubLogo size={18} weight="fill" /> 用 GitHub 登录
|
|
71
67
|
</Link>
|
|
72
68
|
)}
|
|
73
69
|
<Link href="/">
|
package/app/admin/ui/page.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { UiStudio } from "../../components/UiStudio";
|
|
|
3
3
|
import { modelConfig, recipeSources, remainingDrafts } from "../../lib/ui-model";
|
|
4
4
|
|
|
5
5
|
export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
|
|
6
|
-
export const metadata = { title: "
|
|
6
|
+
export const metadata = { title: "写作台", robots: { index: false, follow: false } };
|
|
7
7
|
|
|
8
8
|
export default async function UiStudioPage() {
|
|
9
9
|
if (isStaticExport()) return <StaticAdminNotice />;
|
|
@@ -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
|
}
|
|
@@ -8,9 +8,6 @@ import {
|
|
|
8
8
|
ArrowUpRight,
|
|
9
9
|
CheckCircle,
|
|
10
10
|
Copy,
|
|
11
|
-
DotsThree,
|
|
12
|
-
FileText,
|
|
13
|
-
LinkSimple,
|
|
14
11
|
PaperPlaneTilt,
|
|
15
12
|
Plus,
|
|
16
13
|
Trash,
|
|
@@ -55,7 +52,7 @@ function MediaFields({
|
|
|
55
52
|
<div className="admin-field-heading">
|
|
56
53
|
<div>
|
|
57
54
|
<span>附件</span>
|
|
58
|
-
<small
|
|
55
|
+
<small>一条随想里可以混放图片、音频、视频和链接。</small>
|
|
59
56
|
</div>
|
|
60
57
|
<button type="button" className="admin-quiet-button" onClick={() => onChange([...attachments, emptyAttachment()])}>
|
|
61
58
|
<Plus size={14} /> 添加附件
|
|
@@ -153,20 +150,17 @@ function IntelligentUiCard({ ui }: { ui: UiOverview }) {
|
|
|
153
150
|
return (
|
|
154
151
|
<section className="admin-panel admin-ui-card">
|
|
155
152
|
<div className="admin-panel-heading">
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
<h2>交互块</h2>
|
|
159
|
-
</div>
|
|
160
|
-
<Link href="/admin/ui">打开工作台</Link>
|
|
153
|
+
<h2>文章里的交互</h2>
|
|
154
|
+
<Link href="/admin/ui">打开写作台</Link>
|
|
161
155
|
</div>
|
|
162
156
|
<p className="ui-studio-meta">
|
|
163
|
-
{ui.modelName ? ui.modelName : "
|
|
157
|
+
{ui.modelName ? ui.modelName : "还没接上模型"}
|
|
164
158
|
{" · "}
|
|
165
159
|
{counts
|
|
166
160
|
? <Link href="/admin/ui/blocks">{`未编译 ${counts.uncompiled} · 过期 ${counts.stale} · 有错 ${counts.error} · 正常 ${counts.ok}`}</Link>
|
|
167
|
-
: failed ? "
|
|
161
|
+
: failed ? "没读到文章里的块" : "正在查看文章…"}
|
|
168
162
|
{" · "}
|
|
169
|
-
<Link href="/admin/ui/catalog"
|
|
163
|
+
<Link href="/admin/ui/catalog">看组件</Link>
|
|
170
164
|
</p>
|
|
171
165
|
</section>
|
|
172
166
|
);
|
|
@@ -367,7 +361,7 @@ export function AdminDashboard({
|
|
|
367
361
|
return (
|
|
368
362
|
<main className="admin-shell">
|
|
369
363
|
<header className="admin-topbar">
|
|
370
|
-
<Link className="admin-brand" href="/admin">
|
|
364
|
+
<Link className="admin-brand" href="/admin">Garden<span>后台</span></Link>
|
|
371
365
|
<div>
|
|
372
366
|
<span>{userName}</span>
|
|
373
367
|
<form action="/api/auth/logout" method="post">
|
|
@@ -380,16 +374,15 @@ export function AdminDashboard({
|
|
|
380
374
|
</header>
|
|
381
375
|
|
|
382
376
|
<section className="admin-hero">
|
|
383
|
-
<
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
</dl>
|
|
377
|
+
<h1>写一条随想。</h1>
|
|
378
|
+
<p>长文仍在编辑器里改。这里写随想、上传附件、置顶,以及发到其他网站。</p>
|
|
379
|
+
<p className="admin-quiet-count">
|
|
380
|
+
{publishedCount + staticThoughtCount} 条公开随想
|
|
381
|
+
{" · "}
|
|
382
|
+
{catalogEntries.filter((entry) => entry.kind === "post").length} 篇博文
|
|
383
|
+
{" · "}
|
|
384
|
+
{catalogEntries.filter((entry) => entry.kind === "column").length} 篇专栏
|
|
385
|
+
</p>
|
|
393
386
|
</section>
|
|
394
387
|
|
|
395
388
|
<IntelligentUiCard ui={ui} />
|
|
@@ -400,12 +393,11 @@ export function AdminDashboard({
|
|
|
400
393
|
<section className="admin-grid">
|
|
401
394
|
<div className="admin-panel admin-compose-panel">
|
|
402
395
|
<div className="admin-panel-heading">
|
|
403
|
-
<
|
|
404
|
-
<PaperPlaneTilt size={22} />
|
|
396
|
+
<h2>写一条</h2>
|
|
405
397
|
</div>
|
|
406
|
-
<label>标题<input value={title} onChange={(event) => setTitle(event.target.value)} placeholder="
|
|
407
|
-
<label
|
|
408
|
-
<label
|
|
398
|
+
<label>标题<input value={title} onChange={(event) => setTitle(event.target.value)} placeholder="标题" /></label>
|
|
399
|
+
<label>正文<textarea value={content} onChange={(event) => setContent(event.target.value)} placeholder="写下来。可以用 Markdown。" rows={8} /></label>
|
|
400
|
+
<label>标签<input value={tags} onChange={(event) => setTags(event.target.value)} placeholder="用逗号分开,例如:夜晚, 散步" /></label>
|
|
409
401
|
<MediaFields attachments={attachments} onChange={setAttachments} onUpload={uploadMedia} uploading={uploadingCount > 0} />
|
|
410
402
|
<div className="admin-actions">
|
|
411
403
|
<button type="button" className="admin-secondary-button" disabled={isSaving || uploadingCount > 0} onClick={() => publishThought("draft")}>保存草稿</button>
|
|
@@ -415,11 +407,10 @@ export function AdminDashboard({
|
|
|
415
407
|
|
|
416
408
|
<div className="admin-panel admin-thoughts-panel">
|
|
417
409
|
<div className="admin-panel-heading">
|
|
418
|
-
<
|
|
419
|
-
<DotsThree size={23} />
|
|
410
|
+
<h2>草稿和已发布</h2>
|
|
420
411
|
</div>
|
|
421
412
|
<div className="admin-thought-list">
|
|
422
|
-
{thoughts.length === 0 ? <p className="admin-empty"
|
|
413
|
+
{thoughts.length === 0 ? <p className="admin-empty">还没有在这里写过随想。</p> : thoughts.map((thought) => (
|
|
423
414
|
<article key={thought.id}>
|
|
424
415
|
<div><span className={`admin-status ${thought.status}`}>{thought.status === "published" ? "已发布" : "草稿"}</span><h3>{thought.title}</h3><p>{thought.content.slice(0, 76)}{thought.content.length > 76 ? "…" : ""}</p></div>
|
|
425
416
|
<div className="admin-item-actions">
|
|
@@ -434,10 +425,9 @@ export function AdminDashboard({
|
|
|
434
425
|
|
|
435
426
|
<section className="admin-panel admin-distribution-panel">
|
|
436
427
|
<div className="admin-panel-heading">
|
|
437
|
-
<
|
|
438
|
-
<FileText size={22} />
|
|
428
|
+
<h2>置顶,或发到别处</h2>
|
|
439
429
|
</div>
|
|
440
|
-
<p className="admin-distribution-note"
|
|
430
|
+
<p className="admin-distribution-note">正文仍在仓库里改。这里可以置顶,发到 X,或同步到 CSDN、知乎、掘金。没配接口时,复制标题和链接。</p>
|
|
441
431
|
<div className="admin-catalog">
|
|
442
432
|
{catalogEntries.map((entry) => (
|
|
443
433
|
<article key={`${entry.kind}-${entry.column ?? ""}-${entry.path}`}>
|
|
@@ -83,9 +83,9 @@ export function AdminBlocks({ siteUrl }: { siteUrl: string }) {
|
|
|
83
83
|
return (
|
|
84
84
|
<section className="admin-panel ui-admin-blocks">
|
|
85
85
|
<div className="admin-panel-heading">
|
|
86
|
-
<
|
|
86
|
+
<h2>文章里的块</h2>
|
|
87
87
|
</div>
|
|
88
|
-
<p className="ui-studio-meta"
|
|
88
|
+
<p className="ui-studio-meta">这里是仓库里已经推送的内容,包括草稿。还没推的本地改动看不到。</p>
|
|
89
89
|
<div className="ui-studio-switch" role="group" aria-label="按状态筛选">
|
|
90
90
|
{(["todo", "uncompiled", "stale", "error", "ok", "all"] as const).map((value) => (
|
|
91
91
|
<button key={value} type="button" aria-pressed={filter === value} onClick={() => setFilter(value)}>
|
|
@@ -115,7 +115,7 @@ export function AdminBlocks({ siteUrl }: { siteUrl: string }) {
|
|
|
115
115
|
<td><span className={`ui-admin-status ui-admin-status-${block.status}`}>{STATUS_LABELS[block.status]}</span></td>
|
|
116
116
|
<td>{block.caption ?? block.intent?.split("\n")[0] ?? ""}{block.diagnostics.length ? `(${block.diagnostics.length} 条诊断)` : ""}</td>
|
|
117
117
|
<td className="ui-admin-actions">
|
|
118
|
-
<Link href={`/admin/ui?post=${encodeURIComponent(file.path)}&line=${block.startLine}`}
|
|
118
|
+
<Link href={`/admin/ui?post=${encodeURIComponent(file.path)}&line=${block.startLine}`}>在写作台打开</Link>
|
|
119
119
|
{block.status !== "ok" ? (
|
|
120
120
|
<button type="button" onClick={() => copyPrompt(file, block)}>
|
|
121
121
|
{copied === `${file.path}:${block.startLine}` ? "已复制" : "复制修复提示词"}
|
|
@@ -218,9 +218,9 @@ export function AdminEvals({ cases, configured }: { cases: EvalCase[]; configure
|
|
|
218
218
|
return (
|
|
219
219
|
<section className="admin-panel ui-admin-evals">
|
|
220
220
|
<div className="admin-panel-heading">
|
|
221
|
-
<
|
|
221
|
+
<h2>模型自检</h2>
|
|
222
222
|
</div>
|
|
223
|
-
<p className="ui-studio-meta"
|
|
223
|
+
<p className="ui-studio-meta">会逐条调用写作台,一共 {cases.length} 次,计入每小时次数。换了模型或提示词之后跑一次。</p>
|
|
224
224
|
<div className="admin-actions">
|
|
225
225
|
<button type="button" className="admin-primary-button" disabled={!configured || running} onClick={run}>{running ? `正在跑 ${results.length}/${cases.length}…` : "开始自检"}</button>
|
|
226
226
|
</div>
|