Refine documentation UI with distinct collection navigation and borderless surfaces
Verify and deploy Tabularium / verify-and-deploy (pull_request) Has been cancelled
Verify and deploy Tabularium / verify-and-deploy (pull_request) Has been cancelled
This commit is contained in:
@@ -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`。这些文件不作为站点静态资源发布。
|
||||
|
||||
|
||||
@@ -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" /></>,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 已合并或生产环境已部署。
|
||||
|
||||

|
||||
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Reference in New Issue
Block a user