Expand document collections with Pretext tutorial and simplify library navigation
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m47s

This commit is contained in:
2026-10-28 13:42:51 +08:00
parent 1f3ab5a7ee
commit 2b3b3a1ae4
28 changed files with 1174 additions and 761 deletions
+16
View File
@@ -0,0 +1,16 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { listDocs, readDoc } from "@/lib/docs";
// The proxy authenticates this endpoint, including index content.
export default async function handler(request: NextApiRequest, response: NextApiResponse) {
response.setHeader("Cache-Control", "private, no-store");
if (request.method !== "GET") {
response.setHeader("Allow", "GET");
return response.status(405).end();
}
const docs = await listDocs();
const index = await Promise.all(docs.map(async (doc) => ({
...doc, searchText: (await readDoc(doc.slug)).content,
})));
response.status(200).json(index);
}
@@ -18,15 +18,15 @@ import { listDocs, readDoc, type Doc, type DocLink } from "@/lib/docs";
import { Shell, Navigation } from "@/components/Shell";
import { ArchitectureFigure } from "@/components/ArchitectureFigure";
import { CodeBlock } from "@/components/CodeBlock";
import { headingsFor, sectionFor } from "@/lib/navigation";
import { headingsFor } from "@/lib/navigation";
type Props = { doc: Doc; docs: DocLink[] };
export const getStaticPaths: GetStaticPaths = async () => ({
paths: (await listDocs()).map(({ slug }) => ({ params: { slug } })),
paths: (await listDocs()).map(({ slug }) => ({ params: { slug: slug.split("/") } })),
fallback: false,
});
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
const slug = params?.slug;
const slug = Array.isArray(params?.slug) ? params.slug.join("/") : params?.slug;
if (typeof slug !== "string") return { notFound: true };
const docs = await listDocs();
if (!docs.some((d) => d.slug === slug)) return { notFound: true };
@@ -47,8 +47,9 @@ export default function DocPage({
}: 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);
const siblings = docs.filter((d) => d.collection === doc.collection);
const index = siblings.findIndex((d) => d.slug === doc.slug);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
@@ -80,7 +81,7 @@ export default function DocPage({
<>
<Head>
<title>{doc.title} · Tabularium</title>
<meta name="description" content={section.description} />
<meta name="description" content={doc.description} />
</Head>
<Shell key={doc.slug} docs={docs} current={doc.slug}>
<div className="reading-layout">
@@ -91,13 +92,8 @@ export default function DocPage({
<div className="breadcrumb">
<Link href="/">文档</Link>
<span>/</span>
<span>{section.group}</span>
</div>
<div className="article-meta">
<span className="eyebrow">{section.group}</span>
{section.label.includes("草案") && (
<span className="draft-badge">讨论草案</span>
)}
<span>{doc.group}</span>
{doc.draft && <span className="draft-badge">草案</span>}
</div>
<article className="prose prose-neutral max-w-none">
<ReactMarkdown
@@ -183,7 +179,7 @@ export default function DocPage({
blockquote: ({ children }) => (
<blockquote
className={
section.label.includes("草案")
doc.draft
? "callout callout-draft"
: "callout"
}
@@ -200,7 +196,7 @@ export default function DocPage({
</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`}
>
@@ -209,9 +205,9 @@ export default function DocPage({
</div>
<nav className="pagination" aria-label="相邻文档">
{index > 0 ? (
<Link href={`/docs/${docs[index - 1].slug}`}>
<Link href={`/docs/${siblings[index - 1].slug}`}>
<small>← 上一篇</small>
<strong>{docs[index - 1].title}</strong>
<strong>{siblings[index - 1].title}</strong>
</Link>
) : (
<Link href="/">
@@ -219,31 +215,20 @@ export default function DocPage({
<strong>文档概览</strong>
</Link>
)}
{index < docs.length - 1 && (
{index < siblings.length - 1 && (
<Link
className="next-page"
href={`/docs/${docs[index + 1].slug}`}
href={`/docs/${siblings[index + 1].slug}`}
>
<small>下一篇 →</small>
<strong>{docs[index + 1].title}</strong>
<strong>{siblings[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>
{section.label.includes("草案")
? "这里记录正在讨论的设计,尚不代表现有实现。"
: "每一次理解,都成为下一步的起点。"}
</p>
</div>
</aside>
</div>
</Shell>
+51 -160
View File
@@ -1,168 +1,59 @@
import type { GetStaticProps, InferGetStaticPropsType } from "next";
import Head from "next/head";
import Link from "next/link";
import { useState } from "react";
import { listDocs, type DocLink } from "@/lib/docs";
import { Shell, Navigation } from "@/components/Shell";
import { CodeBlock } from "@/components/CodeBlock";
import { sectionFor } from "@/lib/navigation";
import { Icon } from "@/components/Icon";
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>
<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>
export const getStaticProps: GetStaticProps<{ docs: DocLink[] }> = async () => ({
props: { docs: await listDocs() },
});
const PAGE_SIZE = 8;
export default function Home({ docs }: InferGetStaticPropsType<typeof getStaticProps>) {
const [collection, setCollection] = useState("");
const [query, setQuery] = useState("");
const [page, setPage] = useState(0);
const collections = [...new Set(docs.map((doc) => doc.collection))];
const filtered = docs.filter((doc) => (!collection || doc.collection === collection) &&
`${doc.title} ${doc.description}`.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()));
const featured = docs.filter((doc) => doc.featured).slice(0, 3);
return <>
<Head><title>Tabularium · 资料库</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="library-intro">
<h1>资料库</h1>
<p>系统设计、源码研究与学习笔记。</p>
</section>
<section className="featured-section" aria-labelledby="featured-title">
<h2 id="featured-title">推荐阅读</h2>
<div className="featured-grid">{featured.map((doc) => <Link className="featured-link" href={`/docs/${doc.slug}`} key={doc.slug}>
<Icon name={doc.collection === "Endor 文档" ? "book" : "code"} size={23} />
<h3>{doc.title.replace("(草案)", "")}</h3>
<p>{doc.description}</p>
<Icon name="arrow" />
</Link>)}</div>
</section>
<section className="library-section" id="library" aria-labelledby="library-title">
<h2 id="library-title">全部资料</h2>
<div className="library-tools">
<div className="collection-filters" role="group" aria-label="资料分类">
{["", ...collections].map((name) => <button key={name} aria-pressed={collection === name} onClick={() => { setCollection(name); setPage(0); }}>{name || "全部"}</button>)}
</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>
{section.label.includes("草案") && (
<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>
</Shell>
</>
);
<label className="library-search"><Icon name="search" /><input aria-label="筛选资料" placeholder="筛选资料…" value={query} onChange={(event) => {setQuery(event.target.value); setPage(0);}} /></label>
</div>
<ul className="library-list">{filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE).map((doc) => <li key={doc.slug}>
<Link href={`/docs/${doc.slug}`}><span className="library-row-title">{doc.title.replace("(草案)", "")}{doc.draft && <span className="draft-badge">草案</span>}</span><span className="library-row-group">{doc.group}</span><Icon name="arrow" size={16} /></Link>
</li>)}</ul>
{filtered.length === 0 && <p className="library-empty">没有找到相关资料。</p>}
{filtered.length > PAGE_SIZE && <nav className="library-pagination" aria-label="资料分页"><button disabled={page === 0} onClick={() => setPage(page - 1)}>上一页</button><span>{page + 1} / {Math.ceil(filtered.length / PAGE_SIZE)}</span><button disabled={(page + 1) * PAGE_SIZE >= filtered.length} onClick={() => setPage(page + 1)}>下一页</button></nav>}
</section>
<footer className="site-footer"><span>Tabularium</span><a href="https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs">文档仓库 ↗</a></footer>
</main>
</div>
</Shell>
</>;
}