Expand document collections with Pretext tutorial and simplify library navigation
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m47s
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m47s
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
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";
|
||||
size?: number; style?: CSSProperties;
|
||||
}) {
|
||||
const paths = {
|
||||
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" /></>,
|
||||
code: <path d="m8 7-5 5 5 5m8-10 5 5-5 5M14 4l-4 16" />,
|
||||
menu: <path d="M4 6h16M4 12h16M4 18h16" />,
|
||||
close: <path d="m6 6 12 12M6 18 18 6" />,
|
||||
};
|
||||
return <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={style}>{paths[name]}</svg>;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
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 { sectionFor } from "@/lib/navigation";
|
||||
|
||||
|
||||
export function Brand() {
|
||||
return (
|
||||
@@ -28,7 +29,7 @@ export function Navigation({
|
||||
current?: string;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
const groups = [...new Set(docs.map((d) => sectionFor(d.slug).group))];
|
||||
const groups = [...new Set(docs.map((d) => d.group))];
|
||||
return (
|
||||
<nav aria-label="文档导航">
|
||||
<Link
|
||||
@@ -36,13 +37,13 @@ export function Navigation({
|
||||
className={`nav-home ${!current ? "selected" : ""}`}
|
||||
href="/"
|
||||
>
|
||||
▦ <span>文档概览</span>
|
||||
<Icon name="book" size={17} /> <span>资料库</span>
|
||||
</Link>
|
||||
{groups.map((group) => (
|
||||
<div className="nav-group" key={group}>
|
||||
<p>{group}</p>
|
||||
{docs
|
||||
.filter((d) => sectionFor(d.slug).group === group)
|
||||
.filter((d) => d.group === group)
|
||||
.map((d) => (
|
||||
<Link
|
||||
onClick={onNavigate}
|
||||
@@ -52,17 +53,13 @@ export function Navigation({
|
||||
key={d.slug}
|
||||
>
|
||||
{d.title}
|
||||
{sectionFor(d.slug).label.includes("草案") && (
|
||||
{d.draft && (
|
||||
<span className="tiny-dot" aria-label="草案" />
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<div className="nav-note">
|
||||
<span className="status-dot" /> 与代码一起演进
|
||||
<p>文字、设计与图形的共同档案。</p>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -79,18 +76,32 @@ export function Shell({
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const trigger = useRef<HTMLButtonElement>(null);
|
||||
const [query, setQuery] = useState("");
|
||||
const [searchDocs, setSearchDocs] = useState(docs);
|
||||
const [searchLoaded, setSearchLoaded] = useState(false);
|
||||
const [searchStatus, setSearchStatus] = useState("");
|
||||
const [selected, setSelected] = useState(0);
|
||||
const [menu, setMenu] = useState(false);
|
||||
const results = docs.filter((d) =>
|
||||
`${d.title} ${d.searchText}`
|
||||
const results = searchDocs.filter((d) =>
|
||||
`${d.title} ${d.description} ${d.searchText ?? ""}`
|
||||
.toLocaleLowerCase()
|
||||
.includes(query.trim().toLocaleLowerCase()),
|
||||
);
|
||||
function openSearch() {
|
||||
async function openSearch() {
|
||||
setQuery("");
|
||||
setSelected(0);
|
||||
dialog.current?.showModal();
|
||||
input.current?.focus();
|
||||
if (searchLoaded) return;
|
||||
setSearchStatus("正在加载全文索引…");
|
||||
try {
|
||||
const response = await fetch("/api/search");
|
||||
if (!response.ok) throw new Error("Search unavailable");
|
||||
setSearchDocs(await response.json());
|
||||
setSearchLoaded(true);
|
||||
setSearchStatus("");
|
||||
} catch {
|
||||
setSearchStatus("全文搜索暂时不可用,当前仅搜索标题和摘要。关闭后重新打开可重试。");
|
||||
}
|
||||
}
|
||||
function closeSearch() {
|
||||
dialog.current?.close();
|
||||
@@ -100,8 +111,7 @@ export function Shell({
|
||||
function keyboard(e: KeyboardEvent) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
||||
e.preventDefault();
|
||||
dialog.current?.showModal();
|
||||
input.current?.focus();
|
||||
trigger.current?.click();
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", keyboard);
|
||||
@@ -115,7 +125,7 @@ export function Shell({
|
||||
<header className="site-header">
|
||||
<Brand />
|
||||
<nav className="top-nav" aria-label="主导航">
|
||||
<Link href="/">文档概览</Link>
|
||||
<Link href="/">资料库</Link>
|
||||
<Link href="/docs/00-endor-concepts-draft">架构设计</Link>
|
||||
<a href="https://gitea.entroia.com/Zayden-Jung/endorphin-tabularium-docs">
|
||||
仓库 ↗
|
||||
@@ -127,7 +137,7 @@ export function Shell({
|
||||
aria-label="搜索文档"
|
||||
onClick={openSearch}
|
||||
>
|
||||
<span aria-hidden="true">⌕</span>
|
||||
<Icon name="search" />
|
||||
<span>搜索文档…</span>
|
||||
<kbd>⌘ K</kbd>
|
||||
</button>
|
||||
@@ -154,7 +164,7 @@ export function Shell({
|
||||
aria-expanded={menu}
|
||||
aria-controls="mobile-navigation"
|
||||
>
|
||||
☰ <span>文档目录</span>
|
||||
<Icon name="menu" size={16} /> <span>文档目录</span>
|
||||
<span>{menu ? "−" : "+"}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -178,7 +188,7 @@ export function Shell({
|
||||
onClose={() => trigger.current?.focus()}
|
||||
>
|
||||
<div className="search-input-row">
|
||||
<span aria-hidden="true">⌕</span>
|
||||
<Icon name="search" />
|
||||
<input
|
||||
ref={input}
|
||||
value={query}
|
||||
@@ -214,6 +224,7 @@ export function Shell({
|
||||
Esc
|
||||
</button>
|
||||
</div>
|
||||
{searchStatus && <p className="search-status" role="status">{searchStatus}</p>}
|
||||
<div id="search-results" className="search-results">
|
||||
<p className="eyebrow" role="status">
|
||||
{query ? `${results.length} 篇匹配文档` : "浏览文档"}
|
||||
@@ -228,11 +239,11 @@ export function Shell({
|
||||
key={d.slug}
|
||||
href={`/docs/${d.slug}`}
|
||||
>
|
||||
<span>{sectionFor(d.slug).group}</span>
|
||||
<span>{d.group}</span>
|
||||
<strong>
|
||||
{d.title} <span aria-hidden="true">↗</span>
|
||||
</strong>
|
||||
<small>{sectionFor(d.slug).description}</small>
|
||||
<small>{d.description}</small>
|
||||
</Link>
|
||||
))}
|
||||
{!results.length && (
|
||||
|
||||
Reference in New Issue
Block a user