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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user