Initialize local Tabularium documentation monorepo

This commit is contained in:
2026-10-26 19:20:07 +08:00
commit 5ceb4e57aa
27 changed files with 5582 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
export default defineConfig([
...nextVitals,
...nextTs,
globalIgnores([".next/**", "out/**", "next-env.d.ts"]),
]);
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
+8
View File
@@ -0,0 +1,8 @@
import type { NextConfig } from "next";
const config: NextConfig = {
reactStrictMode: true,
poweredByHeader: false,
};
export default config;
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@endor/docs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "NEXT_TELEMETRY_DISABLED=1 next dev --hostname 127.0.0.1",
"build": "NEXT_TELEMETRY_DISABLED=1 next build",
"start": "NEXT_TELEMETRY_DISABLED=1 next start --hostname 127.0.0.1",
"lint": "eslint . --max-warnings=0",
"typecheck": "NEXT_TELEMETRY_DISABLED=1 next typegen && tsc --noEmit"
},
"dependencies": {
"next": "16.4.3",
"react": "19.3.1",
"react-dom": "19.3.1",
"react-markdown": "10.1.0",
"remark-gfm": "4.0.1"
},
"devDependencies": {
"@tailwindcss/postcss": "4.3.3",
"@tailwindcss/typography": "0.5.20",
"@types/node": "22.20.5",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"eslint": "^9.39.5",
"eslint-config-next": "16.4.3",
"postcss": "8.5.31",
"tailwindcss": "4.3.3",
"typescript": "~5.9.3"
}
}
+3
View File
@@ -0,0 +1,3 @@
const config = { plugins: { "@tailwindcss/postcss": {} } };
export default config;
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="960" height="240" viewBox="0 0 960 240">
<defs>
</defs>
<title>Local figure workflow</title>
<rect x="0" y="0" width="960" height="240" fill="#f5f5f0" rx="16" />
<text x="32" y="42" font-size="16" fill="#57534e" font-family="sans-serif">TABULARIUM / FIGURE WORKFLOW</text>
<rect x="32" y="80" width="272" height="120" rx="12" fill="#ffffff" stroke="#a8a29e" />
<text x="50" y="126" font-size="20" fill="#134e4a" font-family="sans-serif">Python source</text>
<text x="50" y="164" font-size="14" fill="#57534e" font-family="monospace">tools/figures</text>
<path d="M312,140 L334,140" stroke="#0f766e" stroke-width="2" />
<path d="M329,135 L334,140 L329,145" fill="none" stroke="#0f766e" stroke-width="2" />
<rect x="344" y="80" width="272" height="120" rx="12" fill="#ffffff" stroke="#a8a29e" />
<text x="362" y="126" font-size="20" fill="#134e4a" font-family="sans-serif">SVG asset</text>
<text x="362" y="164" font-size="14" fill="#57534e" font-family="monospace">apps/docs/public/figures</text>
<path d="M624,140 L646,140" stroke="#0f766e" stroke-width="2" />
<path d="M641,135 L646,140 L641,145" fill="none" stroke="#0f766e" stroke-width="2" />
<rect x="656" y="80" width="272" height="120" rx="12" fill="#ffffff" stroke="#a8a29e" />
<text x="674" y="126" font-size="20" fill="#134e4a" font-family="sans-serif">Local documentation</text>
<text x="674" y="164" font-size="14" fill="#57534e" font-family="monospace">content/docs</text>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+25
View File
@@ -0,0 +1,25 @@
import fs from "node:fs/promises";
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 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 };
}
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 };
}));
}
+6
View File
@@ -0,0 +1,6 @@
import type { AppProps } from "next/app";
import "@/styles/globals.css";
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
+10
View File
@@ -0,0 +1,10 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="zh-CN">
<Head />
<body><Main /><NextScript /></body>
</Html>
);
}
+43
View File
@@ -0,0 +1,43 @@
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
import Head from "next/head";
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { listDocs, readDoc, type Doc, type DocLink } from "@/lib/docs";
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 };
return { props: { doc: await readDoc(slug), docs } };
};
export default function DocPage({ doc, docs }: InferGetStaticPropsType<typeof getStaticProps>) {
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>
</article>
</main>
</div>
</div>
</>
);
}
+33
View File
@@ -0,0 +1,33 @@
import type { GetStaticProps, InferGetStaticPropsType } from "next";
import Head from "next/head";
import Link from "next/link";
import { listDocs, type DocLink } from "@/lib/docs";
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>
</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>
</>
);
}
+10
View File
@@ -0,0 +1,10 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@theme {
--font-sans: ui-sans-serif, system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
}
body { background: #fafaf8; color: #292524; }
::selection { background: #ccfbf1; }
.prose table { display: block; overflow-x: auto; }
+35
View File
@@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}