Refine documentation UI with distinct collection navigation and borderless surfaces
Verify and deploy Tabularium / verify-and-deploy (pull_request) Has been cancelled

This commit is contained in:
2026-10-28 15:31:25 +08:00
parent e381deb04e
commit 627c0ee887
9 changed files with 288 additions and 96 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ tools/figures/ 独立 Python 项目,使用 drawsvg 编程制图
- `pnpm start`:在本地预览已构建站点。
- `pnpm check`:依次执行 lint、类型检查、制图与构建。
站点默认浅色,使用本地系统字体。全站搜索支持正文关键词、`⌘/Ctrl K`、方向键与 Enter;代码块提供复制与反馈。桌面提供分组导航和滚动高亮的页内目录,小屏可展开文档菜单与页内目录。首页入口、草案提示、前后篇导航均引用现有 Markdown 内容和资料目录配置。
站点默认浅色,使用本地系统字体。全站搜索支持正文关键词、`⌘/Ctrl K`、方向键与 Enter;代码块提供复制与反馈。桌面提供分组导航和滚动高亮的页内目录,小屏可展开文档菜单与页内目录。首页入口、草案提示、前后篇导航均引用现有 Markdown 内容和资料目录配置。顶部提供文档切换,阅读页左侧只展示当前文档,并有独立图标、名称与返回入口;搜索框标明当前文档范围。界面主要通过留白、轻渐变与状态反馈分层,支持减少动态效果的系统偏好。
参考设计截图与研究记录保存在 `design/references`;浏览器验收截图与检查说明保存在 `design/verification`。这些文件不作为站点静态资源发布。
+4 -1
View File
@@ -2,10 +2,13 @@ import type { CSSProperties } from "react";
// One stroke style for controls and navigation; labels carry the meaning.
export function Icon({ name, size = 18, style }: {
name: "search" | "arrow" | "book" | "code" | "menu" | "close";
name: "search" | "arrow" | "book" | "code" | "menu" | "close" | "layers" | "nodes" | "chevron";
size?: number; style?: CSSProperties;
}) {
const paths = {
layers: <><path d="m12 3 9 5-9 5-9-5 9-5Zm-9 9 9 5 9-5M3 16l9 5 9-5" /></>,
nodes: <><circle cx="5" cy="6" r="2" /><circle cx="19" cy="6" r="2" /><circle cx="12" cy="19" r="2" /><path d="M7 6h10M6 8l5 9m7-9-5 9" /></>,
chevron: <path d="m9 5 7 7-7 7" />,
search: <><circle cx="10.5" cy="10.5" r="6.5" /><path d="m16 16 4 4" /></>,
arrow: <path d="M5 12h14m-6-6 6 6-6 6" />,
book: <><path d="M12 5c-3-2-6-2-9-1v15c3-1 6-1 9 1 3-2 6-2 9-1V4c-3-1-6-1-9 1Z" /><path d="M12 5v15" /></>,
+31 -48
View File
@@ -2,6 +2,7 @@ import Link from "next/link";
import { Icon } from "@/components/Icon";
import { useEffect, useRef, useState, type ReactNode } from "react";
import type { DocLink } from "@/lib/docs";
import { collectionEntries, collectionIcon, collectionLabel, collectionTone } from "@/lib/collections";
export function Brand() {
@@ -13,10 +14,9 @@ export function Brand() {
<i />
</span>
<span>
Endor <span className="brand-divider">/</span>{" "}
<strong>Tabularium</strong>
</span>
<span className="docs-badge">文档</span>
<span className="docs-badge">Docs</span>
</Link>
);
}
@@ -32,51 +32,36 @@ export function Navigation({
const collection = docs.find((d) => d.slug === current)?.collection;
const scopedDocs = docs.filter((d) => d.collection === collection);
const groups = [...new Set(scopedDocs.map((d) => d.group))];
const collections = [...new Set(docs.map((d) => d.collection))];
const collections = collectionEntries(docs);
return (
<nav aria-label="文档导航">
<Link
onClick={onNavigate}
className={`nav-home ${!current ? "selected" : ""}`}
href="/"
>
<Icon name="book" size={17} /> <span>资料库</span>
</Link>
{!current && collections.map((name) => (
<Link
onClick={onNavigate}
className="nav-item"
href={`/docs/${docs.find((d) => d.collection === name)!.slug}`}
key={name}
>
{name}
{collection ? <>
<Link className="nav-back" href="/" onClick={onNavigate}>
<Icon name="arrow" size={15} style={{ transform: "rotate(180deg)" }} />全部文档
</Link>
))}
{collection && !groups.includes(collection) && <p className="eyebrow">{collection}</p>}
<Link className="collection-identity" href={`/docs/${collections.find((c) => c.name === collection)!.entry.slug}`} onClick={onNavigate}>
<Icon name={collectionIcon(collection)} size={22} /><span>{collectionLabel(collection)}</span>
</Link>
</> : <div className="collection-identity"><Icon name="book" size={22} /><span>文档中心</span></div>}
{!current && collections.map(({ name, entry }) => <Link
onClick={onNavigate} className="collection-nav-link" href={`/docs/${entry.slug}`} key={name}
><Icon name={collectionIcon(name)} size={18} /><span>{collectionLabel(name)}</span><Icon name="chevron" size={13} /></Link>)}
{groups.map((group) => (
<div className="nav-group" key={group}>
<p>{group}</p>
{scopedDocs
.filter((d) => d.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.draft && (
<span className="tiny-dot" aria-label="草案" />
)}
</Link>
))}
<p>{group === collection ? "章节目录" : group}</p>
{scopedDocs.filter((d) => d.group === group).map((d) => (
<Link onClick={onNavigate} aria-current={d.slug === current ? "page" : undefined}
className="nav-item" href={`/docs/${d.slug}`} key={d.slug}>
<span>{d.slug.endsWith("/00-overview") ? "概览" : d.title.replace("(草案)", "")}</span>
{d.draft && <span className="tiny-dot" aria-label="草案" />}
</Link>
))}
</div>
))}
</nav>
);
}
export function Shell({
docs,
current,
@@ -96,6 +81,7 @@ export function Shell({
const [selected, setSelected] = useState(0);
const [menu, setMenu] = useState(false);
const collection = docs.find((d) => d.slug === current)?.collection;
const collections = collectionEntries(docs);
const results = searchDocs.filter((d) =>
(!collection || d.collection === collection) &&
`${d.title} ${d.description} ${d.searchText ?? ""}`
@@ -134,18 +120,15 @@ export function Shell({
return () => document.removeEventListener("keydown", keyboard);
}, []);
return (
<>
<div className="docs-shell" data-tone={collectionTone(collection)}>
<a className="skip-link" href="#content">
跳到正文
</a>
<header className="site-header">
<Brand />
<nav className="top-nav" aria-label="主导航">
<Link href="/">资料库</Link>
<Link href="/docs/00-endor-concepts-draft">架构设计</Link>
<a href="https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs">
仓库 ↗
</a>
<Link href="/" aria-current={!current ? "page" : undefined}>文档中心</Link>
{collections.map(({ name, entry }) => <Link key={name} href={`/docs/${entry.slug}`} aria-current={collection === name ? "page" : undefined}>{collectionLabel(name)}</Link>)}
</nav>
<button
ref={trigger}
@@ -154,7 +137,7 @@ export function Shell({
onClick={openSearch}
>
<Icon name="search" />
<span>搜索文档…</span>
<span>{collection ? `搜索${collectionLabel(collection)}…` : "搜索全部文档…"}</span>
<kbd>⌘ K</kbd>
</button>
<button
@@ -180,7 +163,7 @@ export function Shell({
aria-expanded={menu}
aria-controls="mobile-navigation"
>
<Icon name="menu" size={16} /> <span>文档目录</span>
<Icon name={menu ? "close" : "menu"} size={16} /> <span>{collection ? collectionLabel(collection) : "全部文档"}</span>
<span>{menu ? "−" : "+"}</span>
</button>
</div>
@@ -208,7 +191,7 @@ export function Shell({
<input
ref={input}
value={query}
placeholder="搜索文档、命令或概念…"
placeholder={collection ? `搜索${collectionLabel(collection)}的内容…` : "搜索全部文档、命令或概念…"}
aria-label="搜索文档内容"
aria-controls="search-results"
onChange={(e) => {
@@ -264,7 +247,7 @@ export function Shell({
))}
{!results.length && (
<p className="empty-search">
没有找到相关文档。试试「节点」「SVG」或「pnpm」。
没有找到相关内容,请尝试其他关键词。
</p>
)}
</div>
@@ -272,6 +255,6 @@ export function Shell({
↑ ↓ 选择 <span>Enter 打开 · Esc 关闭</span>
</div>
</dialog>
</>
</div>
);
}
+24
View File
@@ -0,0 +1,24 @@
import type { DocLink } from "@/lib/docs";
export function collectionLabel(name: string) {
return name.replace(/ 文档$| 源码教程$|问题集$/g, "");
}
export function collectionTone(name?: string) {
if (name?.includes("Pretext")) return "blue";
if (name?.includes("语言设计")) return "violet";
if (name?.includes("垃圾回收")) return "amber";
return "green";
}
export function collectionIcon(name?: string): "book" | "code" | "layers" | "nodes" {
if (name?.includes("Pretext")) return "code";
if (name?.includes("语言设计")) return "layers";
if (name?.includes("垃圾回收")) return "nodes";
return "book";
}
export function collectionEntries(docs: DocLink[]) {
return [...new Set(docs.map((doc) => doc.collection))].map((name) => {
const items = docs.filter((doc) => doc.collection === name);
const entry = items.find((doc) => doc.slug.endsWith("/00-overview")) ?? items[0];
return { name, entry, count: items.length };
});
}
+6 -3
View File
@@ -18,6 +18,8 @@ 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 { collectionEntries, collectionLabel } from "@/lib/collections";
import { Icon } from "@/components/Icon";
import { headingsFor } from "@/lib/navigation";
type Props = { doc: Doc; docs: DocLink[] };
@@ -49,6 +51,7 @@ export default function DocPage({
const [active, setActive] = useState("");
const siblings = docs.filter((d) => d.collection === doc.collection);
const collectionEntry = collectionEntries(docs).find((c) => c.name === doc.collection)!.entry;
const index = siblings.findIndex((d) => d.slug === doc.slug);
useEffect(() => {
const observer = new IntersectionObserver(
@@ -90,9 +93,9 @@ export default function DocPage({
</aside>
<main id="content" className="reading-main">
<div className="breadcrumb">
<Link href="/">文档</Link>
<span>/</span>
<span>{doc.group}</span>
<Link href={`/docs/${collectionEntry.slug}`}>{collectionLabel(doc.collection)}</Link>
<Icon name="chevron" size={12} />
<span>{doc.slug === collectionEntry.slug ? "概览" : doc.group === doc.collection ? doc.title : doc.group}</span>
{doc.draft && <span className="draft-badge">草案</span>}
</div>
<article className="prose prose-neutral max-w-none">
+9 -8
View File
@@ -5,6 +5,7 @@ import { useState } from "react";
import { listDocs, type DocLink } from "@/lib/docs";
import { Shell, Navigation } from "@/components/Shell";
import { Icon } from "@/components/Icon";
import { collectionEntries, collectionIcon, collectionLabel, collectionTone } from "@/lib/collections";
export const getStaticProps: GetStaticProps<{ docs: DocLink[] }> = async () => ({
props: { docs: await listDocs() },
@@ -17,7 +18,7 @@ export default function Home({ docs }: InferGetStaticPropsType<typeof getStaticP
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);
const entries = collectionEntries(docs);
return <>
<Head><title>Tabularium · 资料库</title><meta name="description" content="Endor 文档、源码研究与学习资料。" /></Head>
<Shell docs={docs}>
@@ -25,16 +26,16 @@ export default function Home({ docs }: InferGetStaticPropsType<typeof getStaticP
<aside className="sidebar"><Navigation docs={docs} /></aside>
<main id="content" className="home-main">
<section className="library-intro">
<h1>资料库</h1>
<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" />
<h2 id="featured-title">选择一份文档</h2>
<div className="featured-grid">{entries.map(({ name, entry, count }) => <Link className="featured-link" data-tone={collectionTone(name)} href={`/docs/${entry.slug}`} key={name}>
<span className="collection-art"><Icon name={collectionIcon(name)} size={26} /></span>
<h3>{collectionLabel(name)}</h3>
<p>{entry.description}</p>
<span className="collection-entry">{count} 篇 <Icon name="arrow" size={17} /></span>
</Link>)}</div>
</section>
<section className="library-section" id="library" aria-labelledby="library-title">
+192 -35
View File
@@ -55,7 +55,7 @@ button {
}
.site-header {
height: 68px;
border-bottom: 1px solid var(--line);
border: 0;
display: flex;
align-items: center;
padding: 0 32px;
@@ -82,9 +82,9 @@ button {
color: #c8ceca;
}
.docs-badge {
font-size: 10px;
font-size: 13px;
padding: 2px 5px;
border: 1px solid var(--line);
border: 0;
border-radius: 4px;
color: var(--muted);
letter-spacing: 0;
@@ -123,7 +123,7 @@ button {
display: flex;
gap: 9px;
align-items: center;
border: 1px solid #e2e7e3;
border: 0;
background: #fafbf9;
border-radius: 7px;
padding: 7px 10px;
@@ -138,7 +138,7 @@ button {
}
.search-trigger kbd {
margin-left: auto;
border: 1px solid var(--line);
border: 0;
border-radius: 3px;
padding: 0 4px;
font-size: 10px;
@@ -163,7 +163,7 @@ button {
grid-template-columns: 238px minmax(0, 1fr);
}
.sidebar {
border-right: 1px solid var(--line);
border: 0;
padding: 32px 22px;
height: calc(100dvh - 68px);
position: sticky;
@@ -200,7 +200,7 @@ button {
padding: 9px 10px;
line-height: 1.6;
color: #637066;
border-left: 1px solid var(--line);
border: 0;
margin-left: 10px;
}
.nav-item:hover {
@@ -225,9 +225,9 @@ button {
padding: 0 48px;
}
.eyebrow { font-size: 13px; font-weight: 500; color: var(--muted); margin: 0; }
.draft-badge { color: #8c733e; background: #faf6e9; border: 1px solid #eee4c9; border-radius: 4px; padding: 2px 6px; font-size: 12px; white-space: nowrap; }
.draft-badge { color: #8c733e; background: #faf6e9; border: 0; border-radius: 4px; padding: 2px 6px; font-size: 12px; white-space: nowrap; }
.site-footer {
border-top: 1px solid var(--line);
border: 0;
display: flex;
justify-content: space-between;
gap: 15px;
@@ -324,7 +324,7 @@ button {
SFMono-Regular,
Consolas,
monospace;
border: 1px solid #e7ece7;
border: 0;
background: #f6f8f5;
border-radius: 4px;
padding: 2px 5px;
@@ -344,7 +344,7 @@ button {
min-width: 360px;
margin: 0;
font-size: 13px;
border: 1px solid var(--line);
border: 0;
border-radius: 7px;
}
.prose thead {
@@ -357,7 +357,7 @@ button {
min-width: 100px;
}
.prose img {
border: 1px solid var(--line);
border: 0;
border-radius: 8px;
padding: 12px;
background: #fafbf8;
@@ -366,7 +366,7 @@ button {
.callout {
display: flex;
gap: 12px;
border: 1px solid #e2e9e0;
border: 0;
padding: 15px 18px;
border-radius: 7px;
background: #f8faf6;
@@ -392,7 +392,7 @@ button {
font-size: 16px;
}
.code-block {
border: 1px solid #e1e7e0;
border: 0;
border-radius: 8px;
overflow: hidden;
background: #f9fbf7;
@@ -404,7 +404,7 @@ button {
justify-content: space-between;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid #e7ece4;
border: 0;
color: #879781;
font:
10px ui-monospace,
@@ -415,7 +415,7 @@ button {
}
.code-toolbar button {
padding: 2px 6px;
border: 1px solid #dce5d6;
border: 0;
border-radius: 4px;
font-size: 11px;
min-height: 28px;
@@ -472,13 +472,13 @@ button {
margin-bottom: 18px;
}
.toc nav {
border-left: 1px solid var(--line);
border: 0;
}
.toc nav a {
display: block;
padding: 7px 0 7px 15px;
margin-left: -1px;
border-left: 1px solid transparent;
border: 0;
color: #69766b;
line-height: 1.6;
}
@@ -494,7 +494,7 @@ button {
display: flex;
gap: 15px;
justify-content: space-between;
border-top: 1px solid var(--line);
border: 0;
padding: 25px 0;
margin-top: 45px;
font-size: 10px;
@@ -510,7 +510,7 @@ button {
margin: 10px 0 45px;
}
.pagination > a {
border: 1px solid var(--line);
border: 0;
padding: 18px;
border-radius: 7px;
}
@@ -533,7 +533,7 @@ button {
}
.search-dialog {
padding: 0;
border: 1px solid var(--line);
border: 0;
border-radius: 12px;
width: min(570px, calc(100vw - 32px));
max-height: 80dvh;
@@ -550,7 +550,7 @@ button {
align-items: center;
gap: 13px;
padding: 17px;
border-bottom: 1px solid var(--line);
border: 0;
}
.search-input-row > span {
font-size: 25px;
@@ -564,7 +564,7 @@ button {
}
.search-input-row button {
font-size: 10px;
border: 1px solid var(--line);
border: 0;
border-radius: 4px;
padding: 3px 5px;
}
@@ -578,7 +578,7 @@ button {
}
.search-result {
display: block;
border: 1px solid transparent;
border: 0;
padding: 13px;
border-radius: 7px;
}
@@ -612,7 +612,7 @@ button {
}
.search-footer {
padding: 12px 20px;
border-top: 1px solid var(--line);
border: 0;
font-size: 10px;
color: #9da797;
display: flex;
@@ -656,7 +656,7 @@ button {
.mobile-toc {
display: block;
margin: 18px 0;
border: 1px solid var(--line);
border: 0;
border-radius: 6px;
padding: 10px 15px;
font-size: 12px;
@@ -699,7 +699,7 @@ button {
top: 62px;
z-index: 25;
background: #fff;
border-bottom: 1px solid var(--line);
border: 0;
}
.mobile-bar button {
display: flex;
@@ -716,7 +716,7 @@ button {
.mobile-navigation {
display: block;
padding: 15px 22px;
border-bottom: 1px solid var(--line);
border: 0;
max-height: 65dvh;
overflow: auto;
}
@@ -833,6 +833,7 @@ button {
*,
*::before,
*::after {
animation: none !important;
transition: none !important;
}
}
@@ -852,7 +853,7 @@ button {
.table-scroll {
overflow-x: auto;
border: 1px solid var(--line);
border: 0;
border-radius: 7px;
margin: 24px 0;
}
@@ -981,7 +982,7 @@ button {
.auth-card {
width: min(390px, calc(100vw - 40px));
padding: 36px;
border: 1px solid var(--line);
border: 0;
border-radius: 12px;
background: white;
box-shadow: 0 15px 45px #34523c06;
@@ -1007,7 +1008,7 @@ button {
.auth-card input {
width: 100%;
padding: 13px;
border: 1px solid #d8e2d7;
border: 0;
border-radius: 6px;
font:
24px ui-monospace,
@@ -1060,7 +1061,7 @@ button {
.library-intro p { margin: 0; font-size: 16px; color: var(--muted); }
.featured-section h2, .library-section h2 { font-size: 19px; font-weight: 550; margin: 0 0 22px; }
.featured-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: 20px; }
.featured-link { position: relative; border: 1px solid var(--line); border-radius: 8px; padding: 26px; }
.featured-link { position: relative; border: 0; border-radius: 8px; padding: 26px; }
.featured-link:hover { border-color: #a4b7aa; background: #fafcfb; }
.featured-link > svg:first-child { color: var(--green); margin-bottom: 18px; }
.featured-link h3 { font-size: 18px; font-weight: 550; line-height: 1.6; margin: 0 0 10px; padding-right: 18px; }
@@ -1071,11 +1072,11 @@ button {
.collection-filters { display: flex; gap: 6px; flex-wrap: wrap; }
.collection-filters button { font-size: 14px; padding: 8px 12px; border-radius: 5px; color: var(--muted); }
.collection-filters button[aria-pressed="true"] { background: #eef4f0; color: var(--green); }
.library-search { display: flex; align-items: center; gap: 8px; color: var(--muted); border: 1px solid var(--line); border-radius: 6px; padding: 9px 12px; }
.library-search { display: flex; align-items: center; gap: 8px; color: var(--muted); border: 0; border-radius: 6px; padding: 9px 12px; }
.library-search input { min-width: 0; width: 145px; outline: 0; font-size: 14px; }
.library-search:focus-within { border-color: var(--green); }
.library-list { padding: 0; margin: 0; list-style: none; border-top: 1px solid var(--line); }
.library-list li { border-bottom: 1px solid var(--line); }
.library-list { padding: 0; margin: 0; list-style: none; border: 0; }
.library-list li { border: 0; }
.library-list a { display: flex; align-items: center; gap: 18px; padding: 20px 4px; }
.library-list a:hover { color: var(--green); background: #fafcfb; }
.library-row-title { flex: 1; font-size: 15px; line-height: 1.7; }
@@ -1105,3 +1106,159 @@ button {
.site-footer { font-size: 13px; }
}
.search-status { padding: 12px 20px 0; font-size: 13px; color: var(--muted); }
/* Shared typography; each document keeps its own identity and reading context. */
:root { --ink: #202724; --muted: #707873; --green: #27745b; --accent-wash: #e9f2ec; --ease: cubic-bezier(.2,.7,.2,1); }
body { background: #fdfefd; }
svg { flex-shrink: 0; }
[data-tone="blue"] { --green: #456da2; --accent-wash: #eaf0f9; }
[data-tone="violet"] { --green: #79609e; --accent-wash: #f1ecf8; }
[data-tone="amber"] { --green: #927346; --accent-wash: #f6f0e6; }
.site-header { height: 64px; padding: 0 30px; gap: 32px; border-bottom: 1px solid #edf0ee; background: #fdfefded; backdrop-filter: blur(18px); }
.brand { gap: 10px; }
.brand-mark i { background: linear-gradient(160deg,#80b69a,#27745b); }
.docs-badge { color: #819087; padding: 0; font-size: 13px; }
.top-nav { align-self: stretch; align-items: stretch; gap: 26px; margin-left: 16px; }
.top-nav a, .top-nav a:nth-child(2) { display: flex; align-items: center; position: relative; white-space: nowrap; transition: color 180ms; }
.top-nav a::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--green); transform: scaleX(0); transform-origin: left; transition: transform 220ms var(--ease); }
.top-nav a:hover, .top-nav a[aria-current="page"] { color: var(--ink); }
.top-nav a[aria-current="page"]::after { transform: scaleX(1); }
.search-trigger { margin-left: auto; width: 218px; padding: 8px 12px; background: #f0f3f1; color: #768078; transition: background 180ms,box-shadow 180ms; }
.search-trigger:hover { background: #eaf0ec; box-shadow: 0 3px 16px #1c3c2810; }
.search-trigger span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.search-trigger kbd { background: transparent; font-size: 11px; white-space: nowrap; }
.logout-button { font-size: 12px; }
.home-layout { grid-template-columns: 272px minmax(0,1fr); }
.reading-layout { grid-template-columns: 272px minmax(0,1fr) 222px; }
.sidebar { padding: 30px 26px 40px; height: calc(100dvh - 64px); top: 64px; scrollbar-width: thin; scrollbar-color: #d9e2dc transparent; }
.nav-back { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; color: #8a948e; margin: 0 8px 28px; transition: color 160ms; }
.nav-back:hover { color: var(--green); }
.collection-identity { display: flex; gap: 12px; align-items: center; color: var(--green); font-size: 16px; font-weight: 550; margin: 0 8px 28px; line-height: 1.5; }
.collection-nav-link { display: flex; align-items: center; gap: 12px; padding: 12px 10px; margin-bottom: 6px; border-radius: 7px; color: #647169; transition: background 180ms,color 180ms; }
.collection-nav-link svg:last-child { margin-left: auto; opacity: 0; transform: translateX(-3px); transition: opacity 180ms,transform 180ms; }
.collection-nav-link:hover { background: #f0f5f1; color: var(--green); }
.collection-nav-link:hover svg:last-child { opacity: 1; transform: none; }
.nav-group { margin-top: 28px; }
.nav-group > p { font-size: 12px; font-weight: 500; color: #909991; margin: 0 10px 10px; }
.nav-item { position: relative; padding: 8px 10px; margin: 2px 0; border-radius: 5px; line-height: 1.65; color: #68756d; transition: color 160ms,background 160ms; }
.nav-item:hover { color: var(--green); background: color-mix(in srgb,var(--accent-wash) 45%,transparent); }
.nav-item[aria-current="page"] { color: var(--green); background: linear-gradient(90deg,var(--accent-wash),transparent); }
.nav-item[aria-current="page"]::before { content: ""; position: absolute; left: -3px; top: 10px; bottom: 10px; width: 2px; border-radius: 2px; background: var(--green); }
.home-main { max-width: 1160px; }
.library-intro { position: relative; padding: 68px 0 46px; isolation: isolate; }
.library-intro::before { content: ""; position: absolute; z-index: -1; width: 420px; max-width: 100%; height: 240px; top: 0; left: -30px; background: radial-gradient(ellipse at 30% 30%,#eaf3ed99,transparent 70%); pointer-events: none; }
.library-intro h1 { font-weight: 550; }
.featured-grid { grid-template-columns: repeat(auto-fit,minmax(min(100%,220px),1fr)); }
.featured-link { display: flex; flex-direction: column; padding: 24px; border-radius: 12px; background: linear-gradient(145deg,color-mix(in srgb,var(--accent-wash) 65%,white),#fbfcfb 65%); transition: transform 240ms var(--ease),box-shadow 240ms var(--ease); }
.featured-link:hover { background: linear-gradient(145deg,var(--accent-wash),#fbfcfb 80%); transform: translateY(-4px); box-shadow: 0 14px 36px #1d35220d; }
.collection-art { display: grid; place-items: center; width: 44px; height: 44px; border-radius: 12px; color: var(--green); background: linear-gradient(145deg,#ffffffc9,var(--accent-wash)); margin-bottom: 26px; box-shadow: 0 2px 8px #25483705; transition: transform 240ms var(--ease); }
.featured-link:hover .collection-art { transform: translateY(-2px) rotate(-4deg); }
.featured-link h3 { font-size: 19px; margin: 0 0 12px; padding: 0; }
.featured-link p { flex: 1; }
.collection-entry { display: flex; align-items: center; justify-content: space-between; margin-top: 28px; color: var(--green); font-size: 13px; }
.collection-entry svg { transition: transform 200ms var(--ease); }
.featured-link:hover .collection-entry svg { transform: translateX(4px); }
.collection-filters { gap: 4px; }
.collection-filters button { font-size: 13px; transition: background 180ms,color 180ms; }
.collection-filters button:hover { color: var(--green); background: #f0f4f1; }
.collection-filters button[aria-pressed="true"] { background: linear-gradient(120deg,var(--accent-wash),#f2f6f3); }
.library-search { background: #f1f4f2; }
.library-search:focus-within { box-shadow: 0 0 0 2px var(--accent-wash); }
.library-list a { padding: 19px 14px; border-radius: 7px; transition: background 180ms,color 180ms; }
.library-list a:hover { background: #f2f6f3; }
.library-row-title { font-size: 14px; }
.library-row-group { font-size: 12px; color: #929b95; }
.library-list svg { transition: transform 180ms; }
.library-list a:hover svg { transform: translateX(3px); }
.site-footer { padding: 32px 0; font-size: 12px; color: #919a94; }
.reading-main { max-width: 852px; padding: 38px 44px 0; }
.breadcrumb { align-items: center; flex-wrap: wrap; font-size: 12px; margin-bottom: 30px; line-height: 1.7; }
.breadcrumb a { color: #6c7970; }
.breadcrumb > span:not(.draft-badge) { max-width: 100%; }
.draft-badge { font-size: 11px; background: #f7f2e9; }
.prose { line-height: 1.9; --tw-prose-links: var(--green); }
.prose > p:first-of-type { font-size: 17px; color: #788379; }
.prose code:not(pre code) { background: #eef2ef; }
.prose tbody tr, .prose thead tr { border: 0; }
.prose tbody tr:nth-child(even) { background: #f6f8f6; }
.prose thead { background: #eef3ef; }
.prose img { background: transparent; padding: 0; }
.callout { padding: 18px 20px; font-size: 14px; background: linear-gradient(120deg,#eef4ef,#f8faf7); }
.callout-draft { background: linear-gradient(120deg,#f7f1e7,#fcfaf5); }
.code-block { background: #f2f5f2; }
.code-toolbar { font-size: 12px; background: #eaf0eb88; }
.code-toolbar button { font-size: 12px; background: #ffffffaa; transition: background 180ms; }
.code-toolbar button:hover { background: white; }
.toc { top: 64px; height: calc(100dvh - 64px); padding: 44px 24px 30px 12px; font-size: 12px; }
.toc > .eyebrow { font-size: 12px; color: #929c94; }
.toc nav a { padding: 7px 10px; margin: 0; border-radius: 4px; color: #8a968d; transition: color 160ms,background 160ms; }
.toc nav a:hover, .toc nav a.active { color: var(--green); background: linear-gradient(90deg,var(--accent-wash),transparent); }
.article-end { margin-top: 30px; font-size: 12px; }
.pagination > a { background: #f3f6f3; transition: background 180ms,transform 180ms var(--ease); }
.pagination > a:hover { background: var(--accent-wash); transform: translateY(-2px); }
.pagination small { font-size: 12px; }
.search-dialog { background: #fdfefd; box-shadow: 0 24px 90px #192d2330; }
.search-dialog[open] { animation: surface-in 180ms var(--ease); }
.search-dialog::backdrop { backdrop-filter: blur(5px); }
.search-input-row { background: #f3f6f3; padding: 20px; }
.search-result.selected, .search-result:hover { background: var(--accent-wash); }
.search-result > span, .search-result small { font-size: 12px; }
.auth-page { background: radial-gradient(ellipse at 50% 30%,#edf5ef,transparent 60%),#fbfdfb; }
.auth-card { box-shadow: 0 18px 64px #294d340b; }
.auth-card input { background: #eff4ef; }
.auth-card button[type="submit"] { background: linear-gradient(120deg,#3e8969,#2c7057); }
@keyframes surface-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }
.reading-main, .library-intro { animation: surface-in 320ms var(--ease) both; }
@media (max-width: 1200px) {
.site-header { gap: 22px; padding: 0 24px; }
.top-nav { gap: 18px; margin-left: 0; }
.search-trigger { width: 180px; }
.reading-layout { grid-template-columns: 248px minmax(0,1fr); }
.home-layout { grid-template-columns: 248px minmax(0,1fr); }
.sidebar { padding-left: 22px; padding-right: 22px; }
.toc { display: none; }
.home-main { padding: 0 36px; }
.mobile-toc { display: block; background: #f3f6f3; }
}
@media (min-width: 761px) and (max-width: 980px) {
.top-nav { display: flex; gap: 14px; font-size: 12px; }
.search-trigger { width: 36px; height: 34px; padding: 8px; justify-content: center; }
.search-trigger span, .search-trigger kbd { display: none; }
.mobile-bar, .mobile-navigation { display: none; }
.sidebar { display: block; }
}
@media (max-width: 760px) {
html { scroll-padding-top: 165px; }
.site-header { height: auto; padding: 0 20px; column-gap: 18px; row-gap: 0; flex-wrap: wrap; }
.brand { height: 62px; font-size: 15px; }
.top-nav { display: flex; order: 4; width: 100%; overflow-x: auto; gap: 24px; min-width: 0; margin: 0; height: 44px; flex: 0 0 100%; font-size: 13px; scrollbar-width: none; }
.top-nav a { padding: 0; }
.search-trigger { width: 36px; height: 34px; padding: 8px; justify-content: center; }
.search-trigger span, .search-trigger kbd { display: none; }
.home-layout, .reading-layout { grid-template-columns: 1fr; }
.sidebar { display: none; }
.mobile-bar { display: block; top: 106px; background: #fdfefdf0; backdrop-filter: blur(14px); }
.mobile-bar button { font-size: 13px; color: var(--green); }
.mobile-navigation { padding: 20px 22px; background: #f7faf7; box-shadow: 0 16px 24px #26382c08; }
.mobile-navigation .collection-identity, .mobile-navigation .nav-back { margin-bottom: 20px; }
.home-main { padding: 0 24px; }
.library-intro { padding: 40px 0 34px; }
.library-intro h1 { font-size: 32px; }
.reading-main { padding: 26px 24px 0; }
.prose h1 { font-size: 29px; }
.breadcrumb { margin-bottom: 24px; }
}
@media (max-width: 520px) {
.featured-grid { grid-template-columns: 1fr; }
.featured-link { padding: 23px; }
.library-section { padding-top: 38px; }
.library-list a { padding: 16px 8px; }
.prose > p:first-of-type { font-size: 16px; }
.pagination strong { font-size: 13px; }
.site-footer { font-size: 12px; }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after { animation: none !important; transition: none !important; }
.featured-link:hover, .featured-link:hover .collection-art, .pagination > a:hover { transform: none; }
}
@@ -0,0 +1,21 @@
# 文档 UI 验收 · 2026-10-28
参考 Supabase Database Overview 与 AI Tools 的桌面界面:共用排版,分别维护文档标识、局部目录与顶部上下文。保留 Tabularium 品牌与现有文档路径。
## 实现
- 顶部切换文档,显示当前选中项。左侧显示当前文档名称、图标、独立目录和返回入口;面包屑连接该文档入口。
- 文档中心按合集提供入口,不把所有章节铺成卡片。原有筛选、分页和全文搜索保留;阅读页搜索提示显示当前文档范围。
- 移除导航、卡片、提示、代码、表格和上下篇等区域的多数边框,以留白、浅色表面和轻渐变建立层次。不同文档使用克制的强调色。
- 入口提供 hover 浮起和箭头反馈;选中项、链接和搜索有过渡;正文及对话框有短暂进入效果。系统设置减少动态效果时关闭动画和过渡。
## 验证
- ESLint、类型检查、Next.js webpack 生产构建通过。认证单元测试 3/3 通过。
- 对最终生产构建运行原有 auth-cli verify:全部文档、搜索索引、附件、静态资源、404 和认证边界通过。
- 浏览器开发预览:1440×900 桌面检查首页、Pretext、语言设计;顶部点击切换后局部目录和搜索提示更新。搜索 nonlocal 返回本合集两篇文章,方向键与 Enter 能打开章节。
- 390×844 检查正文、顶部横向文档切换和本地目录展开;截图未见正文横向溢出。
- 下图为开发预览截图,左下角 Next.js 开发按钮不进入生产产物。生产产物已完成 HTTP 验收;额外的生产浏览器截图因原开发标签页关闭后连接失效未保存。
- 不代表远程 CI 已完成,也不代表 PR 已合并或生产环境已部署。
![桌面语言设计文档](desktop-language.jpg)
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB