From 06b3b94493ab04722e065ae76c7726d80547c414 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 19 Mar 2026 01:50:19 -0500 Subject: [PATCH] colors + search for docs (#927) * colors * search --- docs/src/app/api/search/route.ts | 69 ++++++++ docs/src/app/globals.css | 20 +++ docs/src/components/code-block.tsx | 159 ++++++++++++++++- docs/src/components/docs-chat.tsx | 12 +- docs/src/components/header.tsx | 4 +- docs/src/components/search.tsx | 265 +++++++++++++++++++++++++++++ docs/src/components/ui/dialog.tsx | 92 ++++++++++ docs/src/components/ui/sheet.tsx | 2 +- docs/src/lib/search-index.ts | 66 +++++++ 9 files changed, 678 insertions(+), 11 deletions(-) create mode 100644 docs/src/app/api/search/route.ts create mode 100644 docs/src/components/search.tsx create mode 100644 docs/src/components/ui/dialog.tsx create mode 100644 docs/src/lib/search-index.ts diff --git a/docs/src/app/api/search/route.ts b/docs/src/app/api/search/route.ts new file mode 100644 index 0000000..5125db7 --- /dev/null +++ b/docs/src/app/api/search/route.ts @@ -0,0 +1,69 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getSearchIndex } from "@/lib/search-index"; + +export async function GET(req: NextRequest) { + const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase(); + + if (!q) { + return NextResponse.json({ results: [] }); + } + + const index = await getSearchIndex(); + const terms = q.split(/\s+/).filter(Boolean); + + const results = index + .map((entry) => { + const titleLower = entry.title.toLowerCase(); + const contentLower = entry.content.toLowerCase(); + + const titleMatch = terms.every((t) => titleLower.includes(t)); + const contentMatch = terms.every((t) => contentLower.includes(t)); + + if (!titleMatch && !contentMatch) return null; + + let snippet = ""; + if (contentMatch) { + const firstTermIdx = Math.min( + ...terms.map((t) => { + const idx = contentLower.indexOf(t); + return idx === -1 ? Infinity : idx; + }), + ); + if (firstTermIdx !== Infinity) { + const start = Math.max(0, firstTermIdx - 40); + const end = Math.min(entry.content.length, firstTermIdx + 120); + snippet = + (start > 0 ? "..." : "") + + entry.content.slice(start, end).replace(/\n/g, " ") + + (end < entry.content.length ? "..." : ""); + } + } + + return { + title: entry.title, + href: entry.href, + section: entry.section, + snippet, + score: titleMatch ? 2 : 1, + }; + }) + .filter( + ( + r, + ): r is { + title: string; + href: string; + section: string; + snippet: string; + score: number; + } => r !== null, + ) + .sort((a, b) => b.score - a.score) + .slice(0, 20) + .map(({ score: _, ...rest }) => rest); + + return NextResponse.json( + { results }, + { headers: { "Cache-Control": "public, max-age=60" } }, + ); +} diff --git a/docs/src/app/globals.css b/docs/src/app/globals.css index 04c72fb..843d35f 100644 --- a/docs/src/app/globals.css +++ b/docs/src/app/globals.css @@ -16,6 +16,7 @@ --color-muted-foreground: var(--muted-foreground); --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); + --color-sidebar: var(--sidebar); } :root { @@ -26,6 +27,7 @@ --muted-foreground: #737373; --primary: #171717; --primary-foreground: #fff; + --sidebar: #f5f5f5; } .dark { @@ -36,6 +38,7 @@ --muted-foreground: #a3a3a3; --primary: #f5f5f5; --primary-foreground: #0a0a0a; + --sidebar: #171717; } html { @@ -129,6 +132,23 @@ pre:not(.shiki) { font-size: 0.875em; } +/* Diff line highlighting */ +.diff-add { + color: #00952d; +} + +.diff-remove { + color: #f32e40; +} + +.dark .diff-add { + color: #00ca50; +} + +.dark .diff-remove { + color: #f32e40; +} + /* Shiki dual theme support */ .shiki, .shiki span { diff --git a/docs/src/components/code-block.tsx b/docs/src/components/code-block.tsx index a0c888a..9817d70 100644 --- a/docs/src/components/code-block.tsx +++ b/docs/src/components/code-block.tsx @@ -1,6 +1,156 @@ import { codeToHtml } from "shiki"; import { CopyButton } from "./copy-button"; +const vercelDarkTheme = { + name: "vercel-dark", + type: "dark" as const, + colors: { + "editor.background": "transparent", + "editor.foreground": "#EDEDED", + }, + settings: [ + { + scope: ["comment", "punctuation.definition.comment"], + settings: { foreground: "#A1A1A1" }, + }, + { + scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"], + settings: { foreground: "#00CA50" }, + }, + { + scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"], + settings: { foreground: "#47A8FF" }, + }, + { + scope: ["keyword", "storage.type", "storage.modifier"], + settings: { foreground: "#FF4D8D" }, + }, + { + scope: ["keyword.operator", "keyword.control"], + settings: { foreground: "#FF4D8D" }, + }, + { + scope: ["entity.name.function", "support.function", "meta.function-call"], + settings: { foreground: "#C472FB" }, + }, + { + scope: ["variable", "variable.other"], + settings: { foreground: "#EDEDED" }, + }, + { + scope: ["variable.parameter"], + settings: { foreground: "#FF9300" }, + }, + { + scope: ["entity.name.tag", "support.class.component", "entity.name.type"], + settings: { foreground: "#FF4D8D" }, + }, + { + scope: ["punctuation", "meta.brace", "meta.bracket"], + settings: { foreground: "#EDEDED" }, + }, + { + scope: [ + "support.type.property-name", + "entity.name.tag.json", + "meta.object-literal.key", + "punctuation.support.type.property-name", + ], + settings: { foreground: "#FF4D8D" }, + }, + { + scope: ["entity.other.attribute-name"], + settings: { foreground: "#00CA50" }, + }, + { + scope: ["support.type.primitive", "entity.name.type.primitive"], + settings: { foreground: "#00CA50" }, + }, + ], +}; + +const vercelLightTheme = { + name: "vercel-light", + type: "light" as const, + colors: { + "editor.background": "transparent", + "editor.foreground": "#171717", + }, + settings: [ + { + scope: ["comment", "punctuation.definition.comment"], + settings: { foreground: "#6B7280" }, + }, + { + scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"], + settings: { foreground: "#067A6E" }, + }, + { + scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"], + settings: { foreground: "#0070C0" }, + }, + { + scope: ["keyword", "storage.type", "storage.modifier"], + settings: { foreground: "#D6409F" }, + }, + { + scope: ["keyword.operator", "keyword.control"], + settings: { foreground: "#D6409F" }, + }, + { + scope: ["entity.name.function", "support.function", "meta.function-call"], + settings: { foreground: "#6E56CF" }, + }, + { + scope: ["variable", "variable.other"], + settings: { foreground: "#171717" }, + }, + { + scope: ["variable.parameter"], + settings: { foreground: "#B45309" }, + }, + { + scope: ["entity.name.tag", "support.class.component", "entity.name.type"], + settings: { foreground: "#D6409F" }, + }, + { + scope: ["punctuation", "meta.brace", "meta.bracket"], + settings: { foreground: "#6B7280" }, + }, + { + scope: [ + "support.type.property-name", + "entity.name.tag.json", + "meta.object-literal.key", + "punctuation.support.type.property-name", + ], + settings: { foreground: "#D6409F" }, + }, + { + scope: ["entity.other.attribute-name"], + settings: { foreground: "#067A6E" }, + }, + { + scope: ["support.type.primitive", "entity.name.type.primitive"], + settings: { foreground: "#067A6E" }, + }, + ], +}; + +const PLACEHOLDER_PREFIX = "\u200B\u200B"; +const PLACEHOLDER_SUFFIX = "\u200B\u200B"; + +function shieldPlaceholders(code: string): string { + return code.replace(/<([\w|]+)>/g, `${PLACEHOLDER_PREFIX}$1${PLACEHOLDER_SUFFIX}`); +} + +function restorePlaceholders(html: string): string { + return html.replace( + new RegExp(`${PLACEHOLDER_PREFIX}([\\w|]+)${PLACEHOLDER_SUFFIX}`, "g"), + "<$1>", + ); +} + interface CodeBlockProps { code: string; lang?: string; @@ -8,13 +158,16 @@ interface CodeBlockProps { export async function CodeBlock({ code, lang = "bash" }: CodeBlockProps) { const trimmedCode = code.trim(); - const html = await codeToHtml(trimmedCode, { + const shielded = shieldPlaceholders(trimmedCode); + let html = await codeToHtml(shielded, { lang, themes: { - light: "github-light-default", - dark: "github-dark-default", + light: vercelLightTheme, + dark: vercelDarkTheme, }, + defaultColor: false, }); + html = restorePlaceholders(html); return (
diff --git a/docs/src/components/docs-chat.tsx b/docs/src/components/docs-chat.tsx index 4332d36..62e28d9 100644 --- a/docs/src/components/docs-chat.tsx +++ b/docs/src/components/docs-chat.tsx @@ -261,7 +261,7 @@ export function DocsChat({ // Cmd+K to open sidebar and focus prompt, Escape to close useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + if (e.key === "i" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setOpen((prev) => { if (!prev) { @@ -327,7 +327,7 @@ export function DocsChat({ const chatPanel = ( <> {/* Header */} -
+
agent-browser Docs
{showMessages && ( @@ -443,7 +443,7 @@ export function DocsChat({ {/* Input bar */}