diff --git a/README.md b/README.md index fc509b9..14b5a10 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ tools/figures/ 独立 Python 项目,使用 drawsvg 编程制图 - `pnpm start`:在本地预览已构建站点。 - `pnpm check`:依次执行 lint、类型检查、制图与构建。 +站点默认浅色,使用本地系统字体。全站搜索支持正文关键词、`⌘/Ctrl K`、方向键与 Enter;代码块提供复制与反馈。桌面提供分组导航和滚动高亮的页内目录,小屏可展开文档菜单与页内目录。首页入口、草案提示、前后篇导航均引用现有 Markdown 内容。 + +参考设计截图与研究记录保存在 `design/references`;浏览器验收截图与检查说明保存在 `design/verification`。这些文件不作为站点静态资源发布。 + 文档标题取首个 `# 标题`,文档排序按文件名。普通 Markdown 与 GFM 表格可用,嵌入图使用 `/figures/.svg`。不解析原始 HTML。 生产构建会生成文档路由;新增或修改文档后重新运行 build。开发模式按请求读取内容。 diff --git a/apps/docs/src/components/CodeBlock.tsx b/apps/docs/src/components/CodeBlock.tsx new file mode 100644 index 0000000..5654542 --- /dev/null +++ b/apps/docs/src/components/CodeBlock.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; + +export function CodeBlock({ + code, + language = "text", +}: { + code: string; + language?: string; +}) { + const [status, setStatus] = useState("复制"); + async function copy() { + try { + await navigator.clipboard.writeText(code); + setStatus("已复制"); + } catch { + setStatus("复制失败,请手动选择"); + } + setTimeout(() => setStatus("复制"), 2400); + } + return ( +
+
+ {language === "sh" ? "Terminal" : language} + + + {status === "复制" ? "" : status} + +
+
+        
+          {code
+            .replace(/\n$/, "")
+            .split("\n")
+            .map((line, i) => (
+              
+                
+                
+                  {/^(pnpm|uv)\b/.test(line) ? (
+                    <>
+                      {line.split(" ")[0]}
+                      {line.slice(line.indexOf(" "))}
+                    
+                  ) : (
+                    line || "\u00a0"
+                  )}
+                
+              
+            ))}
+        
+      
+
+ ); +} diff --git a/apps/docs/src/components/Shell.tsx b/apps/docs/src/components/Shell.tsx new file mode 100644 index 0000000..8570964 --- /dev/null +++ b/apps/docs/src/components/Shell.tsx @@ -0,0 +1,234 @@ +import Link from "next/link"; +import { useEffect, useRef, useState, type ReactNode } from "react"; +import type { DocLink } from "@/lib/docs"; +import { sectionFor } from "@/lib/navigation"; + +export function Brand() { + return ( + + + + Endor /{" "} + Tabularium + + 文档 + + ); +} +export function Navigation({ + docs, + current, + onNavigate, +}: { + docs: DocLink[]; + current?: string; + onNavigate?: () => void; +}) { + const groups = [...new Set(docs.map((d) => sectionFor(d.slug).group))]; + return ( + + ); +} +export function Shell({ + docs, + current, + children, +}: { + docs: DocLink[]; + current?: string; + children: ReactNode; +}) { + const dialog = useRef(null); + const input = useRef(null); + const trigger = useRef(null); + const [query, setQuery] = useState(""); + const [selected, setSelected] = useState(0); + const [menu, setMenu] = useState(false); + const results = docs.filter((d) => + `${d.title} ${d.searchText}` + .toLocaleLowerCase() + .includes(query.trim().toLocaleLowerCase()), + ); + function openSearch() { + setQuery(""); + setSelected(0); + dialog.current?.showModal(); + input.current?.focus(); + } + function closeSearch() { + dialog.current?.close(); + trigger.current?.focus(); + } + useEffect(() => { + function keyboard(e: KeyboardEvent) { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { + e.preventDefault(); + dialog.current?.showModal(); + input.current?.focus(); + } + } + document.addEventListener("keydown", keyboard); + return () => document.removeEventListener("keydown", keyboard); + }, []); + return ( + <> + + 跳到正文 + +
+ + + +
+
+ +
+ {menu && ( +
+ setMenu(false)} + /> +
+ )} + {children} + { + if (e.target === e.currentTarget) closeSearch(); + }} + onClose={() => trigger.current?.focus()} + > +
+ + { + setQuery(e.target.value); + setSelected(0); + }} + onKeyDown={(e) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + setSelected((i) => + Math.max(0, Math.min(i + 1, results.length - 1)), + ); + } + if (e.key === "ArrowUp") { + e.preventDefault(); + setSelected((i) => Math.max(i - 1, 0)); + } + if (e.key === "Enter") { + e.preventDefault(); + dialog.current + ?.querySelector( + `[data-result="${selected}"]`, + ) + ?.click(); + } + }} + /> + +
+
+

+ {query ? `${results.length} 篇匹配文档` : "浏览文档"} +

+ {results.map((d, i) => ( + + {sectionFor(d.slug).group} + + {d.title} + + {sectionFor(d.slug).description} + + ))} + {!results.length && ( +

+ 没有找到相关文档。试试「节点」「SVG」或「pnpm」。 +

+ )} +
+
+ ↑ ↓ 选择 Enter 打开 · Esc 关闭 +
+
+ + ); +} diff --git a/apps/docs/src/lib/docs.ts b/apps/docs/src/lib/docs.ts index 9c813d9..c3a97bb 100644 --- a/apps/docs/src/lib/docs.ts +++ b/apps/docs/src/lib/docs.ts @@ -4,22 +4,24 @@ import path from "node:path"; const directory = path.resolve(process.cwd(), "../../content/docs"); const validSlug = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; -export type DocLink = { slug: string; title: string }; +export type DocLink = { slug: string; title: string; searchText: string }; export type Doc = DocLink & { content: string }; export async function readDoc(slug: string): Promise { if (!validSlug.test(slug)) throw new Error("Invalid document slug"); const content = await fs.readFile(path.join(directory, `${slug}.md`), "utf8"); const title = content.match(/^# (.+)$/m)?.[1]?.trim() ?? slug; - return { slug, title, content }; + return { slug, title, content, searchText: content }; } export async function listDocs(): Promise { - const files = (await fs.readdir(directory)).filter((file) => - file.endsWith(".md") && validSlug.test(file.slice(0, -3)), - ).sort(); - return Promise.all(files.map(async (file) => { - const { slug, title } = await readDoc(file.slice(0, -3)); - return { slug, title }; - })); + const files = (await fs.readdir(directory)) + .filter((file) => file.endsWith(".md") && validSlug.test(file.slice(0, -3))) + .sort(); + return Promise.all( + files.map(async (file) => { + const { slug, title, searchText } = await readDoc(file.slice(0, -3)); + return { slug, title, searchText }; + }), + ); } diff --git a/apps/docs/src/lib/navigation.ts b/apps/docs/src/lib/navigation.ts new file mode 100644 index 0000000..5779c03 --- /dev/null +++ b/apps/docs/src/lib/navigation.ts @@ -0,0 +1,58 @@ +export type Section = { + group: string; + description: string; + label: string; + symbol: string; +}; +export function sectionFor(slug: string): Section { + if (slug.includes("mounting")) + return { + group: "架构设计", + label: "讨论草案", + symbol: "◇", + description: "探索节点、对象与统一接入协议,记录仍在演进的设计。", + }; + if (slug.includes("figures")) + return { + group: "图形档案", + label: "工具与工作流", + symbol: "⌘", + description: "从 Python 源码到可编辑 SVG,让图形与文档一起演进。", + }; + return { + group: "开始使用", + label: "指南", + symbol: "↗", + description: "搭建本地工作台,添加第一篇文档,了解检查与发布流程。", + }; +} +export type Heading = { id: string; text: string; level: number; line: number }; +export function headingsFor(content: string): Heading[] { + let fenced = false; + const counts = new Map(); + return content.split("\n").flatMap((line, index) => { + if (/^\s*(```|~~~)/.test(line)) { + fenced = !fenced; + return []; + } + const match = !fenced && line.match(/^(#{2,3})\s+(.+?)\s*#*$/); + if (!match) return []; + const text = match[2].replace(/[`*_]/g, ""); + const base = + text + .toLowerCase() + .replace(/[^\p{L}\p{N}\s-]/gu, "") + .trim() + .replace(/\s+/g, "-") || "section"; + const count = counts.get(base) ?? 0; + counts.set(base, count + 1); + return [ + { + id: count ? `${base}-${count}` : base, + text, + level: match[1].length, + line: index + 1, + }, + ]; + }); +} diff --git a/apps/docs/src/pages/docs/[slug].tsx b/apps/docs/src/pages/docs/[slug].tsx index e022f0b..0f9b476 100644 --- a/apps/docs/src/pages/docs/[slug].tsx +++ b/apps/docs/src/pages/docs/[slug].tsx @@ -1,43 +1,235 @@ -import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next"; +import type { + GetStaticPaths, + GetStaticProps, + InferGetStaticPropsType, +} from "next"; import Head from "next/head"; import Link from "next/link"; +import { + Children, + isValidElement, + useEffect, + useState, + type ReactNode, +} from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { listDocs, readDoc, type Doc, type DocLink } from "@/lib/docs"; +import { Shell, Navigation } from "@/components/Shell"; +import { CodeBlock } from "@/components/CodeBlock"; +import { headingsFor, sectionFor } from "@/lib/navigation"; type Props = { doc: Doc; docs: DocLink[] }; - export const getStaticPaths: GetStaticPaths = async () => ({ paths: (await listDocs()).map(({ slug }) => ({ params: { slug } })), fallback: false, }); - export const getStaticProps: GetStaticProps = async ({ params }) => { const slug = params?.slug; if (typeof slug !== "string") return { notFound: true }; const docs = await listDocs(); - if (!docs.some((doc) => doc.slug === slug)) return { notFound: true }; + if (!docs.some((d) => d.slug === slug)) return { notFound: true }; return { props: { doc: await readDoc(slug), docs } }; }; - -export default function DocPage({ doc, docs }: InferGetStaticPropsType) { +function textOf(value: ReactNode): string { + return Children.toArray(value) + .map((child) => + isValidElement<{ children?: ReactNode }>(child) + ? textOf(child.props.children) + : String(child), + ) + .join(""); +} +export default function DocPage({ + doc, + docs, +}: InferGetStaticPropsType) { + const headings = headingsFor(doc.content); + const [active, setActive] = useState(""); + const section = sectionFor(doc.slug); + const index = docs.findIndex((d) => d.slug === doc.slug); + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + const visible = entries.filter((e) => e.isIntersecting); + if (visible[0]) setActive(visible[0].target.id); + }, + { rootMargin: "-80px 0px -60% 0px" }, + ); + document + .querySelectorAll("article h2, article h3") + .forEach((el) => observer.observe(el)); + return () => observer.disconnect(); + }, [doc.slug]); + const toc = ( + + ); return ( <> - {doc.title} · Tabularium -
-
ENDOR / TABULARIUM
- 跳到正文 -
- -
-
- {doc.content} + + {doc.title} · Tabularium + + + +
+ +
+
+ 文档 + / + {section.group} +
+
+ {section.group} + {doc.slug.includes("draft") && ( + 讨论草案 + )} +
+
+ ( +
+ {children}
+
+ ), + h1: ({ children }) => ( + <> +

{children}

+ {headings.length > 0 && ( +
+ 本页内容 + {toc} +
+ )} + + ), + h2: ({ children, node }) => { + const h = headings.find( + (h) => h.line === node?.position?.start.line, + ); + return ( +

+ + {children} + + +

+ ); + }, + h3: ({ children, node }) => { + const h = headings.find( + (h) => h.line === node?.position?.start.line, + ); + return ( +

+ + {children} + + +

+ ); + }, + pre: ({ children }) => { + const child = Children.toArray(children)[0]; + if ( + isValidElement<{ + className?: string; + children?: ReactNode; + }>(child) + ) + return ( + + ); + return
{children}
; + }, + blockquote: ({ children }) => ( +
+ +
{children}
+
+ ), + }} + > + {doc.content} +
+
+ 文档与源码共同维护 + + 查看文档源码 ↗ + +
+ +
+ ENDOR / TABULARIUM 记录思考,连接系统。 +
+
-
+ ); } diff --git a/apps/docs/src/pages/index.tsx b/apps/docs/src/pages/index.tsx index 768dc8b..39824c9 100644 --- a/apps/docs/src/pages/index.tsx +++ b/apps/docs/src/pages/index.tsx @@ -2,32 +2,167 @@ import type { GetStaticProps, InferGetStaticPropsType } from "next"; import Head from "next/head"; import Link from "next/link"; import { listDocs, type DocLink } from "@/lib/docs"; +import { Shell, Navigation } from "@/components/Shell"; +import { CodeBlock } from "@/components/CodeBlock"; +import { sectionFor } from "@/lib/navigation"; -export const getStaticProps: GetStaticProps<{ docs: DocLink[] }> = async () => ({ - props: { docs: await listDocs() }, -}); - -export default function Home({ docs }: InferGetStaticPropsType) { +export const getStaticProps: GetStaticProps<{ + docs: DocLink[]; +}> = async () => ({ props: { docs: await listDocs() } }); +export default function Home({ + docs, +}: InferGetStaticPropsType) { return ( <> - Tabularium · Endor 文档 -
-
- ENDOR / TABULARIUM - 文档工作台 + + Tabularium · Endor 文档 + + + +
+ +
+
+
+

+ ENDOR KNOWLEDGE ARCHIVE +

+

+ 理解系统。 +
+ 构建下一步。 +

+

+ 欢迎来到 Tabularium,Endor 的知识档案。 +
+ 从第一篇文档到系统设计,让想法有迹可循。 +

+
+ + 开始阅读 → + + + 探索档案 ↓ + +
+

+ 文档 · 架构 · 图形 持续生长的工作台 +

+
+
+
+
+
+

EXPLORE THE ARCHIVE

+

从这里开始探索

+
+ + {String(docs.length).padStart(2, "0")} 篇文档 + +
+
+ {docs.map((doc, index) => { + const section = sectionFor(doc.slug); + return ( + +
+ + {section.symbol} + + 0{index + 1} +
+

{section.group}

+

+ {doc.title.replace("(草案)", "")} + +

+

{section.description}

+
+ {section.label} + {doc.slug.includes("draft") && ( + 草案 + )} +
+ + ); + })} +
+
+
+
+

LOCAL WORKSPACE

+

一个命令,继续探索。

+

+ 在本地启动文档工作台, +
+ 让文字和可复现的图形留在同一个仓库。 +

+ + 查看完整启动指南 → + +
+ +
+ +
-

文档 · 设计 · 图形

-

Endor 的知识档案。

-

记录系统概念、架构讨论和开发约定,让文字与图形一起演进。

-
- {docs.map((doc, index) => ( - - {String(index + 1).padStart(2, "0")} -

{doc.title}

- - ))} -
-
+ ); } diff --git a/apps/docs/src/styles/globals.css b/apps/docs/src/styles/globals.css index 61a3c9a..fa77bf6 100644 --- a/apps/docs/src/styles/globals.css +++ b/apps/docs/src/styles/globals.css @@ -1,10 +1,1342 @@ @import "tailwindcss"; @plugin "@tailwindcss/typography"; - @theme { - --font-sans: ui-sans-serif, system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; + --font-sans: + ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", + "Microsoft YaHei", sans-serif; +} +:root { + color-scheme: light; + --ink: #232927; + --muted: #68736b; + --line: #e8ece9; + --green: #20785e; + --soft: #f6f8f6; +} +* { + box-sizing: border-box; +} +html { + scroll-behavior: smooth; + scroll-padding-top: 96px; +} +body { + margin: 0; + background: #fff; + color: var(--ink); + -webkit-font-smoothing: antialiased; +} +button, +a, +input, +summary { + -webkit-tap-highlight-color: transparent; +} +button, +summary { + cursor: pointer; +} +a { + text-decoration: none; +} +button, +input { + font: inherit; +} +button { + color: inherit; +} +::selection { + background: #d5eee2; +} +:focus-visible { + outline: 2px solid var(--green); + outline-offset: 4px; +} +.site-header { + height: 68px; + border-bottom: 1px solid var(--line); + display: flex; + align-items: center; + padding: 0 32px; + gap: 34px; + position: sticky; + top: 0; + background: rgba(255, 255, 255, 0.96); + backdrop-filter: blur(12px); + z-index: 30; +} +.brand { + display: flex; + align-items: center; + gap: 12px; + white-space: nowrap; + font-size: 15px; + letter-spacing: -0.4px; +} +.brand strong { + font-weight: 600; +} +.brand-divider { + margin: 0 7px; + color: #c8ceca; +} +.docs-badge { + font-size: 10px; + padding: 2px 5px; + border: 1px solid var(--line); + border-radius: 4px; + color: var(--muted); + letter-spacing: 0; +} +.brand-mark { + width: 23px; + height: 27px; + display: flex; + gap: 3px; + align-items: center; + transform: skewY(-18deg); +} +.brand-mark i { + width: 5px; + background: var(--green); + height: 23px; + border-radius: 1px; +} +.brand-mark i:nth-child(2) { + height: 17px; +} +.brand-mark i:nth-child(3) { + height: 11px; +} +.top-nav { + display: flex; + gap: 25px; + margin-left: auto; + font-size: 13px; + color: #626b66; +} +.top-nav a:hover { + color: var(--green); +} +.search-trigger { + display: flex; + gap: 9px; + align-items: center; + border: 1px solid #e2e7e3; + background: #fafbf9; + border-radius: 7px; + padding: 7px 10px; + width: 205px; + font-size: 12px; + color: #89918b; +} +.search-trigger > span:first-child { + font-size: 23px; + line-height: 16px; + color: #707b73; +} +.search-trigger kbd { + margin-left: auto; + border: 1px solid var(--line); + border-radius: 3px; + padding: 0 4px; + font-size: 10px; + background: white; +} +.skip-link { + position: fixed; + top: -70px; + left: 20px; + background: white; + padding: 12px; + z-index: 100; +} +.skip-link:focus { + top: 10px; +} +.home-layout, +.reading-layout { + max-width: 1600px; + margin: auto; + display: grid; + grid-template-columns: 238px minmax(0, 1fr); +} +.sidebar { + border-right: 1px solid var(--line); + padding: 32px 22px; + height: calc(100dvh - 68px); + position: sticky; + top: 68px; + overflow-y: auto; + font-size: 13px; +} +.nav-home { + display: flex; + gap: 10px; + align-items: center; + padding: 9px 10px; + border-radius: 5px; + color: #6a756e; +} +.nav-home.selected { + background: #f0f5f1; + color: var(--green); + font-weight: 600; +} +.nav-group { + margin-top: 30px; +} +.nav-group > p { + font-weight: 600; + font-size: 11px; + color: #363f39; + margin: 0 10px 10px; +} +.nav-item { + display: flex; + align-items: center; + gap: 7px; + padding: 9px 10px; + line-height: 1.6; + color: #637066; + border-left: 1px solid var(--line); + margin-left: 10px; +} +.nav-item:hover { + color: var(--green); + background: #fafbf9; +} +.nav-item[aria-current="page"] { + border-left-color: var(--green); + color: var(--green); + font-weight: 550; + background: #f4f8f5; +} +.tiny-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: #c2a56e; + flex-shrink: 0; +} +.nav-note { + border-top: 1px solid var(--line); + margin: 45px 10px 0; + padding-top: 20px; + font-size: 10px; + color: #78847b; + line-height: 1.8; +} +.nav-note p { + color: #a0a8a1; + margin-top: 4px; + font-size: 10px; +} +.status-dot { + display: inline-block; + width: 5px; + height: 5px; + background: #378467; + border-radius: 50%; + margin-right: 7px; + vertical-align: middle; +} +.home-main { + min-width: 0; + padding: 0 48px; +} +.home-hero { + display: grid; + grid-template-columns: 1.3fr 1fr; + align-items: center; + gap: 28px; + padding: 66px 0 58px; + border-bottom: 1px solid var(--line); +} +.eyebrow { + font-size: 10px; + font-weight: 500; + letter-spacing: 1.8px; + color: #849087; + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; + margin: 0; +} +.hero-copy h1 { + font-size: clamp(36px, 3.5vw, 52px); + line-height: 1.28; + letter-spacing: -2px; + font-weight: 550; + margin: 25px 0 19px; +} +.hero-description { + font-size: 15px; + line-height: 2; + color: #637066; +} +.hero-description strong { + font-weight: 500; + color: #4e5f54; +} +.hero-actions { + display: flex; + align-items: center; + gap: 26px; + margin-top: 26px; +} +.primary-button { + display: flex; + gap: 26px; + align-items: center; + color: white; + background: #2c7057; + border: 1px solid #24644d; + box-shadow: 0 2px 3px #204d5010; + border-radius: 6px; + padding: 10px 15px; + font-size: 12px; +} +.primary-button:hover { + background: #235c47; +} +.text-link { + color: #5b6d60; + font-size: 12px; + display: inline-flex; + align-items: center; + gap: 12px; +} +.text-link:hover { + color: var(--green); +} +.hero-footnote { + color: #a3aba4; + font-size: 9px; + margin-top: 34px; + letter-spacing: 0.6px; +} +.hero-footnote span { + border-left: 1px solid #dce3dc; + margin-left: 12px; + padding-left: 12px; +} +.archive-visual { + height: 300px; + position: relative; + background: radial-gradient(ellipse, #edf3eb 0, transparent 70%); + overflow: hidden; +} +.archive-grid { + position: absolute; + inset: 0; + background-image: + linear-gradient(#d8e1d530 1px, transparent 1px), + linear-gradient(90deg, #d8e1d530 1px, transparent 1px); + background-size: 24px 24px; + mask-image: radial-gradient(ellipse, black, transparent 75%); +} +.archive-caption { + position: absolute; + top: 1px; + left: 12px; + font: + 8px ui-monospace, + monospace; + letter-spacing: 2px; + color: #9aab99; +} +.archive-sheet { + position: absolute; + width: 170px; + height: 205px; + left: calc(50% - 85px); + top: 52px; + border: 1px solid #d1dbcd; + border-radius: 5px; + background: #fff; + box-shadow: 0 12px 25px #6073550c; + padding: 20px; +} +.sheet-back { + transform: translate(35px, -16px) rotate(12deg); + background: #edf1e8; + color: #a7b19e; +} +.sheet-middle { + transform: translate(16px, -5px) rotate(5deg); + background: #f6f8f0; + color: #9ba890; +} +.archive-sheet > span:first-child { + font: + 8px ui-monospace, + monospace; + letter-spacing: 1px; +} +.archive-sheet i { + display: block; + width: 80%; + height: 1px; + background: #d5decf; + margin: 15px 0; +} +.sheet-front { + transform: rotate(-7deg); + background: linear-gradient(140deg, #fffef8, #f4f6eb); +} +.sheet-label { + color: #839679; +} +.sheet-emblem { + font-size: 65px; + font-family: Georgia, serif; + color: #56764b; + line-height: 1.2; + margin: 12px 0; +} +.sheet-emblem span { + font: + 28px ui-sans-serif, + system-ui; + vertical-align: top; + margin-left: 14px; + color: #9aaa8c; +} +.sheet-rule { + height: 1px; + background: #d9e0d0; + margin-bottom: 13px; +} +.archive-sheet b { + font: + 12px Georgia, + serif; + color: #637759; +} +.archive-sheet small { + display: block; + color: #a1ad96; + font-size: 8px; + margin-top: 5px; +} +.archive-coordinate { + position: absolute; + bottom: 2px; + left: 15px; + right: 10px; + font-size: 8px; + color: #9aaa98; + letter-spacing: 1px; + display: flex; + justify-content: space-between; +} +.explore-section { + padding: 35px 0; + scroll-margin-top: 90px; +} +.section-heading { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 22px; +} +.section-heading h2, +.quickstart h2 { + margin: 10px 0 0; + font-size: 20px; + font-weight: 550; + letter-spacing: -0.6px; +} +.section-count { + font: + 10px ui-monospace, + monospace; + color: #9ca59d; +} +.doc-cards { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; +} +.doc-card { + border: 1px solid var(--line); + border-radius: 8px; + padding: 21px; + display: flex; + flex-direction: column; + background: linear-gradient(#fff, #fcfdfb); + transition: + border-color 0.2s, + transform 0.2s; +} +.doc-card:hover { + border-color: #a5bdac; + transform: translateY(-3px); +} +.card-top { + display: flex; + align-items: center; + justify-content: space-between; +} +.card-icon { + width: 31px; + height: 31px; + display: grid; + place-items: center; + font-size: 18px; + background: #eef4ef; + border: 1px solid #e2eae1; + border-radius: 7px; + color: #587d5d; +} +.icon-1 { + color: #8b7e60; + background: #f6f3ec; + border-color: #eee8dd; +} +.icon-2 { + color: #6f8291; + background: #eff3f6; + border-color: #e5eaf0; +} +.card-number { + font: + 9px ui-monospace, + monospace; + color: #b1b8b1; +} +.card-group { + font-size: 11px; + color: #73816f; + margin: 22px 0 6px; +} +.doc-card h3 { + font-size: 16px; + font-weight: 550; + letter-spacing: -0.3px; + margin: 0; + display: flex; + justify-content: space-between; + gap: 8px; +} +.doc-card h3 > span { + color: #a0aaa1; + font-size: 13px; +} +.card-description { + color: #69766b; + font-size: 13px; + line-height: 1.9; + margin: 10px 0 22px; + flex: 1; +} +.card-bottom { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 11px; + color: #a1aaa1; + border-top: 1px solid #eef1ed; + padding-top: 14px; +} +.draft-badge { + color: #9a7c40; + background: #faf6e9; + border: 1px solid #eee4c9; + border-radius: 4px; + padding: 2px 6px; + font-size: 9px; + white-space: nowrap; +} +.quickstart { + border-top: 1px solid var(--line); + padding: 35px 0 42px; + display: grid; + grid-template-columns: 1fr 1.2fr; + gap: 35px; + align-items: center; +} +.quickstart p:not(.eyebrow) { + font-size: 12px; + color: #6d796d; + line-height: 1.9; + margin: 13px 0 20px; +} +.site-footer { + border-top: 1px solid var(--line); + display: flex; + justify-content: space-between; + gap: 15px; + padding: 24px 0; + color: #a1aaa1; + font-size: 9px; +} +.site-footer strong { + font-weight: 500; + color: #7b877c; +} +.reading-layout { + grid-template-columns: 238px minmax(0, 1fr) 210px; +} +.reading-main { + min-width: 0; + max-width: 820px; + width: 100%; + margin: auto; + padding: 34px 48px 0; +} +.breadcrumb { + display: flex; + gap: 12px; + font-size: 11px; + color: #8e978f; + margin-bottom: 35px; +} +.breadcrumb a:hover { + color: var(--green); +} +.breadcrumb > span:nth-child(2) { + color: #c6cec7; +} +.article-meta { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 12px; +} +.article-meta .eyebrow { + font-size: 10px; + color: var(--green); + letter-spacing: 1px; +} +.prose { + font-size: 16px; + line-height: 1.95; + color: #606b63; + overflow-wrap: anywhere; + --tw-prose-body: #606b63; + --tw-prose-headings: #252e28; + --tw-prose-bold: #414e44; + --tw-prose-links: #287358; + --tw-prose-counters: #919d93; + --tw-prose-bullets: #a7b2a8; +} +.prose h1 { + font-size: 34px; + letter-spacing: -1.2px; + font-weight: 550; + line-height: 1.45; + margin: 0 0 20px; +} +.prose > p:first-of-type { + font-size: 16px; + color: #6a766b; + line-height: 1.9; + margin-bottom: 34px; +} +.prose h2 { + font-size: 21px; + font-weight: 550; + margin: 44px 0 20px; + padding-top: 10px; + letter-spacing: -0.4px; +} +.prose h3 { + font-size: 18px; + font-weight: 550; + margin: 30px 0 12px; +} +.prose .heading-anchor { + color: inherit; + text-decoration: none; + font-weight: inherit; +} +.heading-anchor > span { + margin-left: 10px; + color: #bac8bc; + font-weight: 400; + opacity: 0; +} +.heading-anchor:hover > span, +.heading-anchor:focus > span { + opacity: 1; +} +.prose a { + text-decoration-color: #b1cdbb; + text-underline-offset: 4px; +} +.prose code:not(pre code) { + font: + 0.85em ui-monospace, + SFMono-Regular, + Consolas, + monospace; + border: 1px solid #e7ece7; + background: #f6f8f5; + border-radius: 4px; + padding: 2px 5px; + font-weight: 400; + color: #4c6655; +} +.prose code::before, +.prose code::after { + content: none; +} +.prose li { + padding-left: 3px; +} +.prose table { + display: table; + width: 100%; + min-width: 360px; + margin: 0; + font-size: 13px; + border: 1px solid var(--line); + border-radius: 7px; +} +.prose thead { + background: #f7f9f5; +} +.prose th, +.prose td { + padding: 12px 15px; + white-space: normal; + min-width: 100px; +} +.prose img { + border: 1px solid var(--line); + border-radius: 8px; + padding: 12px; + background: #fafbf8; + width: 100%; +} +.callout { + display: flex; + gap: 12px; + border: 1px solid #e2e9e0; + padding: 15px 18px; + border-radius: 7px; + background: #f8faf6; + font-size: 13px; + font-weight: 400; + font-style: normal; + color: #6f7f6c; +} +.callout-draft { + border-color: #ebe3d0; + background: #fcfaf3; + color: #927f54; +} +.callout p { + margin: 0; +} +.callout p::before, +.callout p::after { + content: none; +} +.callout-icon { + flex-shrink: 0; + font-size: 16px; +} +.code-block { + border: 1px solid #e1e7e0; + border-radius: 8px; + overflow: hidden; + background: #f9fbf7; + margin: 24px 0; + min-width: 0; +} +.code-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + border-bottom: 1px solid #e7ece4; + color: #879781; + font: + 10px ui-monospace, + SFMono-Regular, + Consolas, + monospace; + background: #f5f8f2; +} +.code-toolbar button { + padding: 2px 6px; + border: 1px solid #dce5d6; + border-radius: 4px; + font-size: 11px; + min-height: 28px; + min-width: 44px; + background: #fff; + color: #607553; +} +.code-toolbar button:hover { + border-color: #95ac8d; +} +.code-block pre, +.prose .code-block pre { + margin: 0; + background: transparent; + color: #5b6f55; + font: + 11px/2.2 ui-monospace, + SFMono-Regular, + Consolas, + monospace; + padding: 16px 15px; + overflow-x: auto; + border-radius: 0; +} +.code-block code { + font: inherit; + color: inherit; +} +.code-line { + display: flex; + min-width: max-content; +} +.line-number { + width: 25px; + flex-shrink: 0; + color: #b3bfb0; + user-select: none; +} +.code-command { + color: #337c57; + font-weight: 600; +} +.quickstart .code-block { + margin: 0; +} +.toc { + padding: 47px 25px 30px 5px; + position: sticky; + top: 68px; + height: calc(100dvh - 68px); + overflow: auto; + font-size: 13px; +} +.toc > .eyebrow { + letter-spacing: 1px; + font-size: 9px; + margin-bottom: 18px; +} +.toc nav { + border-left: 1px solid var(--line); +} +.toc nav a { + display: block; + padding: 7px 0 7px 15px; + margin-left: -1px; + border-left: 1px solid transparent; + color: #69766b; + line-height: 1.6; +} +.toc nav a:hover, +.toc nav a.active { + color: var(--green); + border-left-color: var(--green); +} +.toc nav a.toc-nested { + padding-left: 25px; +} +.toc-note { + font-size: 10px; + margin-top: 35px; + color: #83947f; + border-top: 1px solid var(--line); + padding-top: 20px; + line-height: 1.9; +} +.toc-note p { + font-size: 10px; + color: #a5aea3; + margin-top: 7px; +} +.article-end { + display: flex; + gap: 15px; + justify-content: space-between; + border-top: 1px solid var(--line); + padding: 25px 0; + margin-top: 45px; + font-size: 10px; + color: #9ba59b; +} +.article-end a { + color: #688169; +} +.pagination { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + margin: 10px 0 45px; +} +.pagination > a { + border: 1px solid var(--line); + padding: 18px; + border-radius: 7px; +} +.pagination > a:hover { + border-color: #afc3b1; +} +.pagination small { + display: block; + font-size: 10px; + color: #9aa79a; + margin-bottom: 7px; +} +.pagination strong { + font-size: 12px; + font-weight: 500; + color: #526c55; +} +.next-page { + text-align: right; +} +.article-footer { + display: flex; + justify-content: space-between; + gap: 15px; + border-top: 1px solid var(--line); + padding: 24px 0; + color: #a7b0a5; + font: + 8px ui-monospace, + monospace; + letter-spacing: 1px; +} +.article-footer span { + letter-spacing: 0; +} +.search-dialog { + padding: 0; + border: 1px solid var(--line); + border-radius: 12px; + width: min(570px, calc(100vw - 32px)); + max-height: 80dvh; + margin: 14vh auto auto; + box-shadow: 0 24px 80px #14281b26; + color: var(--ink); +} +.search-dialog::backdrop { + background: #293b304d; + backdrop-filter: blur(3px); +} +.search-input-row { + display: flex; + align-items: center; + gap: 13px; + padding: 17px; + border-bottom: 1px solid var(--line); +} +.search-input-row > span { + font-size: 25px; + color: #879786; +} +.search-input-row input { + width: 100%; + min-width: 0; + outline: none; + font-size: 14px; +} +.search-input-row button { + font-size: 10px; + border: 1px solid var(--line); + border-radius: 4px; + padding: 3px 5px; +} +.search-results { + padding: 15px; + max-height: 50dvh; + overflow-y: auto; +} +.search-results > .eyebrow { + margin: 0 8px 12px; +} +.search-result { + display: block; + border: 1px solid transparent; + padding: 13px; + border-radius: 7px; +} +.search-result.selected, +.search-result:hover { + background: #f3f7f1; + border-color: #e3ebdf; +} +.search-result > span { + color: #92a18c; + font-size: 10px; +} +.search-result strong { + display: block; + font-size: 14px; + font-weight: 500; + margin: 5px 0; +} +.search-result strong span { + float: right; + color: #91aa85; +} +.search-result small { + font-size: 11px; + color: #69765f; +} +.empty-search { + font-size: 13px; + padding: 25px 8px; + color: #66735f; +} +.search-footer { + padding: 12px 20px; + border-top: 1px solid var(--line); + font-size: 10px; + color: #9da797; + display: flex; + justify-content: space-between; +} +.mobile-bar, +.mobile-navigation, +.mobile-toc { + display: none; +} +@media (min-width: 1450px) { + .home-main { + padding: 0 70px; + } + .home-hero { + padding: 85px 0 70px; + } + .reading-layout { + grid-template-columns: 250px minmax(0, 1fr) 235px; + } +} +@media (max-width: 1150px) { + .top-nav { + gap: 15px; + } + .reading-layout { + grid-template-columns: 220px minmax(0, 1fr); + } + .toc { + display: none; + } + .home-layout { + grid-template-columns: 220px minmax(0, 1fr); + } + .home-main { + padding: 0 30px; + } + .reading-main { + padding: 30px 40px 0; + } + .home-hero { + gap: 10px; + } + .archive-sheet { + width: 145px; + height: 190px; + left: calc(50% - 72px); + } + .hero-copy h1 { + font-size: 38px; + } + .doc-card { + padding: 16px; + } + .card-description { + font-size: 13px; + } + .top-nav a:nth-child(2) { + display: none; + } + .mobile-toc { + display: block; + margin: 18px 0; + border: 1px solid var(--line); + border-radius: 6px; + padding: 10px 15px; + font-size: 12px; + color: #7d8a7d; + } + .mobile-toc nav { + display: grid; + gap: 10px; + padding-top: 15px; + } + .mobile-toc a { + padding: 3px 0; + } + .mobile-toc .active { + color: var(--green); + } +} +@media (max-width: 850px) { + .site-header { + padding: 0 20px; + gap: 20px; + height: 62px; + } + .top-nav { + display: none; + } + .search-trigger { + margin-left: auto; + } + .home-layout, + .reading-layout { + grid-template-columns: 1fr; + } + .sidebar { + display: none; + } + .mobile-bar { + display: block; + position: sticky; + top: 62px; + z-index: 25; + background: #fff; + border-bottom: 1px solid var(--line); + } + .mobile-bar button { + display: flex; + width: 100%; + align-items: center; + gap: 10px; + padding: 12px 22px; + font-size: 11px; + color: #7b897c; + } + .mobile-bar button span:last-child { + margin-left: auto; + } + .mobile-navigation { + display: block; + padding: 15px 22px; + border-bottom: 1px solid var(--line); + max-height: 65dvh; + overflow: auto; + } + .mobile-navigation .nav-group { + margin-top: 15px; + } + .mobile-navigation .nav-note { + display: none; + } + .home-main { + padding: 0 30px; + } + .home-hero { + padding: 45px 0; + grid-template-columns: 1.2fr 1fr; + } + .hero-copy h1 { + font-size: 42px; + } + .reading-main { + max-width: 800px; + padding: 28px 35px 0; + } + .breadcrumb { + margin-bottom: 28px; + } + html { + scroll-padding-top: 120px; + } +} +@media (max-width: 560px) { + .brand { + font-size: 13px; + gap: 9px; + } + .brand-divider { + margin: 0 4px; + } + .docs-badge { + display: none; + } + .site-header { + padding: 0 18px; + gap: 12px; + } + .search-trigger { + width: 34px; + height: 32px; + justify-content: center; + padding: 5px; + } + .search-trigger span:nth-child(2), + .search-trigger kbd { + display: none; + } + .home-main { + padding: 0 22px; + } + .home-hero { + grid-template-columns: 1fr; + gap: 26px; + padding: 35px 0 30px; + } + .hero-copy h1 { + font-size: 42px; + letter-spacing: -1.5px; + margin: 23px 0 18px; + } + .hero-description { + font-size: 14px; + } + .hero-actions { + margin-top: 23px; + } + .hero-footnote { + margin-top: 24px; + } + .archive-visual { + height: 240px; + margin: 0 10px; + } + .archive-sheet { + top: 30px; + height: 180px; + } + .sheet-emblem { + font-size: 52px; + margin: 8px 0; + } + .archive-coordinate { + bottom: 0; + } + .section-heading h2, + .quickstart h2 { + font-size: 19px; + } + .doc-cards { + grid-template-columns: 1fr; + gap: 12px; + } + .doc-card { + padding: 20px; + } + .card-group { + margin-top: 14px; + } + .card-description { + font-size: 12px; + margin-bottom: 17px; + } + .doc-card h3 { + font-size: 17px; + } + .quickstart { + grid-template-columns: 1fr; + gap: 23px; + padding: 30px 0; + } + .quickstart p:not(.eyebrow) br { + display: none; + } + .quickstart p:not(.eyebrow) { + font-size: 12px; + } + .site-footer { + flex-wrap: wrap; + font-size: 9px; + gap: 12px; + } + .site-footer > span:nth-child(2) { + display: none; + } + .reading-main { + padding: 25px 22px 0; + } + .prose { + font-size: 15px; + line-height: 1.95; + } + .prose h1 { + font-size: 28px; + letter-spacing: -0.8px; + } + .prose > p:first-of-type { + font-size: 15px; + } + .prose h2 { + font-size: 20px; + } + .article-meta { + margin-bottom: 14px; + } + .pagination { + gap: 10px; + } + .pagination > a { + padding: 13px; + } + .pagination strong { + font-size: 11px; + } + .article-end { + flex-wrap: wrap; + font-size: 10px; + } + .code-block pre, + .prose .code-block pre { + font-size: 12px; + padding: 14px 11px; + } + .line-number { + width: 20px; + } + .callout { + padding: 13px; + font-size: 13px; + } + .article-footer { + font-size: 7px; + } + .eyebrow { + font-size: 9px; + } + .search-dialog { + margin-top: 8vh; + } + .search-result small { + line-height: 1.7; + display: block; + } +} +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + *, + *::before, + *::after { + transition: none !important; + } } -body { background: #fafaf8; color: #292524; } -::selection { background: #ccfbf1; } -.prose table { display: block; overflow-x: auto; } +.prose .mobile-toc { + font-size: 12px; + font-weight: 400; + line-height: 1.6; +} +.prose .mobile-toc a { + text-decoration: none; + font-weight: 400; +} +.mobile-toc summary { + padding: 4px 0; +} + +.table-scroll { + overflow-x: auto; + border: 1px solid var(--line); + border-radius: 7px; + margin: 24px 0; +} +.prose .table-scroll table { + border: 0; + border-radius: 0; +} diff --git a/design/references/analysis.md b/design/references/analysis.md new file mode 100644 index 0000000..b88ad9c --- /dev/null +++ b/design/references/analysis.md @@ -0,0 +1,20 @@ +# 文档站参考研究 · 2026-10-27 + +实际使用浏览器访问并截图;Linear 使用顶部 Toggle theme 切换浅色,Tailwind 在页脚选择 Light theme,Supabase 在右上菜单选择 Light,React 页继承同域浅色设置。截图为 1280×720,未复制品牌文案或资源。 + +| 页面 | 截图 | 观察与落地 | +| -------------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [Linear Projects](https://linear.app/docs/projects) | linear-projects-light.png | 64px 顶栏、280px 左目录、约670px 正文、右侧细线目录;纯白与浅灰边界、32px 标题和14–16px正文、节制强调。落地固定顶栏、安静三栏、青绿当前项。 | +| [Tailwind Editor setup](https://tailwindcss.com/docs/editor-setup) | tailwind-editor-light-top.png、tailwind-editor-light.png | 56px顶栏、约288px左栏、约560px正文;等宽分类标签、竖线当前项、段落间宽留白、右侧章节列表;页脚显式主题选择。落地目录分组、章节锚点、精确等宽代码与语言标签。 | +| [Supabase Docs](https://supabase.com/docs) | supabase-home-light.png | 50px横向全站导航和搜索;首页标题与命令入口左右布局,下面分区标题+多列入口,边框浅而有序。落地首页快速启动、三个档案分区、明确描述和阅读入口。 | +| [Supabase React](https://supabase.com/docs/guides/getting-started/quickstarts/reactjs) | supabase-react-light.png、supabase-react-code-light.png | 左侧约316px、中间约605px、右侧目录;面包屑和大标题引导;步骤标题、带语言与复制操作的浅色代码块、低对比提示框。落地面包屑、带复制反馈的代码、独立草案提示、上下篇。 | + +## 统一设计决定 + +- 默认浅色,系统本地字体,无 CDN。正文16px/1.9,中文阅读栏最大720px,导航13px,标题32–40px。青绿只用于品牌、链接、当前状态。 +- 首页以「理解、构建、记录」为主线,突出入门,再按开始使用/架构设计/图形档案组织现有内容;不制造未存在的产品页。 +- 全站顶部品牌返回首页,档案入口和仓库链接;本地全文检索支持 Cmd/Ctrl K、Escape、方向键,不向外部服务发送查询。 +- 宽屏三栏、平板隐藏右目录、小屏使用可展开文档菜单与页内目录;代码和表格各自滚动,正文不溢出。 +- 保持 Pages Router、pnpm、uv 与现有 CI/CD、镜像缓存、回退和18135部署入口。截图仅为研究证据,不进入站点资源。 + +响应式交互是对以上设计规律的本地实现决定;参考站的手机布局未单独测量。Tabularium 的手机截图和实测记录见 ../verification。 diff --git a/design/references/linear-projects-light.png b/design/references/linear-projects-light.png new file mode 100644 index 0000000..30e17b5 Binary files /dev/null and b/design/references/linear-projects-light.png differ diff --git a/design/references/supabase-home-light.png b/design/references/supabase-home-light.png new file mode 100644 index 0000000..5eee815 Binary files /dev/null and b/design/references/supabase-home-light.png differ diff --git a/design/references/supabase-react-code-light.png b/design/references/supabase-react-code-light.png new file mode 100644 index 0000000..4417624 Binary files /dev/null and b/design/references/supabase-react-code-light.png differ diff --git a/design/references/supabase-react-light.png b/design/references/supabase-react-light.png new file mode 100644 index 0000000..58e4428 Binary files /dev/null and b/design/references/supabase-react-light.png differ diff --git a/design/references/tailwind-editor-light-top.png b/design/references/tailwind-editor-light-top.png new file mode 100644 index 0000000..ce68fda Binary files /dev/null and b/design/references/tailwind-editor-light-top.png differ diff --git a/design/references/tailwind-editor-light.png b/design/references/tailwind-editor-light.png new file mode 100644 index 0000000..58643ff Binary files /dev/null and b/design/references/tailwind-editor-light.png differ diff --git a/design/verification/README.md b/design/verification/README.md new file mode 100644 index 0000000..653f8cc --- /dev/null +++ b/design/verification/README.md @@ -0,0 +1,28 @@ +# 浏览器验收 · 2026-10-27 + +在 `127.0.0.1:3015` 预览 Pages Router 的生产构建,使用实际浏览器操作与截图;未调用外部字体或 CDN,未修改部署架构。截图保留了改版后的首页、三篇文档、搜索及手机/平板布局。 + +## 检查结果 + +- `pnpm lint`、`pnpm typecheck`:通过。 +- `UV_CACHE_DIR=/private/tmp/tabularium-uv-cache pnpm figures`:通过,SVG 与仓库一致。默认 uv 缓存路径受本机沙箱限制,使用临时缓存完成。 +- `pnpm --filter @endor/docs run build --webpack`:通过,首页、三篇文档、404 与健康接口均成功生成。 +- 默认 `pnpm build` 的 Turbopack 在这台机器因 PostCSS 编译子进程绑定本地端口遭 `EPERM` 中断;使用 Next.js 自带 webpack 参数完成本地构建。没有修改 package.json 或 CI 的构建命令,远程仍使用原有构建流程。 +- `python3 scripts/smoke.py http://127.0.0.1:3015`:通过,覆盖健康、全部文档、生成 JS/CSS、SVG 和404。 +- 中文章节锚点、重名标题后缀、代码围栏内的伪标题、行号映射和行内代码标题:使用 Node 断言通过。 +- 实际输入「生命周期」只返回架构草案,Enter 打开对应文档;⌘K 打开搜索,Escape 关闭并将焦点恢复到搜索按钮;无结果时显示建议词。方向键保持选择索引边界。 +- 代码复制按钮显示「已复制」状态;未将系统剪贴板内容作为验收依据。 +- 手机菜单展开后可跳转并自动收起;页内目录展开后可跳转中文章节。草案标签、提示、表格和 SVG 正常呈现。 +- 390px 首页和草案页、768px 入门页实测 `documentElement.scrollWidth === innerWidth`。320px 入门页页面宽320px,长命令的代码容器宽274px、内容宽320px,仅容器内部横向滚动。 +- 检查浏览器捕获的 error/warn 日志为空。未进行跨浏览器测试;本次使用内置浏览器。 + +## 截图索引 + +- `home-desktop.png`、`home-desktop-full.png`:桌面首页和完整分区布局。 +- `home-mobile.png`、`home-mobile-full.png`:390px 手机首页和完整纵向布局。 +- `getting-started-desktop.png`、`getting-started-mobile.png`、`getting-started-tablet.png`:正文、代码块及不同尺寸的阅读布局。 +- `draft-desktop.png`、`draft-mobile.png`、`draft-mobile-table.png`:草案状态、提示块、术语表与手机锚点定位。 +- `figures-desktop.png`:本地 SVG 与文字的组合。 +- `search-desktop.png`、`search-empty.png`:正文检索和无结果状态。 + +验收过程中发现并修正:辅助文字过淡、代码字号偏小、手机搜索图标缺少无障碍名称、手机目录位于正文标题之前,以及表格标题行未铺满阅读栏。截图来自同一轮渐进验收;最终重拍了受修正影响的关键视图。 diff --git a/design/verification/draft-desktop.png b/design/verification/draft-desktop.png new file mode 100644 index 0000000..1726bd6 Binary files /dev/null and b/design/verification/draft-desktop.png differ diff --git a/design/verification/draft-mobile-table.png b/design/verification/draft-mobile-table.png new file mode 100644 index 0000000..a28d106 Binary files /dev/null and b/design/verification/draft-mobile-table.png differ diff --git a/design/verification/draft-mobile.png b/design/verification/draft-mobile.png new file mode 100644 index 0000000..5ec4b41 Binary files /dev/null and b/design/verification/draft-mobile.png differ diff --git a/design/verification/figures-desktop.png b/design/verification/figures-desktop.png new file mode 100644 index 0000000..402be43 Binary files /dev/null and b/design/verification/figures-desktop.png differ diff --git a/design/verification/getting-started-desktop.png b/design/verification/getting-started-desktop.png new file mode 100644 index 0000000..d02ec75 Binary files /dev/null and b/design/verification/getting-started-desktop.png differ diff --git a/design/verification/getting-started-mobile.png b/design/verification/getting-started-mobile.png new file mode 100644 index 0000000..1ab1276 Binary files /dev/null and b/design/verification/getting-started-mobile.png differ diff --git a/design/verification/getting-started-tablet.png b/design/verification/getting-started-tablet.png new file mode 100644 index 0000000..5526b33 Binary files /dev/null and b/design/verification/getting-started-tablet.png differ diff --git a/design/verification/home-desktop-full.png b/design/verification/home-desktop-full.png new file mode 100644 index 0000000..e0dd61f Binary files /dev/null and b/design/verification/home-desktop-full.png differ diff --git a/design/verification/home-desktop.png b/design/verification/home-desktop.png new file mode 100644 index 0000000..c1ba063 Binary files /dev/null and b/design/verification/home-desktop.png differ diff --git a/design/verification/home-mobile-full.png b/design/verification/home-mobile-full.png new file mode 100644 index 0000000..6fb59ac Binary files /dev/null and b/design/verification/home-mobile-full.png differ diff --git a/design/verification/home-mobile.png b/design/verification/home-mobile.png new file mode 100644 index 0000000..3728357 Binary files /dev/null and b/design/verification/home-mobile.png differ diff --git a/design/verification/search-desktop.png b/design/verification/search-desktop.png new file mode 100644 index 0000000..442287c Binary files /dev/null and b/design/verification/search-desktop.png differ diff --git a/design/verification/search-empty.png b/design/verification/search-empty.png new file mode 100644 index 0000000..aa8af30 Binary files /dev/null and b/design/verification/search-empty.png differ