mirror of
https://github.com/Hmbown/DeepSeek-TUI.git
synced 2026-09-03 06:50:13 +08:00
feat(web): add /docs/guide and /docs/vocabulary routes
The guide topic flips hasPage true and gains a real page instead of linking out to the repository; vocabulary is a new core-concepts topic. Both are registered in docs-map.ts and app/sitemap.ts, so the docs index, navigation, and sitemap stay in agreement. /docs/guide renders the shared four-step path and the pending real-session media slot. /docs/vocabulary renders the execution nouns, modes and permission postures, route-identity provenance fields, the Consultant advisory role, and the measurement principles — including the explicit statement that this site publishes no benchmark leaderboard. docs/fleet now reads its vocabulary from the shared module rather than an inline copy, so the definitions cannot drift between pages. Tests: docs-ia.test.ts covers information architecture; docs-navigation.test.ts is updated for guide's new local-page routing. Harvested from the #3413 web-maturity lane.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { PRODUCT_TERMS } from "@/lib/content/vocabulary";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
@@ -19,19 +20,10 @@ export default async function FleetPage({ params }: { params: Promise<{ locale:
|
||||
const bodyClass = isZh
|
||||
? "text-ink-soft leading-[1.9] tracking-wide"
|
||||
: "text-ink-soft leading-relaxed";
|
||||
const vocabulary = isZh
|
||||
? [
|
||||
{ term: "Fleet", definition: "谁来做工作:配置好的 worker、角色、模型、主机和信任边界。" },
|
||||
{ term: "Workflow", definition: "工作按什么顺序进行:阶段、门禁、预算、回放和汇总。" },
|
||||
{ term: "Lane", definition: "一个正在运行的 Workflow 实例及其实时进度。" },
|
||||
{ term: "Runtime", definition: "Lane 在哪里、如何执行:本地或远程进程、提供商路由、沙箱和 API 边界。" },
|
||||
]
|
||||
: [
|
||||
{ term: "Fleet", definition: "Who does the work: the configured workers, roles, models, hosts, and trust boundaries." },
|
||||
{ term: "Workflow", definition: "What order the work follows: phases, gates, budgets, replay, and fan-in." },
|
||||
{ term: "Lane", definition: "One running Workflow instance and its live progress." },
|
||||
{ term: "Runtime", definition: "Where and how a Lane executes: local or remote process, provider route, sandbox, and API boundary." },
|
||||
];
|
||||
const vocabulary = PRODUCT_TERMS.map((row) => ({
|
||||
term: row.term,
|
||||
definition: isZh ? row.long.zh : row.long.en,
|
||||
}));
|
||||
|
||||
return (
|
||||
<section className="space-y-10">
|
||||
|
||||
83
web/app/[locale]/docs/guide/page.tsx
Normal file
83
web/app/[locale]/docs/guide/page.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import Link from "next/link";
|
||||
import { GettingStartedSteps } from "@/components/getting-started-steps";
|
||||
import { SessionMedia } from "@/components/session-media";
|
||||
import { GUIDE_NEXT_LINKS } from "@/lib/content/getting-started";
|
||||
import { getMediaAsset } from "@/lib/media-manifest";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
return buildPageMetadata({
|
||||
path: "/docs/guide",
|
||||
locale,
|
||||
title: isZh ? "新手指引 · Codewhale 文档" : "Getting started · Codewhale Docs",
|
||||
description: isZh
|
||||
? "从安装到第一个 Fleet Workflow 的完整路径:安装、无需密钥的首次会话、连接提供商、运行 Fleet。"
|
||||
: "The full path from install to a first Fleet workflow: install, a first keyless session, provider connection, and Fleet.",
|
||||
});
|
||||
}
|
||||
|
||||
export default async function GuidePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
const bodyClass = isZh
|
||||
? "text-ink-soft leading-[1.9] tracking-wide"
|
||||
: "text-ink-soft leading-relaxed";
|
||||
const session = getMediaAsset("first-fleet-session");
|
||||
|
||||
return (
|
||||
<section className="space-y-10">
|
||||
<section id="overview" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">{isZh ? "新手指引" : "Getting started"}</h2>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh
|
||||
? "从一条安装命令到第一个 Fleet Workflow,四步走完。每一步都只陈述当前版本真实的行为;未发布或待录制的内容会明确标注。"
|
||||
: "Four steps from one install command to a first Fleet workflow. Every step states only what the current candidate actually does; anything unreleased or unrecorded is labeled as such."}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="path" className="scroll-mt-32">
|
||||
<GettingStartedSteps locale={locale} />
|
||||
</section>
|
||||
|
||||
{session && (
|
||||
<section id="session-media" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">
|
||||
{isZh ? "看一次真实会话" : "Watch a real session"}
|
||||
</h2>
|
||||
<p className={`${bodyClass} mt-3 mb-4`}>
|
||||
{isZh
|
||||
? "下方是真实会话媒体位。它当前处于待录制状态——这是有意为之:在 v0.9.2 候选版 dogfood 录制完成前,本站不展示任何占位或摆拍影像。"
|
||||
: "Below is the real-session media slot. It is deliberately in the pending state: until the v0.9.2 candidate dogfood recording exists, this site shows no placeholder or staged footage."}
|
||||
</p>
|
||||
<SessionMedia asset={session} locale={locale} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section id="next" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">{isZh ? "接下来" : "Where next"}</h2>
|
||||
<div className="hairline-t mt-4">
|
||||
{GUIDE_NEXT_LINKS.map((item) => (
|
||||
<div key={item.href} className="py-4 hairline-b">
|
||||
<h3 className="font-display text-xl">
|
||||
<Link href={`/${locale}${item.href}`} className="hover:text-indigo transition-colors">
|
||||
{isZh ? item.label.zh : item.label.en}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className={`${bodyClass} mt-1 text-sm`}>{isZh ? item.note.zh : item.note.en}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="source" className="hairline-t pt-8">
|
||||
<p className="text-sm text-ink-mute">
|
||||
{isZh
|
||||
? "来源文档:docs/GUIDE.md, docs/KEYBINDINGS.md · 步骤文案来自 web/lib/content/getting-started.ts;更新时请同步修改 docs-map.ts。"
|
||||
: "Source documents: docs/GUIDE.md, docs/KEYBINDINGS.md · Step copy lives in web/lib/content/getting-started.ts; update docs-map.ts when changing."}
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -28,8 +28,8 @@ export default async function DocsLayout({
|
||||
<h1>{isZh ? "查找准确的使用说明。" : "Find the guidance you need."}</h1>
|
||||
<p>
|
||||
{isZh
|
||||
? "从安装和首次运行开始,或者直接查找模式、权限、工具、提供商、Fleet、MCP 与运行时 API。这些页面就是正式的产品文档;每页都链接仓库中的源文档,方便查阅完整细节。"
|
||||
: "Start with installation and first run, or go straight to modes, permissions, tools, providers, Fleet, MCP, and the Runtime API. These pages are the product documentation; each one cites the source documents in the repository for full detail."}
|
||||
? "从新手指引和安装开始,或者直接查找产品名词、模式、权限、工具、提供商、Fleet、钩子、MCP 与运行时 API。这些页面就是正式的产品文档;每页都链接仓库中的源文档,方便查阅完整细节。"
|
||||
: "Start with the getting-started guide and installation, or go straight to vocabulary, modes, permissions, tools, providers, Fleet, hooks, MCP, and the Runtime API. These pages are the product documentation; each one cites the source documents in the repository for full detail."}
|
||||
</p>
|
||||
<div className="portal-actions">
|
||||
<Link href={`/${locale}/install`} className="portal-button portal-button-primary">
|
||||
|
||||
141
web/app/[locale]/docs/vocabulary/page.tsx
Normal file
141
web/app/[locale]/docs/vocabulary/page.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { StatusBadge } from "@/components/status-badge";
|
||||
import {
|
||||
ADVISORY_ROLE,
|
||||
CONTROL_MODES,
|
||||
MEASUREMENT_PRINCIPLES,
|
||||
PERMISSION_POSTURES,
|
||||
PRODUCT_TERMS,
|
||||
ROUTE_IDENTITY,
|
||||
} from "@/lib/content/vocabulary";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
return buildPageMetadata({
|
||||
path: "/docs/vocabulary",
|
||||
locale,
|
||||
title: isZh ? "产品名词 · Codewhale 文档" : "Vocabulary · Codewhale Docs",
|
||||
description: isZh
|
||||
? "确切的产品名词:Fleet、Workflow、Lane、Runtime、Consultant,Plan / Act / Operate 与权限姿态,以及请求→实际思考档位、路由来源与测量原则。"
|
||||
: "The exact product nouns: Fleet, Workflow, Lane, Runtime, Consultant, Plan / Act / Operate and permission postures, plus requested→effective reasoning, routing source, and measurement principles.",
|
||||
});
|
||||
}
|
||||
|
||||
export default async function VocabularyPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
const bodyClass = isZh
|
||||
? "text-ink-soft leading-[1.9] tracking-wide"
|
||||
: "text-ink-soft leading-relaxed";
|
||||
|
||||
return (
|
||||
<section className="space-y-10">
|
||||
<section id="overview" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">{isZh ? "产品名词" : "Vocabulary"}</h2>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh
|
||||
? "这些名词在全站、TUI 和收据里含义完全一致。名词本身不翻译;每条定义都与仓库中的公共事实矩阵逐字对应。"
|
||||
: "These nouns mean the same thing on this site, in the TUI, and in receipts. The nouns themselves are never translated; every definition matches the public fact matrix in the repository verbatim."}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="execution-nouns" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">
|
||||
{isZh ? "执行名词" : "Execution nouns"}
|
||||
</h2>
|
||||
<div className="hairline-t mt-4">
|
||||
{PRODUCT_TERMS.map((row) => (
|
||||
<section key={row.term} className="py-4 hairline-b">
|
||||
<h3 className="font-display text-xl">
|
||||
{row.term}{" "}
|
||||
<span className="text-sm text-ink-mute font-normal">
|
||||
= {isZh ? row.short.zh : row.short.en}
|
||||
</span>
|
||||
</h3>
|
||||
<p className={`${bodyClass} mt-1 text-sm`}>{isZh ? row.long.zh : row.long.en}</p>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="control-nouns" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">
|
||||
{isZh ? "模式与权限姿态" : "Modes and permission postures"}
|
||||
</h2>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh
|
||||
? "模式(Tab 循环,输入框空闲时)决定可见的交互方式;权限姿态(Shift+Tab 循环)决定工具执行前询问的激进程度。两者正交。"
|
||||
: "Modes (Tab, composer idle) decide the visible interaction; permission postures (Shift+Tab) decide how aggressively tools ask before executing. The two are orthogonal."}
|
||||
</p>
|
||||
<div className="hairline-t mt-4">
|
||||
{[...CONTROL_MODES, ...PERMISSION_POSTURES].map((row) => (
|
||||
<section key={row.term} className="py-4 hairline-b">
|
||||
<h3 className="font-display text-xl">{row.term}</h3>
|
||||
<p className={`${bodyClass} mt-1 text-sm`}>
|
||||
{isZh ? row.description.zh : row.description.en}
|
||||
</p>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="route-identity" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">
|
||||
{isZh
|
||||
? "路由身份:provider · 模型 · 请求→实际思考档位 · 来源"
|
||||
: "Route identity: provider · model · requested→effective reasoning · source"}
|
||||
</h2>
|
||||
<div className="hairline-t mt-4">
|
||||
{ROUTE_IDENTITY.map((row) => (
|
||||
<section key={row.term} className="py-4 hairline-b">
|
||||
<h3 className="font-display text-xl">{row.term}</h3>
|
||||
<p className={`${bodyClass} mt-1 text-sm`}>
|
||||
{isZh ? row.description.zh : row.description.en}
|
||||
</p>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="advisory-role" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">
|
||||
{isZh ? "咨询角色" : "Advisory role"}
|
||||
</h2>
|
||||
<div className="hairline-t mt-4 py-4 hairline-b">
|
||||
<h3 className="font-display text-xl">{ADVISORY_ROLE.term}</h3>
|
||||
<p className={`${bodyClass} mt-1 text-sm`}>
|
||||
{isZh ? ADVISORY_ROLE.description.zh : ADVISORY_ROLE.description.en}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="measurement" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">
|
||||
{isZh ? "测量原则" : "Measurement principles"}
|
||||
</h2>
|
||||
<ul className="mt-4 space-y-3">
|
||||
{MEASUREMENT_PRINCIPLES.map((principle) => (
|
||||
<li key={principle.en} className={`${bodyClass} text-sm`}>
|
||||
{isZh ? principle.zh : principle.en}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className={`${bodyClass} mt-4 text-sm`}>
|
||||
<StatusBadge kind="unavailable" locale={locale} />{" "}
|
||||
{isZh
|
||||
? "基准排行榜:本站没有,也不会在没有路由身份与测量工具链的情况下出现。"
|
||||
: "Benchmark leaderboard: none on this site, and none will appear without route identity and a measurement harness attached."}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="source" className="hairline-t pt-8">
|
||||
<p className="text-sm text-ink-mute">
|
||||
{isZh
|
||||
? "来源文档:docs/FLEET.md, docs/MODES.md, docs/public-surface-facts.json · 名词文案来自 web/lib/content/vocabulary.ts;更新时请同步修改 docs-map.ts。"
|
||||
: "Source documents: docs/FLEET.md, docs/MODES.md, docs/public-surface-facts.json · Vocabulary copy lives in web/lib/content/vocabulary.ts; update docs-map.ts when changing."}
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { SITE_URL } from "@/lib/page-meta";
|
||||
|
||||
// Public, indexable routes (locale-prefixed). /admin and /api are
|
||||
// intentionally excluded; see app/robots.ts.
|
||||
const PATHS = ["", "/install", "/constitution", "/models", "/runtime", "/docs", "/docs/configuration", "/docs/constitution", "/docs/fleet", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/web", "/docs/work", "/faq", "/roadmap", "/feed", "/digest", "/contribute", "/community"];
|
||||
const PATHS = ["", "/install", "/constitution", "/models", "/runtime", "/docs", "/docs/configuration", "/docs/constitution", "/docs/fleet", "/docs/guide", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/vocabulary", "/docs/web", "/docs/work", "/faq", "/roadmap", "/feed", "/digest", "/contribute", "/community"];
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const lastModified = new Date();
|
||||
|
||||
35
web/components/getting-started-steps.tsx
Normal file
35
web/components/getting-started-steps.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* <GettingStartedSteps> — renders the shared new-user path from
|
||||
* web/lib/content/getting-started.ts: install → first offline session →
|
||||
* provider connection → first Fleet workflow.
|
||||
*
|
||||
* Used by the homepage band and the /docs/guide page so the path reads
|
||||
* identically in both places. Server component, SSG-safe.
|
||||
*/
|
||||
|
||||
import Link from "next/link";
|
||||
import { GETTING_STARTED_STEPS } from "@/lib/content/getting-started";
|
||||
|
||||
export function GettingStartedSteps({ locale = "en" }: { locale?: string }) {
|
||||
const isZh = locale === "zh";
|
||||
|
||||
return (
|
||||
<ol className="gs-steps">
|
||||
{GETTING_STARTED_STEPS.map((step, index) => (
|
||||
<li key={step.id} data-step-id={step.id}>
|
||||
<span className="gs-step-index" aria-hidden="true">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<h3>{isZh ? step.title.zh : step.title.en}</h3>
|
||||
<p>{isZh ? step.body.zh : step.body.en}</p>
|
||||
{step.commands.length > 0 && (
|
||||
<pre className="code-block gs-step-commands"><code>{step.commands.join("\n")}</code></pre>
|
||||
)}
|
||||
<Link href={`/${locale}${step.link.href}`} className="gs-step-link">
|
||||
{isZh ? step.link.label.zh : step.link.label.en} →
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
169
web/lib/docs-ia.test.ts
Normal file
169
web/lib/docs-ia.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Information-architecture contracts: docs-map registration, sitemap and
|
||||
* hreflang preservation, navigation parity across breakpoints and locales,
|
||||
* and the accessibility hooks (skip link, labelled nav, aria-current).
|
||||
*
|
||||
* These are deterministic source/unit contracts in the same style as
|
||||
* public-copy.test.ts: they read the real sources and assert structure, so a
|
||||
* future IA change fails here first instead of drifting silently.
|
||||
*/
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DOC_TOPICS, docTopicHref, getTopic } from "./docs-map";
|
||||
import { docsTopicIsCurrent } from "./docs-navigation";
|
||||
import { locales } from "./i18n/config";
|
||||
|
||||
const webRoot = new URL("../", import.meta.url);
|
||||
const repoRoot = new URL("../../", import.meta.url);
|
||||
|
||||
function webText(path: string): string {
|
||||
return readFileSync(new URL(path, webRoot), "utf8");
|
||||
}
|
||||
|
||||
const sitemap = webText("app/sitemap.ts");
|
||||
const nav = webText("components/nav.tsx");
|
||||
const navLinks = webText("components/nav-links.tsx");
|
||||
const mobileMenu = webText("components/mobile-menu.tsx");
|
||||
const footer = webText("components/footer.tsx");
|
||||
const localeLayout = webText("app/[locale]/layout.tsx");
|
||||
const css = webText("app/globals.css");
|
||||
|
||||
describe("docs-map registration", () => {
|
||||
it("registers the guide and vocabulary topics as first-party pages", () => {
|
||||
const guide = getTopic("guide");
|
||||
const vocabulary = getTopic("vocabulary");
|
||||
expect(guide?.hasPage).toBe(true);
|
||||
expect(vocabulary?.hasPage).toBe(true);
|
||||
expect(vocabulary?.category).toBe("core-concepts");
|
||||
expect(docTopicHref(guide!, "en")).toBe("/en/docs/guide");
|
||||
expect(docTopicHref(vocabulary!, "zh")).toBe("/zh/docs/vocabulary");
|
||||
expect(docsTopicIsCurrent(vocabulary!, "en", "/en/docs/vocabulary")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps every docs topic repo source on disk", () => {
|
||||
for (const topic of DOC_TOPICS) {
|
||||
const sources = Array.isArray(topic.repoSource) ? topic.repoSource : [topic.repoSource];
|
||||
for (const source of sources) {
|
||||
expect(existsSync(new URL(source, repoRoot)), `${topic.id}: ${source}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps topic labels and descriptions bilingual", () => {
|
||||
for (const topic of DOC_TOPICS) {
|
||||
for (const pair of [topic.label, topic.description]) {
|
||||
expect(pair.en.trim().length, `${topic.id} en`).toBeGreaterThan(0);
|
||||
expect(pair.zh.trim().length, `${topic.id} zh`).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("sitemap and hreflang preservation", () => {
|
||||
it("indexes every first-party docs page", () => {
|
||||
for (const topic of DOC_TOPICS) {
|
||||
if (!topic.hasPage) continue;
|
||||
const path = topic.sitePath ? `/${topic.sitePath}` : `/docs/${topic.slug}`;
|
||||
expect(sitemap, path).toContain(`"${path}"`);
|
||||
}
|
||||
expect(sitemap).toContain('"/docs/guide"');
|
||||
expect(sitemap).toContain('"/docs/vocabulary"');
|
||||
});
|
||||
|
||||
it("keeps per-locale alternate pairs for every indexed route", () => {
|
||||
expect(sitemap).toContain("alternates");
|
||||
expect(sitemap).toContain("en: `${SITE_URL}/en${path}`");
|
||||
expect(sitemap).toContain("zh: `${SITE_URL}/zh${path}`");
|
||||
// Route generation is driven by the shipped locale set, never hardcoded.
|
||||
expect(sitemap).toContain("locales.map");
|
||||
expect(locales).toEqual(["en", "zh"]);
|
||||
});
|
||||
|
||||
it("keeps the new docs pages on the shared metadata helper", () => {
|
||||
for (const route of ["guide", "vocabulary"]) {
|
||||
const page = webText(`app/[locale]/docs/${route}/page.tsx`);
|
||||
expect(page, route).toContain('import { buildPageMetadata } from "@/lib/page-meta"');
|
||||
expect(page, route).toContain(`path: "/docs/${route}"`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("navigation parity and accessibility", () => {
|
||||
it("keeps desktop and mobile navigation on one shared link set", () => {
|
||||
// Both surfaces consume the same `links` prop from nav.tsx — assert the
|
||||
// wiring rather than duplicating the arrays.
|
||||
expect(nav).toContain("<NavLinks links={links} isZh={isZh} />");
|
||||
expect(nav).toContain("links={links}");
|
||||
expect(mobileMenu).toContain("links.map");
|
||||
expect(navLinks).toContain("links.map");
|
||||
});
|
||||
|
||||
it("keeps en/zh nav link paths in exact locale-swap parity", () => {
|
||||
const enHrefs = [...nav.matchAll(/href: "\/en\/([^"]+)"/g)].map((m) => m[1]);
|
||||
const zhHrefs = [...nav.matchAll(/href: "\/zh\/([^"]+)"/g)].map((m) => m[1]);
|
||||
expect(enHrefs.length).toBeGreaterThanOrEqual(4);
|
||||
expect(enHrefs).toEqual(zhHrefs);
|
||||
expect(enHrefs).toContain("docs/guide");
|
||||
expect(enHrefs).toContain("faq");
|
||||
});
|
||||
|
||||
it("labels the primary nav and marks the current page accessibly", () => {
|
||||
expect(navLinks).toContain('aria-label={isZh ? "主导航" : "Primary"}');
|
||||
expect(navLinks).toContain('aria-current={isActive ? "page" : undefined}');
|
||||
expect(mobileMenu).toContain('aria-current={isActive ? "page" : undefined}');
|
||||
expect(mobileMenu).toContain('aria-expanded={open}');
|
||||
expect(mobileMenu).toContain('aria-controls="mobile-menu"');
|
||||
expect(mobileMenu).toContain('role="dialog"');
|
||||
});
|
||||
|
||||
it("ships a keyboard-reachable skip link to the main landmark", () => {
|
||||
expect(localeLayout).toContain('href="#main-content"');
|
||||
expect(localeLayout).toContain('className="skip-link"');
|
||||
expect(localeLayout).toContain('<main id="main-content">');
|
||||
expect(css).toContain(".skip-link:focus-visible");
|
||||
});
|
||||
|
||||
it("keeps responsive breakpoints for the getting-started steps", () => {
|
||||
// 4-up grid by default, 2-up at the tablet breakpoint, 1-up on phones —
|
||||
// the same responsive ladder as the existing workflow steps.
|
||||
expect(css).toMatch(/\.gs-steps\s*\{[^}]*repeat\(4, minmax\(0, 1fr\)\)/);
|
||||
expect(css).toMatch(
|
||||
/@media \(max-width: 760px\)[\s\S]*?\.gs-steps\s*\{[^}]*repeat\(2, minmax\(0, 1fr\)\)/,
|
||||
);
|
||||
expect(css).toMatch(
|
||||
/@media \(max-width: 520px\)[\s\S]*?\.gs-steps\s*\{\s*grid-template-columns: 1fr/,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the footer discovery links alongside the pinned legal links", () => {
|
||||
expect(footer).toContain('href: "/en/docs/guide"');
|
||||
expect(footer).toContain('href: "/zh/docs/guide"');
|
||||
expect(footer).toContain('href: "/en/faq"');
|
||||
expect(footer).toContain(
|
||||
'{ label: "MIT license", href: "https://github.com/Hmbown/CodeWhale/blob/main/LICENSE" }',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("homepage integration", () => {
|
||||
const homepage = webText("app/[locale]/page.tsx");
|
||||
|
||||
it("renders the shared getting-started path on the homepage", () => {
|
||||
expect(homepage).toContain('import { GettingStartedSteps } from "@/components/getting-started-steps"');
|
||||
expect(homepage).toContain("<GettingStartedSteps locale={locale} />");
|
||||
expect(homepage).toContain("product-start");
|
||||
expect(homepage).toContain("/docs/guide");
|
||||
expect(homepage).toContain("/docs/vocabulary");
|
||||
});
|
||||
|
||||
it("keeps the previously pinned homepage facts intact", () => {
|
||||
// Guard against the new band accidentally displacing the public-copy
|
||||
// gate's required surface (the full contract lives in public-copy.test.ts).
|
||||
expect(homepage).toContain("facts.latestPublishedRelease");
|
||||
expect(homepage).toContain("Source candidate");
|
||||
expect(homepage).toContain('src="/codewhale-tui.png"');
|
||||
for (const label of ["Plan", "Act", "Operate", "Ask", "Auto-Review", "Full Access"]) {
|
||||
expect(homepage).toContain(label);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -53,9 +53,21 @@ export const DOC_TOPICS: DocTopic[] = [
|
||||
zh: "首次运行、会话、命令、快捷键和日常使用流程。",
|
||||
},
|
||||
repoSource: ["docs/GUIDE.md", "docs/KEYBINDINGS.md"],
|
||||
hasPage: false,
|
||||
hasPage: true,
|
||||
category: "getting-started",
|
||||
},
|
||||
{
|
||||
id: "vocabulary",
|
||||
slug: "vocabulary",
|
||||
label: { en: "Vocabulary", zh: "产品名词" },
|
||||
description: {
|
||||
en: "The exact product nouns — Fleet, Workflow, Lane, Runtime; Plan / Act / Operate; Consultant; and explicit route provenance — plus measurement principles.",
|
||||
zh: "确切的产品名词——Fleet、Workflow、Lane、Runtime;Plan / Act / Operate;Consultant;明确的路由来源——以及测量原则。",
|
||||
},
|
||||
repoSource: ["docs/FLEET.md", "docs/MODES.md", "docs/public-surface-facts.json"],
|
||||
hasPage: true,
|
||||
category: "core-concepts",
|
||||
},
|
||||
{
|
||||
id: "configuration",
|
||||
slug: "configuration",
|
||||
|
||||
@@ -28,8 +28,15 @@ describe("docsTopicIsCurrent", () => {
|
||||
expect(docTopicIsExternal(topic("providers"))).toBe(false);
|
||||
});
|
||||
|
||||
it("routes the guide to its dedicated docs page in either locale", () => {
|
||||
expect(docTopicHref(topic("guide"), "en")).toBe("/en/docs/guide");
|
||||
expect(docsTopicIsCurrent(topic("guide"), "en", "/en/docs/guide")).toBe(true);
|
||||
expect(docsTopicIsCurrent(topic("guide"), "zh", "/zh/docs/guide/")).toBe(true);
|
||||
expect(docTopicIsExternal(topic("guide"))).toBe(false);
|
||||
});
|
||||
|
||||
it("never marks source-document links as local pages", () => {
|
||||
expect(docsTopicIsCurrent(topic("guide"), "en", "/en/docs/guide")).toBe(false);
|
||||
expect(docsTopicIsCurrent(topic("contribution"), "en", "/en/docs/contribution")).toBe(false);
|
||||
});
|
||||
|
||||
it("routes former link-out topics to their dedicated docs pages", () => {
|
||||
|
||||
Reference in New Issue
Block a user