Refine Tabularium into a readable light documentation workspace
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 4m26s
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 4m26s
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export function CodeBlock({
|
||||
code,
|
||||
language = "text",
|
||||
}: {
|
||||
code: string;
|
||||
language?: string;
|
||||
}) {
|
||||
const [status, setStatus] = useState("复制");
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setStatus("已复制");
|
||||
} catch {
|
||||
setStatus("复制失败,请手动选择");
|
||||
}
|
||||
setTimeout(() => setStatus("复制"), 2400);
|
||||
}
|
||||
return (
|
||||
<div className="code-block">
|
||||
<div className="code-toolbar">
|
||||
<span>{language === "sh" ? "Terminal" : language}</span>
|
||||
<button onClick={copy} aria-label="复制代码">
|
||||
{status}
|
||||
</button>
|
||||
<span className="sr-only" role="status">
|
||||
{status === "复制" ? "" : status}
|
||||
</span>
|
||||
</div>
|
||||
<pre>
|
||||
<code>
|
||||
{code
|
||||
.replace(/\n$/, "")
|
||||
.split("\n")
|
||||
.map((line, i) => (
|
||||
<span className="code-line" key={i}>
|
||||
<span className="line-number" aria-hidden="true">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span>
|
||||
{/^(pnpm|uv)\b/.test(line) ? (
|
||||
<>
|
||||
<span className="code-command">{line.split(" ")[0]}</span>
|
||||
{line.slice(line.indexOf(" "))}
|
||||
</>
|
||||
) : (
|
||||
line || "\u00a0"
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import Link from "next/link";
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import type { DocLink } from "@/lib/docs";
|
||||
import { sectionFor } from "@/lib/navigation";
|
||||
|
||||
export function Brand() {
|
||||
return (
|
||||
<Link className="brand" href="/" aria-label="Tabularium 首页">
|
||||
<span className="brand-mark" aria-hidden="true">
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</span>
|
||||
<span>
|
||||
Endor <span className="brand-divider">/</span>{" "}
|
||||
<strong>Tabularium</strong>
|
||||
</span>
|
||||
<span className="docs-badge">文档</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
export function Navigation({
|
||||
docs,
|
||||
current,
|
||||
onNavigate,
|
||||
}: {
|
||||
docs: DocLink[];
|
||||
current?: string;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
const groups = [...new Set(docs.map((d) => sectionFor(d.slug).group))];
|
||||
return (
|
||||
<nav aria-label="文档导航">
|
||||
<Link
|
||||
onClick={onNavigate}
|
||||
className={`nav-home ${!current ? "selected" : ""}`}
|
||||
href="/"
|
||||
>
|
||||
▦ <span>文档概览</span>
|
||||
</Link>
|
||||
{groups.map((group) => (
|
||||
<div className="nav-group" key={group}>
|
||||
<p>{group}</p>
|
||||
{docs
|
||||
.filter((d) => sectionFor(d.slug).group === group)
|
||||
.map((d) => (
|
||||
<Link
|
||||
onClick={onNavigate}
|
||||
aria-current={d.slug === current ? "page" : undefined}
|
||||
className="nav-item"
|
||||
href={`/docs/${d.slug}`}
|
||||
key={d.slug}
|
||||
>
|
||||
{d.title}
|
||||
{d.slug.includes("draft") && (
|
||||
<span className="tiny-dot" aria-label="草案" />
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<div className="nav-note">
|
||||
<span className="status-dot" /> 与代码一起演进
|
||||
<p>文字、设计与图形的共同档案。</p>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
export function Shell({
|
||||
docs,
|
||||
current,
|
||||
children,
|
||||
}: {
|
||||
docs: DocLink[];
|
||||
current?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const dialog = useRef<HTMLDialogElement>(null);
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const trigger = useRef<HTMLButtonElement>(null);
|
||||
const [query, setQuery] = useState("");
|
||||
const [selected, setSelected] = useState(0);
|
||||
const [menu, setMenu] = useState(false);
|
||||
const results = docs.filter((d) =>
|
||||
`${d.title} ${d.searchText}`
|
||||
.toLocaleLowerCase()
|
||||
.includes(query.trim().toLocaleLowerCase()),
|
||||
);
|
||||
function openSearch() {
|
||||
setQuery("");
|
||||
setSelected(0);
|
||||
dialog.current?.showModal();
|
||||
input.current?.focus();
|
||||
}
|
||||
function closeSearch() {
|
||||
dialog.current?.close();
|
||||
trigger.current?.focus();
|
||||
}
|
||||
useEffect(() => {
|
||||
function keyboard(e: KeyboardEvent) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
||||
e.preventDefault();
|
||||
dialog.current?.showModal();
|
||||
input.current?.focus();
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", keyboard);
|
||||
return () => document.removeEventListener("keydown", keyboard);
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<a className="skip-link" href="#content">
|
||||
跳到正文
|
||||
</a>
|
||||
<header className="site-header">
|
||||
<Brand />
|
||||
<nav className="top-nav" aria-label="主导航">
|
||||
<Link href="/">文档概览</Link>
|
||||
<Link href="/docs/02-mounting-draft">架构设计</Link>
|
||||
<a href="https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs">
|
||||
仓库 ↗
|
||||
</a>
|
||||
</nav>
|
||||
<button
|
||||
ref={trigger}
|
||||
className="search-trigger"
|
||||
aria-label="搜索文档"
|
||||
onClick={openSearch}
|
||||
>
|
||||
<span aria-hidden="true">⌕</span>
|
||||
<span>搜索文档…</span>
|
||||
<kbd>⌘ K</kbd>
|
||||
</button>
|
||||
</header>
|
||||
<div className="mobile-bar">
|
||||
<button
|
||||
onClick={() => setMenu(!menu)}
|
||||
aria-expanded={menu}
|
||||
aria-controls="mobile-navigation"
|
||||
>
|
||||
☰ <span>文档目录</span>
|
||||
<span>{menu ? "−" : "+"}</span>
|
||||
</button>
|
||||
</div>
|
||||
{menu && (
|
||||
<div className="mobile-navigation" id="mobile-navigation">
|
||||
<Navigation
|
||||
docs={docs}
|
||||
current={current}
|
||||
onNavigate={() => setMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
<dialog
|
||||
ref={dialog}
|
||||
className="search-dialog"
|
||||
aria-label="搜索文档"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) closeSearch();
|
||||
}}
|
||||
onClose={() => trigger.current?.focus()}
|
||||
>
|
||||
<div className="search-input-row">
|
||||
<span aria-hidden="true">⌕</span>
|
||||
<input
|
||||
ref={input}
|
||||
value={query}
|
||||
placeholder="搜索文档、命令或概念…"
|
||||
aria-label="搜索文档内容"
|
||||
aria-controls="search-results"
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
setSelected(0);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setSelected((i) =>
|
||||
Math.max(0, Math.min(i + 1, results.length - 1)),
|
||||
);
|
||||
}
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setSelected((i) => Math.max(i - 1, 0));
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
dialog.current
|
||||
?.querySelector<HTMLAnchorElement>(
|
||||
`[data-result="${selected}"]`,
|
||||
)
|
||||
?.click();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button onClick={closeSearch} aria-label="关闭搜索">
|
||||
Esc
|
||||
</button>
|
||||
</div>
|
||||
<div id="search-results" className="search-results">
|
||||
<p className="eyebrow" role="status">
|
||||
{query ? `${results.length} 篇匹配文档` : "浏览文档"}
|
||||
</p>
|
||||
{results.map((d, i) => (
|
||||
<Link
|
||||
data-result={i}
|
||||
className={
|
||||
selected === i ? "search-result selected" : "search-result"
|
||||
}
|
||||
onClick={closeSearch}
|
||||
key={d.slug}
|
||||
href={`/docs/${d.slug}`}
|
||||
>
|
||||
<span>{sectionFor(d.slug).group}</span>
|
||||
<strong>
|
||||
{d.title} <span aria-hidden="true">↗</span>
|
||||
</strong>
|
||||
<small>{sectionFor(d.slug).description}</small>
|
||||
</Link>
|
||||
))}
|
||||
{!results.length && (
|
||||
<p className="empty-search">
|
||||
没有找到相关文档。试试「节点」「SVG」或「pnpm」。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="search-footer">
|
||||
↑ ↓ 选择 <span>Enter 打开 · Esc 关闭</span>
|
||||
</div>
|
||||
</dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,22 +4,24 @@ import path from "node:path";
|
||||
const directory = path.resolve(process.cwd(), "../../content/docs");
|
||||
const validSlug = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||
|
||||
export type DocLink = { slug: string; title: string };
|
||||
export type DocLink = { slug: string; title: string; searchText: string };
|
||||
export type Doc = DocLink & { content: string };
|
||||
|
||||
export async function readDoc(slug: string): Promise<Doc> {
|
||||
if (!validSlug.test(slug)) throw new Error("Invalid document slug");
|
||||
const content = await fs.readFile(path.join(directory, `${slug}.md`), "utf8");
|
||||
const title = content.match(/^# (.+)$/m)?.[1]?.trim() ?? slug;
|
||||
return { slug, title, content };
|
||||
return { slug, title, content, searchText: content };
|
||||
}
|
||||
|
||||
export async function listDocs(): Promise<DocLink[]> {
|
||||
const files = (await fs.readdir(directory)).filter((file) =>
|
||||
file.endsWith(".md") && validSlug.test(file.slice(0, -3)),
|
||||
).sort();
|
||||
return Promise.all(files.map(async (file) => {
|
||||
const { slug, title } = await readDoc(file.slice(0, -3));
|
||||
return { slug, title };
|
||||
}));
|
||||
const files = (await fs.readdir(directory))
|
||||
.filter((file) => file.endsWith(".md") && validSlug.test(file.slice(0, -3)))
|
||||
.sort();
|
||||
return Promise.all(
|
||||
files.map(async (file) => {
|
||||
const { slug, title, searchText } = await readDoc(file.slice(0, -3));
|
||||
return { slug, title, searchText };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
export type Section = {
|
||||
group: string;
|
||||
description: string;
|
||||
label: string;
|
||||
symbol: string;
|
||||
};
|
||||
export function sectionFor(slug: string): Section {
|
||||
if (slug.includes("mounting"))
|
||||
return {
|
||||
group: "架构设计",
|
||||
label: "讨论草案",
|
||||
symbol: "◇",
|
||||
description: "探索节点、对象与统一接入协议,记录仍在演进的设计。",
|
||||
};
|
||||
if (slug.includes("figures"))
|
||||
return {
|
||||
group: "图形档案",
|
||||
label: "工具与工作流",
|
||||
symbol: "⌘",
|
||||
description: "从 Python 源码到可编辑 SVG,让图形与文档一起演进。",
|
||||
};
|
||||
return {
|
||||
group: "开始使用",
|
||||
label: "指南",
|
||||
symbol: "↗",
|
||||
description: "搭建本地工作台,添加第一篇文档,了解检查与发布流程。",
|
||||
};
|
||||
}
|
||||
export type Heading = { id: string; text: string; level: number; line: number };
|
||||
export function headingsFor(content: string): Heading[] {
|
||||
let fenced = false;
|
||||
const counts = new Map<string, number>();
|
||||
return content.split("\n").flatMap((line, index) => {
|
||||
if (/^\s*(```|~~~)/.test(line)) {
|
||||
fenced = !fenced;
|
||||
return [];
|
||||
}
|
||||
const match = !fenced && line.match(/^(#{2,3})\s+(.+?)\s*#*$/);
|
||||
if (!match) return [];
|
||||
const text = match[2].replace(/[`*_]/g, "");
|
||||
const base =
|
||||
text
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}\s-]/gu, "")
|
||||
.trim()
|
||||
.replace(/\s+/g, "-") || "section";
|
||||
const count = counts.get(base) ?? 0;
|
||||
counts.set(base, count + 1);
|
||||
return [
|
||||
{
|
||||
id: count ? `${base}-${count}` : base,
|
||||
text,
|
||||
level: match[1].length,
|
||||
line: index + 1,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -1,43 +1,235 @@
|
||||
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
|
||||
import type {
|
||||
GetStaticPaths,
|
||||
GetStaticProps,
|
||||
InferGetStaticPropsType,
|
||||
} from "next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Children,
|
||||
isValidElement,
|
||||
useEffect,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { listDocs, readDoc, type Doc, type DocLink } from "@/lib/docs";
|
||||
import { Shell, Navigation } from "@/components/Shell";
|
||||
import { CodeBlock } from "@/components/CodeBlock";
|
||||
import { headingsFor, sectionFor } from "@/lib/navigation";
|
||||
|
||||
type Props = { doc: Doc; docs: DocLink[] };
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => ({
|
||||
paths: (await listDocs()).map(({ slug }) => ({ params: { slug } })),
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
|
||||
const slug = params?.slug;
|
||||
if (typeof slug !== "string") return { notFound: true };
|
||||
const docs = await listDocs();
|
||||
if (!docs.some((doc) => doc.slug === slug)) return { notFound: true };
|
||||
if (!docs.some((d) => d.slug === slug)) return { notFound: true };
|
||||
return { props: { doc: await readDoc(slug), docs } };
|
||||
};
|
||||
|
||||
export default function DocPage({ doc, docs }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
function textOf(value: ReactNode): string {
|
||||
return Children.toArray(value)
|
||||
.map((child) =>
|
||||
isValidElement<{ children?: ReactNode }>(child)
|
||||
? textOf(child.props.children)
|
||||
: String(child),
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
export default function DocPage({
|
||||
doc,
|
||||
docs,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const headings = headingsFor(doc.content);
|
||||
const [active, setActive] = useState("");
|
||||
const section = sectionFor(doc.slug);
|
||||
const index = docs.findIndex((d) => d.slug === doc.slug);
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries.filter((e) => e.isIntersecting);
|
||||
if (visible[0]) setActive(visible[0].target.id);
|
||||
},
|
||||
{ rootMargin: "-80px 0px -60% 0px" },
|
||||
);
|
||||
document
|
||||
.querySelectorAll("article h2, article h3")
|
||||
.forEach((el) => observer.observe(el));
|
||||
return () => observer.disconnect();
|
||||
}, [doc.slug]);
|
||||
const toc = (
|
||||
<nav aria-label="页内目录">
|
||||
{headings.map((h) => (
|
||||
<a
|
||||
key={h.id}
|
||||
href={`#${h.id}`}
|
||||
className={`${h.level === 3 ? "toc-nested" : ""} ${active === h.id ? "active" : ""}`}
|
||||
aria-current={active === h.id ? "location" : undefined}
|
||||
>
|
||||
{h.text}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Head><title>{doc.title} · Tabularium</title></Head>
|
||||
<div className="mx-auto max-w-7xl px-6 py-8">
|
||||
<header className="border-b border-stone-200 pb-6"><Link href="/" className="text-sm font-semibold tracking-widest">ENDOR / TABULARIUM</Link></header>
|
||||
<a href="#content" className="sr-only focus:not-sr-only">跳到正文</a>
|
||||
<div className="grid gap-10 py-10 md:grid-cols-[220px_minmax(0,1fr)] md:gap-16">
|
||||
<nav aria-label="文档导航" className="flex flex-wrap gap-2 self-start md:sticky md:top-8 md:flex-col">
|
||||
{docs.map((item) => <Link key={item.slug} href={`/docs/${item.slug}`} aria-current={item.slug === doc.slug ? "page" : undefined} className={`rounded-lg px-3 py-2 text-sm ${item.slug === doc.slug ? "bg-teal-50 font-medium text-teal-900" : "text-stone-600 hover:bg-stone-100"}`}>{item.title}</Link>)}
|
||||
</nav>
|
||||
<main id="content" className="min-w-0 max-w-3xl">
|
||||
<article className="prose prose-stone max-w-none prose-headings:tracking-tight prose-a:text-teal-800 prose-pre:bg-stone-900 prose-img:rounded-xl">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{doc.content}</ReactMarkdown>
|
||||
<Head>
|
||||
<title>{doc.title} · Tabularium</title>
|
||||
<meta name="description" content={section.description} />
|
||||
</Head>
|
||||
<Shell key={doc.slug} docs={docs} current={doc.slug}>
|
||||
<div className="reading-layout">
|
||||
<aside className="sidebar">
|
||||
<Navigation docs={docs} current={doc.slug} />
|
||||
</aside>
|
||||
<main id="content" className="reading-main">
|
||||
<div className="breadcrumb">
|
||||
<Link href="/">文档</Link>
|
||||
<span>/</span>
|
||||
<span>{section.group}</span>
|
||||
</div>
|
||||
<div className="article-meta">
|
||||
<span className="eyebrow">{section.group}</span>
|
||||
{doc.slug.includes("draft") && (
|
||||
<span className="draft-badge">讨论草案</span>
|
||||
)}
|
||||
</div>
|
||||
<article className="prose prose-neutral max-w-none">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
table: ({ children }) => (
|
||||
<div className="table-scroll">
|
||||
<table>{children}</table>
|
||||
</div>
|
||||
),
|
||||
h1: ({ children }) => (
|
||||
<>
|
||||
<h1>{children}</h1>
|
||||
{headings.length > 0 && (
|
||||
<details className="mobile-toc">
|
||||
<summary>本页内容</summary>
|
||||
{toc}
|
||||
</details>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
h2: ({ children, node }) => {
|
||||
const h = headings.find(
|
||||
(h) => h.line === node?.position?.start.line,
|
||||
);
|
||||
return (
|
||||
<h2 id={h?.id}>
|
||||
<a className="heading-anchor" href={`#${h?.id}`}>
|
||||
{children}
|
||||
<span aria-hidden="true">#</span>
|
||||
</a>
|
||||
</h2>
|
||||
);
|
||||
},
|
||||
h3: ({ children, node }) => {
|
||||
const h = headings.find(
|
||||
(h) => h.line === node?.position?.start.line,
|
||||
);
|
||||
return (
|
||||
<h3 id={h?.id}>
|
||||
<a className="heading-anchor" href={`#${h?.id}`}>
|
||||
{children}
|
||||
<span aria-hidden="true">#</span>
|
||||
</a>
|
||||
</h3>
|
||||
);
|
||||
},
|
||||
pre: ({ children }) => {
|
||||
const child = Children.toArray(children)[0];
|
||||
if (
|
||||
isValidElement<{
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}>(child)
|
||||
)
|
||||
return (
|
||||
<CodeBlock
|
||||
code={textOf(child.props.children)}
|
||||
language={child.props.className?.replace(
|
||||
"language-",
|
||||
"",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
return <pre>{children}</pre>;
|
||||
},
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote
|
||||
className={
|
||||
doc.slug.includes("draft")
|
||||
? "callout callout-draft"
|
||||
: "callout"
|
||||
}
|
||||
>
|
||||
<span className="callout-icon" aria-hidden="true">
|
||||
ⓘ
|
||||
</span>
|
||||
<div>{children}</div>
|
||||
</blockquote>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{doc.content}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
<div className="article-end">
|
||||
<span>文档与源码共同维护</span>
|
||||
<a
|
||||
href={`https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs/src/branch/main/content/docs/${doc.slug}.md`}
|
||||
>
|
||||
查看文档源码 ↗
|
||||
</a>
|
||||
</div>
|
||||
<nav className="pagination" aria-label="相邻文档">
|
||||
{index > 0 ? (
|
||||
<Link href={`/docs/${docs[index - 1].slug}`}>
|
||||
<small>← 上一篇</small>
|
||||
<strong>{docs[index - 1].title}</strong>
|
||||
</Link>
|
||||
) : (
|
||||
<Link href="/">
|
||||
<small>← 返回</small>
|
||||
<strong>文档概览</strong>
|
||||
</Link>
|
||||
)}
|
||||
{index < docs.length - 1 && (
|
||||
<Link
|
||||
className="next-page"
|
||||
href={`/docs/${docs[index + 1].slug}`}
|
||||
>
|
||||
<small>下一篇 →</small>
|
||||
<strong>{docs[index + 1].title}</strong>
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
<footer className="article-footer">
|
||||
ENDOR / TABULARIUM <span>记录思考,连接系统。</span>
|
||||
</footer>
|
||||
</main>
|
||||
<aside className="toc">
|
||||
<p className="eyebrow">本页内容</p>
|
||||
{toc}
|
||||
<div className="toc-note">
|
||||
<span className="status-dot" /> {section.label}
|
||||
<p>
|
||||
{doc.slug.includes("draft")
|
||||
? "这里记录正在讨论的设计,尚不代表现有实现。"
|
||||
: "每一次理解,都成为下一步的起点。"}
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</Shell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+157
-22
@@ -2,32 +2,167 @@ import type { GetStaticProps, InferGetStaticPropsType } from "next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { listDocs, type DocLink } from "@/lib/docs";
|
||||
import { Shell, Navigation } from "@/components/Shell";
|
||||
import { CodeBlock } from "@/components/CodeBlock";
|
||||
import { sectionFor } from "@/lib/navigation";
|
||||
|
||||
export const getStaticProps: GetStaticProps<{ docs: DocLink[] }> = async () => ({
|
||||
props: { docs: await listDocs() },
|
||||
});
|
||||
|
||||
export default function Home({ docs }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
docs: DocLink[];
|
||||
}> = async () => ({ props: { docs: await listDocs() } });
|
||||
export default function Home({
|
||||
docs,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
return (
|
||||
<>
|
||||
<Head><title>Tabularium · Endor 文档</title><meta name="description" content="Endor 的文档、架构设计与制图档案。" /></Head>
|
||||
<main className="mx-auto max-w-5xl px-6 py-16 md:py-24">
|
||||
<div className="flex items-center justify-between border-b border-stone-200 pb-6 text-sm">
|
||||
<span className="font-semibold tracking-widest">ENDOR / TABULARIUM</span>
|
||||
<span className="rounded-full border border-stone-200 px-3 py-1 text-stone-500">文档工作台</span>
|
||||
<Head>
|
||||
<title>Tabularium · Endor 文档</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Endor 的知识档案。探索开发指南、架构设计与可复现的图形工作流。"
|
||||
/>
|
||||
</Head>
|
||||
<Shell docs={docs}>
|
||||
<div className="home-layout">
|
||||
<aside className="sidebar">
|
||||
<Navigation docs={docs} />
|
||||
</aside>
|
||||
<main id="content" className="home-main">
|
||||
<section className="home-hero">
|
||||
<div className="hero-copy">
|
||||
<p className="eyebrow">
|
||||
<span className="status-dot" /> ENDOR KNOWLEDGE ARCHIVE
|
||||
</p>
|
||||
<h1>
|
||||
理解系统。
|
||||
<br />
|
||||
构建下一步。
|
||||
</h1>
|
||||
<p className="hero-description">
|
||||
欢迎来到 <strong>Tabularium</strong>,Endor 的知识档案。
|
||||
<br className="desktop-break" />
|
||||
从第一篇文档到系统设计,让想法有迹可循。
|
||||
</p>
|
||||
<div className="hero-actions">
|
||||
<Link
|
||||
className="primary-button"
|
||||
href="/docs/01-getting-started"
|
||||
>
|
||||
开始阅读 <span>→</span>
|
||||
</Link>
|
||||
<a className="text-link" href="#explore">
|
||||
探索档案 <span>↓</span>
|
||||
</a>
|
||||
</div>
|
||||
<p className="hero-footnote">
|
||||
文档 · 架构 · 图形 <span>持续生长的工作台</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="archive-visual" aria-hidden="true">
|
||||
<div className="archive-grid" />
|
||||
<div className="archive-caption">TABULARIUM / 001</div>
|
||||
<div className="archive-sheet sheet-back">
|
||||
<span>03 — FIGURES</span>
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</div>
|
||||
<div className="archive-sheet sheet-middle">
|
||||
<span>02 — ARCHITECTURE</span>
|
||||
<i />
|
||||
<i />
|
||||
<i />
|
||||
</div>
|
||||
<div className="archive-sheet sheet-front">
|
||||
<span className="sheet-label">01 / ENDOR</span>
|
||||
<div className="sheet-emblem">
|
||||
E<span>↗</span>
|
||||
</div>
|
||||
<div className="sheet-rule" />
|
||||
<b>Ideas, documented.</b>
|
||||
<small>知识在这里连接。</small>
|
||||
</div>
|
||||
<div className="archive-coordinate">
|
||||
文字与图形,一起演进 <span>↗</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="explore-section" id="explore">
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<p className="eyebrow">EXPLORE THE ARCHIVE</p>
|
||||
<h2>从这里开始探索</h2>
|
||||
</div>
|
||||
<span className="section-count">
|
||||
{String(docs.length).padStart(2, "0")} 篇文档
|
||||
</span>
|
||||
</div>
|
||||
<div className="doc-cards">
|
||||
{docs.map((doc, index) => {
|
||||
const section = sectionFor(doc.slug);
|
||||
return (
|
||||
<Link
|
||||
className="doc-card"
|
||||
key={doc.slug}
|
||||
href={`/docs/${doc.slug}`}
|
||||
>
|
||||
<div className="card-top">
|
||||
<span className={`card-icon icon-${index}`}>
|
||||
{section.symbol}
|
||||
</span>
|
||||
<span className="card-number">0{index + 1}</span>
|
||||
</div>
|
||||
<p className="card-group">{section.group}</p>
|
||||
<h3>
|
||||
{doc.title.replace("(草案)", "")}
|
||||
<span aria-hidden="true">↗</span>
|
||||
</h3>
|
||||
<p className="card-description">{section.description}</p>
|
||||
<div className="card-bottom">
|
||||
<span>{section.label}</span>
|
||||
{doc.slug.includes("draft") && (
|
||||
<span className="draft-badge">草案</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
<section className="quickstart">
|
||||
<div>
|
||||
<p className="eyebrow">LOCAL WORKSPACE</p>
|
||||
<h2>一个命令,继续探索。</h2>
|
||||
<p>
|
||||
在本地启动文档工作台,
|
||||
<br />
|
||||
让文字和可复现的图形留在同一个仓库。
|
||||
</p>
|
||||
<Link
|
||||
className="text-link"
|
||||
href="/docs/01-getting-started#启动"
|
||||
>
|
||||
查看完整启动指南 <span>→</span>
|
||||
</Link>
|
||||
</div>
|
||||
<CodeBlock
|
||||
language="sh"
|
||||
code={
|
||||
"pnpm install --frozen-lockfile\nuv sync --project tools/figures --locked\npnpm dev\n"
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
<footer className="site-footer">
|
||||
<span>
|
||||
Endor / <strong>Tabularium</strong>
|
||||
</span>
|
||||
<span>记录思考,连接系统。</span>
|
||||
<a href="https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs">
|
||||
在仓库中探索 ↗
|
||||
</a>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
<p className="mt-16 text-sm tracking-widest text-teal-800">文档 · 设计 · 图形</p>
|
||||
<h1 className="mt-4 text-5xl font-semibold tracking-tight md:text-7xl">Endor 的知识档案。</h1>
|
||||
<p className="mt-6 max-w-2xl text-lg leading-8 text-stone-600">记录系统概念、架构讨论和开发约定,让文字与图形一起演进。</p>
|
||||
<div className="mt-14 grid gap-4 md:grid-cols-2">
|
||||
{docs.map((doc, index) => (
|
||||
<Link key={doc.slug} href={`/docs/${doc.slug}`} className="group rounded-xl border border-stone-200 bg-white p-6 transition hover:border-teal-700 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-teal-700">
|
||||
<span className="font-mono text-xs text-stone-400">{String(index + 1).padStart(2, "0")}</span>
|
||||
<h2 className="mt-4 text-xl font-medium group-hover:text-teal-800">{doc.title} <span aria-hidden="true">↗</span></h2>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</Shell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user