From 787491622baf59ba57a6616a739cd83f6289b61f Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Wed, 28 Oct 2026 19:38:07 +0800 Subject: [PATCH] Replace collection select with horizontal category dropdown navigation --- apps/docs/scripts/auth-cli.ts | 7 +- .../src/components/CategoryNavigation.tsx | 93 +++++++++++++++++++ apps/docs/src/components/Shell.tsx | 21 +---- apps/docs/src/styles/globals.css | 45 +++++++-- 4 files changed, 137 insertions(+), 29 deletions(-) create mode 100644 apps/docs/src/components/CategoryNavigation.tsx diff --git a/apps/docs/scripts/auth-cli.ts b/apps/docs/scripts/auth-cli.ts index e06d461..e702ff5 100644 --- a/apps/docs/scripts/auth-cli.ts +++ b/apps/docs/scripts/auth-cli.ts @@ -203,8 +203,11 @@ async function main() { if (route === "/") home = html; if (route === "/docs/endor/endor-purpose-draft") purpose = html; if (route.startsWith("/docs/")) { - assert.ok(html.includes('aria-label="文档选择"'), route); - assert.ok(html.includes(''), route); + assert.ok(html.includes('aria-label="文档分类"'), route); + assert.ok(html.includes('data-category="语言与运行时"'), route); + assert.ok(html.includes('class="category-panel" hidden=""'), route); + assert.ok(!html.includes('class="collection-picker"'), route); + assert.ok(!html.includes(" match[1]); assert.equal(new Set(ids).size, ids.length, `Unique heading anchors: ${route}`); diff --git a/apps/docs/src/components/CategoryNavigation.tsx b/apps/docs/src/components/CategoryNavigation.tsx new file mode 100644 index 0000000..6011d72 --- /dev/null +++ b/apps/docs/src/components/CategoryNavigation.tsx @@ -0,0 +1,93 @@ +import Link from "next/link"; +import { useEffect, useId, useRef, useState, type KeyboardEvent } from "react"; +import type { DocLink } from "@/lib/docs"; +import { collectionEntries, collectionIcon, collectionLabel, collectionTone } from "@/lib/collections"; +import { Icon } from "./Icon"; + +export function CategoryNavigation({ docs, current }: { docs: DocLink[]; current?: string }) { + const navigation = useRef(null); + const triggers = useRef(new Map()); + const [open, setOpen] = useState(null); + const id = useId(); + const categories = [...new Set(docs.map((doc) => doc.category))]; + const collections = collectionEntries(docs); + const selected = docs.find((doc) => doc.slug === current); + + useEffect(() => { + function outside(event: PointerEvent) { + if (event.target instanceof Node && !navigation.current?.contains(event.target)) setOpen(null); + } + function escape(event: globalThis.KeyboardEvent) { + if (event.key !== "Escape" || !open) return; + if (navigation.current?.contains(document.activeElement)) { + event.preventDefault(); + triggers.current.get(open)?.focus(); + } + setOpen(null); + } + document.addEventListener("pointerdown", outside); + document.addEventListener("keydown", escape); + return () => { + document.removeEventListener("pointerdown", outside); + document.removeEventListener("keydown", escape); + }; + }, [open]); + + function focusItem(panel: string, last = false) { + requestAnimationFrame(() => { + const links = document.getElementById(panel)?.querySelectorAll("a"); + links?.[last ? links.length - 1 : 0]?.focus(); + }); + } + function move(event: KeyboardEvent) { + if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return; + const links = [...event.currentTarget.querySelectorAll("a")]; + const index = links.indexOf(document.activeElement as HTMLAnchorElement); + if (index < 0) return; + event.preventDefault(); + const next = event.key === "Home" ? 0 : event.key === "End" ? links.length - 1 : + (index + (event.key === "ArrowDown" ? 1 : -1) + links.length) % links.length; + links[next]?.focus(); + } + + return ; +} diff --git a/apps/docs/src/components/Shell.tsx b/apps/docs/src/components/Shell.tsx index 533ea44..9de18eb 100644 --- a/apps/docs/src/components/Shell.tsx +++ b/apps/docs/src/components/Shell.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { useRouter } from "next/router"; +import { CategoryNavigation } from "./CategoryNavigation"; import { Icon } from "@/components/Icon"; import { useEffect, useRef, useState, type ReactNode } from "react"; import type { DocLink } from "@/lib/docs"; @@ -69,7 +69,6 @@ export function Shell({ current?: string; children: ReactNode; }) { - const router = useRouter(); const dialog = useRef(null); const input = useRef(null); const trigger = useRef(null); @@ -80,7 +79,6 @@ 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 ?? ""}` @@ -125,22 +123,7 @@ export function Shell({
- +