From 022ed7568275512ed699baf6aa08fcef562c1e99 Mon Sep 17 00:00:00 2001 From: "hezheng.lsw" Date: Fri, 3 Jul 2026 11:45:33 +0800 Subject: [PATCH] feat(pages): add Docs page with search, markdown rendering, and i18n support (#273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(pages): add Docs page with search, markdown rendering, and i18n support - Add DocsPage with full-text search modal (⌘K trigger) - Add MarkdownRenderer with DOMPurify sanitization - Add bilingual docs content (en/zh) for all sections - Add shared headingId utility for consistent TOC anchors - Add search keyboard hints with i18n support - Update Navbar with Docs navigation link - Add icon-search.svg asset - Configure webpack for markdown imports * fix(pages): address PR #273 code review feedback - Replace marked.setOptions() with new Marked instance (no global mutation) - Escape heading ID attribute value to prevent XSS - Use crypto.randomUUID() for mermaid diagram IDs (no collisions) - Add cancellation flag for async mermaid renders on unmount - Move inline
 styles to CSS class (only dynamic align-items inline)
- Move @types/dompurify to devDependencies
- Remove @ts-nocheck from docs/index.ts
- Extract getRawContent helper to reduce duplication
- Fix searchDocs fallback consistency (add enDocs fallback)
- Fix heading ID mismatch by stripping markdown links before ID generation
- Separate sidebar chevron (expand) from label (navigate)
- Guard ⌘K shortcut against input/textarea focus interception
---
 pages/index.html                              |    2 +-
 pages/package.json                            |    4 +
 pages/src/App.tsx                             |    2 +-
 pages/src/assets/icons/icon-search.svg        |    1 +
 pages/src/components/MarkdownRenderer.tsx     |  187 +++
 pages/src/components/Navbar.tsx               |    2 +-
 pages/src/content/docs/en/architecture.md     |  374 +++++
 pages/src/content/docs/en/cli-reference.md    |  396 +++++
 pages/src/content/docs/en/configuration.md    |  283 ++++
 pages/src/content/docs/en/contributing.md     |  220 +++
 pages/src/content/docs/en/faq.md              |  329 +++++
 pages/src/content/docs/en/installation.md     |  183 +++
 pages/src/content/docs/en/integrations.md     |   66 +
 .../docs/en/integrations/agent-skill.md       |  119 ++
 pages/src/content/docs/en/integrations/ci.md  |  459 ++++++
 .../docs/en/integrations/claude-code.md       |  119 ++
 .../docs/en/integrations/subprocess.md        |  171 +++
 pages/src/content/docs/en/overview.md         |  139 ++
 pages/src/content/docs/en/quickstart.md       |  242 +++
 pages/src/content/docs/en/review-rules.md     |  261 ++++
 pages/src/content/docs/en/telemetry.md        |  281 ++++
 pages/src/content/docs/en/tools.md            |  389 +++++
 pages/src/content/docs/en/viewer.md           |  182 +++
 pages/src/content/docs/index.ts               |  177 +++
 pages/src/content/docs/md.d.ts                |    4 +
 pages/src/content/docs/zh/architecture.md     |  328 +++++
 pages/src/content/docs/zh/cli-reference.md    |  376 +++++
 pages/src/content/docs/zh/configuration.md    |  272 ++++
 pages/src/content/docs/zh/contributing.md     |  201 +++
 pages/src/content/docs/zh/faq.md              |  283 ++++
 pages/src/content/docs/zh/installation.md     |  177 +++
 pages/src/content/docs/zh/integrations.md     |   56 +
 .../docs/zh/integrations/agent-skill.md       |  102 ++
 pages/src/content/docs/zh/integrations/ci.md  |  406 +++++
 .../docs/zh/integrations/claude-code.md       |  102 ++
 .../docs/zh/integrations/subprocess.md        |  164 +++
 pages/src/content/docs/zh/overview.md         |  125 ++
 pages/src/content/docs/zh/quickstart.md       |  235 +++
 pages/src/content/docs/zh/review-rules.md     |  241 +++
 pages/src/content/docs/zh/telemetry.md        |  258 ++++
 pages/src/content/docs/zh/tools.md            |  353 +++++
 pages/src/content/docs/zh/viewer.md           |  151 ++
 pages/src/i18n/en.ts                          |   30 +-
 pages/src/i18n/ja.ts                          |   30 +-
 pages/src/i18n/zh.ts                          |   30 +-
 pages/src/pages/DocsPage.tsx                  | 1302 ++++++++---------
 pages/src/styles/docs-markdown.css            |  242 +++
 pages/src/utils/headingId.ts                  |   10 +
 pages/webpack.config.js                       |    4 +
 49 files changed, 9402 insertions(+), 668 deletions(-)
 create mode 100644 pages/src/assets/icons/icon-search.svg
 create mode 100644 pages/src/components/MarkdownRenderer.tsx
 create mode 100644 pages/src/content/docs/en/architecture.md
 create mode 100644 pages/src/content/docs/en/cli-reference.md
 create mode 100644 pages/src/content/docs/en/configuration.md
 create mode 100644 pages/src/content/docs/en/contributing.md
 create mode 100644 pages/src/content/docs/en/faq.md
 create mode 100644 pages/src/content/docs/en/installation.md
 create mode 100644 pages/src/content/docs/en/integrations.md
 create mode 100644 pages/src/content/docs/en/integrations/agent-skill.md
 create mode 100644 pages/src/content/docs/en/integrations/ci.md
 create mode 100644 pages/src/content/docs/en/integrations/claude-code.md
 create mode 100644 pages/src/content/docs/en/integrations/subprocess.md
 create mode 100644 pages/src/content/docs/en/overview.md
 create mode 100644 pages/src/content/docs/en/quickstart.md
 create mode 100644 pages/src/content/docs/en/review-rules.md
 create mode 100644 pages/src/content/docs/en/telemetry.md
 create mode 100644 pages/src/content/docs/en/tools.md
 create mode 100644 pages/src/content/docs/en/viewer.md
 create mode 100644 pages/src/content/docs/index.ts
 create mode 100644 pages/src/content/docs/md.d.ts
 create mode 100644 pages/src/content/docs/zh/architecture.md
 create mode 100644 pages/src/content/docs/zh/cli-reference.md
 create mode 100644 pages/src/content/docs/zh/configuration.md
 create mode 100644 pages/src/content/docs/zh/contributing.md
 create mode 100644 pages/src/content/docs/zh/faq.md
 create mode 100644 pages/src/content/docs/zh/installation.md
 create mode 100644 pages/src/content/docs/zh/integrations.md
 create mode 100644 pages/src/content/docs/zh/integrations/agent-skill.md
 create mode 100644 pages/src/content/docs/zh/integrations/ci.md
 create mode 100644 pages/src/content/docs/zh/integrations/claude-code.md
 create mode 100644 pages/src/content/docs/zh/integrations/subprocess.md
 create mode 100644 pages/src/content/docs/zh/overview.md
 create mode 100644 pages/src/content/docs/zh/quickstart.md
 create mode 100644 pages/src/content/docs/zh/review-rules.md
 create mode 100644 pages/src/content/docs/zh/telemetry.md
 create mode 100644 pages/src/content/docs/zh/tools.md
 create mode 100644 pages/src/content/docs/zh/viewer.md
 create mode 100644 pages/src/styles/docs-markdown.css
 create mode 100644 pages/src/utils/headingId.ts

diff --git a/pages/index.html b/pages/index.html
index c01e8c3..757f6d7 100644
--- a/pages/index.html
+++ b/pages/index.html
@@ -3,7 +3,7 @@
 
   
   
-  Open Code Review — AI Code Review
+  Open Code Review — Agent Native Code Review
   
   
   
diff --git a/pages/package.json b/pages/package.json
index c2ce253..66acb5e 100644
--- a/pages/package.json
+++ b/pages/package.json
@@ -9,6 +9,9 @@
   },
   "dependencies": {
     "@agentscope-ai/icons": "^1.0.68",
+    "dompurify": "^3.4.11",
+    "marked": "^18.0.5",
+    "mermaid": "^11.16.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-router-dom": "^6.8.0",
@@ -22,6 +25,7 @@
     "@babel/preset-env": "^7.29.5",
     "@babel/preset-react": "^7.23.5",
     "@babel/preset-typescript": "^7.23.3",
+    "@types/dompurify": "^3.0.5",
     "@types/react": "^18.2.0",
     "@types/react-dom": "^18.2.0",
     "@types/three": "^0.185.0",
diff --git a/pages/src/App.tsx b/pages/src/App.tsx
index 73de908..ffe893e 100644
--- a/pages/src/App.tsx
+++ b/pages/src/App.tsx
@@ -22,7 +22,7 @@ const App: React.FC = () => {
         } />
         } />
         } />
-        } />
+        } />
       
     
   );
diff --git a/pages/src/assets/icons/icon-search.svg b/pages/src/assets/icons/icon-search.svg
new file mode 100644
index 0000000..3e5f898
--- /dev/null
+++ b/pages/src/assets/icons/icon-search.svg
@@ -0,0 +1 @@
+
diff --git a/pages/src/components/MarkdownRenderer.tsx b/pages/src/components/MarkdownRenderer.tsx
new file mode 100644
index 0000000..c3d6899
--- /dev/null
+++ b/pages/src/components/MarkdownRenderer.tsx
@@ -0,0 +1,187 @@
+import React, { useMemo, useEffect, useRef, useState, useCallback, useId } from 'react';
+import ReactDOM from 'react-dom';
+import { Marked, Renderer } from 'marked';
+import DOMPurify from 'dompurify';
+import mermaid from 'mermaid';
+import { useTranslation } from '../i18n';
+import copyIcon from '../assets/icons/icon-copy.svg';
+import { generateHeadingId } from '../utils/headingId';
+
+// Initialize mermaid with dark theme
+mermaid.initialize({
+  startOnLoad: false,
+  theme: 'dark',
+  themeVariables: {
+    primaryColor: '#1a1a2e',
+    primaryTextColor: 'rgba(255,255,255,0.85)',
+    primaryBorderColor: 'rgba(255,255,255,0.2)',
+    lineColor: 'rgba(255,255,255,0.4)',
+    secondaryColor: '#16213e',
+    tertiaryColor: '#0f3460',
+    background: '#000000',
+    mainBkg: 'rgba(255,255,255,0.04)',
+    nodeBorder: 'rgba(255,255,255,0.16)',
+    clusterBkg: 'rgba(255,255,255,0.02)',
+    titleColor: '#FFFFFF',
+    edgeLabelBackground: '#000000',
+  },
+  flowchart: {
+    htmlLabels: true,
+    curve: 'basis',
+  },
+});
+
+interface MarkdownRendererProps {
+  content: string;
+}
+
+/**
+ * Renders markdown content with dark theme styling matching the existing DocsPage design.
+ * Uses `marked` to parse markdown into HTML, then renders with styled container.
+ * Mermaid diagrams are rendered client-side after mount.
+ */
+const MarkdownRenderer: React.FC = ({ content }) => {
+  const containerRef = useRef(null);
+  const { t } = useTranslation();
+  const [toastVisible, setToastVisible] = useState(false);
+
+  const handleCopy = useCallback((text: string) => {
+    if (navigator.clipboard && window.isSecureContext) {
+      navigator.clipboard.writeText(text).then(() => {
+        setToastVisible(true);
+      }).catch(() => fallbackCopy(text));
+    } else {
+      fallbackCopy(text);
+    }
+  }, []);
+
+  const fallbackCopy = (text: string) => {
+    const textarea = document.createElement('textarea');
+    textarea.value = text;
+    textarea.style.position = 'fixed';
+    textarea.style.opacity = '0';
+    document.body.appendChild(textarea);
+    textarea.select();
+    const success = document.execCommand('copy');
+    document.body.removeChild(textarea);
+    if (success) setToastVisible(true);
+  };
+
+  useEffect(() => {
+    if (!toastVisible) return;
+    const timer = setTimeout(() => setToastVisible(false), 1200);
+    return () => clearTimeout(timer);
+  }, [toastVisible]);
+
+  const html = useMemo(() => {
+    // Custom renderer to generate heading IDs matching the TOC extraction logic
+    const renderer = new Renderer();
+    renderer.heading = function ({ text, depth }: { text: string; depth: number }) {
+      const id = generateHeadingId(text);
+      // Escape id attribute value to prevent XSS
+      const safeId = id.replace(/"/g, '"');
+      return `${text}\n`;
+    };
+    // Strip trailing newlines from code blocks to avoid empty line at bottom
+    renderer.code = function ({ text, lang, escaped }: { text: string; lang?: string; escaped?: boolean }) {
+      const trimmed = text.replace(/^\n+|\n+$/g, '');
+      const content = escaped ? trimmed : trimmed.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+      const langClass = lang ? ` class="language-${lang}"` : '';
+      const isMultiline = trimmed.includes('\n');
+      const alignItems = isMultiline ? 'flex-start' : 'center';
+      return `
${content}
\n`; + }; + const instance = new Marked({ gfm: true, breaks: false, renderer }); + return DOMPurify.sanitize(instance.parse(content) as string); + }, [content]); + + // Render mermaid diagrams and add copy buttons to code blocks after DOM update + useEffect(() => { + if (!containerRef.current) return; + let cancelled = false; + + // Add copy buttons to all pre > code blocks (except mermaid) + const preBlocks = containerRef.current.querySelectorAll('pre'); + preBlocks.forEach((pre) => { + const codeEl = pre.querySelector('code'); + if (!codeEl || codeEl.classList.contains('language-mermaid')) return; + if (pre.querySelector('.code-copy-btn')) return; // already added + + // Create copy button matching reference HTML + const btn = document.createElement('div'); + btn.className = 'code-copy-btn'; + btn.style.cssText = 'display:flex;flex-shrink:0;justify-content:flex-start;align-items:flex-start;flex-direction:column;padding-top:4px;padding-bottom:4px;cursor:pointer;'; + btn.innerHTML = `copy`; + btn.addEventListener('click', () => { + const text = codeEl.textContent || ''; + handleCopy(text); + }); + pre.appendChild(btn); + }); + + // Render mermaid diagrams + const mermaidBlocks = containerRef.current.querySelectorAll('code.language-mermaid'); + if (mermaidBlocks.length === 0) return; + + const renderPromises = Array.from(mermaidBlocks).map(async (block) => { + const pre = block.parentElement; + if (!pre) return; + const code = block.textContent || ''; + try { + const id = `mermaid-diagram-${crypto.randomUUID()}`; + const { svg } = await mermaid.render(id, code); + if (cancelled) return; + // Replace the
 with rendered SVG
+        const wrapper = document.createElement('div');
+        wrapper.className = 'mermaid-rendered';
+        wrapper.innerHTML = svg;
+        pre.replaceWith(wrapper);
+      } catch (e) {
+        if (cancelled) return;
+        // If rendering fails, show the code block normally
+        (block as HTMLElement).style.display = 'block';
+        console.warn('[Mermaid] render failed:', e);
+      }
+    });
+
+    return () => { cancelled = true; };
+  }, [html]);
+
+  return (
+    <>
+      
+ {ReactDOM.createPortal( +
+ {t('quickstart.copied')} +
, + document.body + )} + + ); +}; + +export default MarkdownRenderer; diff --git a/pages/src/components/Navbar.tsx b/pages/src/components/Navbar.tsx index 4488d75..76ac72e 100644 --- a/pages/src/components/Navbar.tsx +++ b/pages/src/components/Navbar.tsx @@ -187,7 +187,7 @@ const Navbar: React.FC = () => { rel="noopener noreferrer" style={{ display: 'flex', alignItems: 'center', opacity: 0.6 }} > - Social + Social - {/* ─── Overview ─── */} -
-

{t('docs.overviewTitle')}

-

- Open Code Review {t('docs.overviewDesc').replace(/<\/?code>/g, '')} -

-

- {t('docs.overviewFeatures')} -

-
- - {'✔\n✔\n✔\n✔\n✔\n✔'.split('\n').map((c, i) => {c}
)} -
- - {t('docs.overviewFeat1')}
- {t('docs.overviewFeat2')}
- {t('docs.overviewFeat3')}
- {t('docs.overviewFeat4')}
- {t('docs.overviewFeat5')}
- {t('docs.overviewFeat6')} -
-
-
- - {/* ─── Install ─── */} -
-

{t('docs.installTitle')}

- {/* Install item */} -
-
- - {t('docs.installLabel')} + {sidebarTree.map((group, gi) => ( +
+ {/* Group header */} +
+ + {t(group.groupLabelKey)} +
- handleCopy('npm i -g @alibaba-group/open-code-review')} /> -
- {/* Verify item */} -
-
- - {t('docs.installVerifyLabel')} -
- handleCopy('ocr version')} /> -
-
- - {/* ─── Configuration & Verification ─── */} -
-

{t('docs.configTitle')}

-

{t('docs.configDesc').replace(/<\/?code>/g, '')}

- -

{t('docs.configInteractive')}

-

{t('docs.configInteractiveDesc')}

- handleCopy('ocr config provider')} /> - -

{t('docs.configModelSelect')}

-

{t('docs.configModelSelectDesc')}

- handleCopy('ocr config model')} /> - -

{t('docs.configListProviders')}

-

{t('docs.configListProvidersDesc')}

- handleCopy('ocr llm providers')} /> - -

{t('docs.configManual')}

-

{t('docs.configCommand')}

- '} /> - -

{t('docs.configExample')}

- handleCopy(`ocr config set llm.url https://api.anthropic.com \\\n && ocr config set llm.auth_token {{your-api-key}} \\\n && ocr config set llm.model claude-opus-4-6 \\\n && ocr config set llm.use_anthropic true \\\n && ocr config set language Chinese`)} - /> - -

{t('docs.configKeys')}

-
- {/* 2-column grid of config keys */} - {[ - [{ key: 'llm.url', desc: t('docs.configKeyUrl') }, { key: 'llm.auth_token', desc: t('docs.configKeyToken') }], - [{ key: 'llm.model', desc: t('docs.configKeyModel') }, { key: 'llm.use_anthropic', desc: t('docs.configKeyAnthropic') }], - [{ key: 'telemetry.enabled', desc: t('docs.configKeyTelemetry') }, { key: 'language', desc: t('docs.configKeyLanguage') }], - [{ key: 'llm.extra_body', desc: t('docs.configKeyExtraBody') }], - ].map((row, ri) => ( -
- {row.map(({ key, desc: d }) => ( -
-

- {key} - {d} -

+ {/* Group items */} + {group.items.map((item) => { + const isActive = item.slug === activeSlug; + const hasChildren = item.children && item.children.length > 0; + const isExpanded = expandedItems[item.id] ?? false; + return ( + +
{ + navigateToDoc(item.slug); + }} + style={{ + height: 36, + display: 'flex', + alignItems: 'center', + gap: 8, + borderRadius: 6, + padding: '10px 12px', + cursor: 'pointer', + transition: 'background 0.15s', + background: isActive ? 'rgba(43, 222, 94, 0.12)' : 'transparent', + }} + > +
+ + {t(item.labelKey)} + +
+ {hasChildren && ( +
{ + e.stopPropagation(); + toggleExpand(item.id); + }} + style={{ display: 'flex', alignItems: 'center', padding: 2 }} + > + +
+ )}
- ))} -
- ))} + {/* Children (sub-items) */} + {hasChildren && isExpanded && item.children!.map((child) => { + const childActive = child.slug === activeSlug; + return ( +
navigateToDoc(child.slug)} + style={{ + height: 36, + display: 'flex', + alignItems: 'center', + gap: 8, + borderRadius: 6, + padding: '10px 12px 10px 28px', + cursor: 'pointer', + background: childActive ? 'rgba(43, 222, 94, 0.12)' : 'transparent', + }} + > +
+ + {t(child.labelKey)} + +
+
+ ); + })} + + ); + })}
+ ))} + + )} -

{t('docs.configVerify')}

- handleCopy('ocr llm test')} - /> -

{t('docs.configVerifyDesc')}

-
+ {/* ─── Main content area ─── */} +
+ {/* Doc title */} +

+ {docTitle} +

+ {/* Rendered markdown content */} + - {/* ─── ocr review ─── */} -
-

{t('docs.reviewTitle')}

-

{t('docs.reviewDesc').replace(/<\/?code>/g, '')}

- -

{t('docs.reviewModes')}

- {/* Workspace Mode */} -
-
- -
- {t('docs.reviewWorkspace')} -

{t('docs.reviewWorkspaceDesc')}

-
-
- handleCopy('ocr review')} /> -
- {/* Branch Diff Mode */} -
-
- -
- {t('docs.reviewBranch')} -

{t('docs.reviewBranchDesc')}

-
-
- handleCopy('ocr review --from master --to dev-ref')} /> -
- {/* Single Commit Mode */} -
-
- -
- {t('docs.reviewCommit')} -

{t('docs.reviewCommitDesc')}

-
-
- handleCopy('ocr review -c abc123')} /> -
- -

{t('docs.reviewAdvanced')}

- {/* Review with Requirement Context */} -
-
- -
- {t('docs.reviewBackground')} -

{t('docs.reviewBackgroundDesc')}

-
-
- handleCopy('ocr review --background "requirement context"')} /> -
- {/* JSON Output */} -
-
- -
- {t('docs.reviewJson')} -

{t('docs.reviewJsonDesc')}

-
-
- handleCopy('ocr review --format json')} /> -
- {/* Agent Mode */} -
-
- -
- {t('docs.reviewAgent')} -

{t('docs.reviewAgentDesc')}

-
-
- handleCopy('ocr review --audience agent')} /> -
- {/* Dry-Run Preview */} -
-
- -
- {t('docs.reviewPreviewLabel')} -

{t('docs.reviewPreviewDesc')}

-
-
- handleCopy('ocr review --preview')} /> -
- -

{t('docs.reviewFlags')}

- {/* Flags table */} -
- {/* Header */} -
-
{t('docs.reviewFlagCol1')}
-
{t('docs.reviewFlagCol2')}
-
{t('docs.reviewFlagCol3')}
-
- {/* Rows */} - {[ - ['-c, --commit', t('docs.reviewFlag1Desc'), '—'], - ['--from', t('docs.reviewFlag2Desc'), '—'], - ['--to', t('docs.reviewFlag3Desc'), '—'], - ['-f, --format', t('docs.reviewFlag4Desc'), 'text'], - ['--repo', t('docs.reviewFlag5Desc'), t('docs.reviewFlag5Default')], - ['--rule', t('docs.reviewFlag6Desc'), t('docs.reviewFlag6Default')], - ['--concurrency', t('docs.reviewFlag7Desc'), '8'], - ['--timeout', t('docs.reviewFlag8Desc'), '10'], - ['--audience', t('docs.reviewFlag9Desc'), 'human'], - ['--max-tools', t('docs.reviewFlag10Desc'), t('docs.reviewFlag10Default')], - ].map(([flag, d, def], idx, arr) => ( -
-
- {flag} -
-
- {d} -
-
- {def} -
-
- ))} -
-

- {t('docs.reviewNote').replace(/<\/?code>/g, '')} -

-
- - {/* ─── ocr scan ─── */} -
-

{t('docs.scanTitle')}

-

{t('docs.scanDesc').replace(/<\/?code>/g, '')}

- -

{t('docs.scanVsTitle')}

-
-
- -
- {t('docs.scanVsReviewLabel')} -

{t('docs.scanVsReview').replace(/<\/?code>/g, '')}

-
-
-
-
-
- -
- {t('docs.scanVsScanLabel')} -

{t('docs.scanVsScan').replace(/<\/?code>/g, '')}

-
-
-
- -

{t('docs.scanUsage')}

-
-
- -
- {t('docs.scanUsageWhole')} -

{t('docs.scanUsageWholeDesc')}

-
-
- handleCopy('ocr scan')} /> -
-
-
- -
- {t('docs.scanUsagePath')} -

{t('docs.scanUsagePathDesc')}

-
-
- handleCopy('ocr scan --path internal/agent')} /> -
-
-
- -
- {t('docs.scanUsageFile')} -

{t('docs.scanUsageFileDesc')}

-
-
- handleCopy('ocr scan --path internal/agent/agent.go,internal/diff/scan.go')} /> -
-
-
- -
- {t('docs.scanUsagePreviewLabel')} -

{t('docs.scanUsagePreviewDesc')}

-
-
- handleCopy('ocr scan --preview')} /> -
- -

{t('docs.scanBatching')}

-

{t('docs.scanBatchingDesc').replace(/<\/?code>/g, '')}

-
- {[ - [t('docs.scanBatchingNone'), t('docs.scanBatchingNoneDesc')], - [t('docs.scanBatchingLang'), t('docs.scanBatchingLangDesc')], - [t('docs.scanBatchingDir'), t('docs.scanBatchingDirDesc')], - ].map(([name, d]) => ( -
-

- {name} - {d} -

-
- ))} -
- handleCopy('ocr scan --batch by-directory')} /> - -

{t('docs.scanToggles')}

-

{t('docs.scanTogglesDesc')}

-
- {[ - ['--no-plan', t('docs.scanTogglesPlanDesc')], - ['--no-dedup', t('docs.scanTogglesDedupDesc')], - ['--no-summary', t('docs.scanTogglesSummaryDesc')], - ].map(([flag, d]) => ( -
- {flag} - {d} -
- ))} -
- handleCopy('ocr scan --no-plan --no-dedup --no-summary')} /> - -

{t('docs.scanBudget')}

-

{t('docs.scanBudgetDesc').replace(/<\/?code>/g, '')}

- handleCopy('ocr scan --max-tokens-budget 500000')} /> - -

{t('docs.scanFlags')}

-
-
-
{t('docs.scanFlagCol1')}
-
{t('docs.scanFlagCol2')}
-
{t('docs.scanFlagCol3')}
-
- {[ - ['--path', t('docs.scanFlag1Desc'), t('docs.scanFlag1Default')], - ['--exclude', t('docs.scanFlag2Desc'), '—'], - ['-p, --preview', t('docs.scanFlag3Desc'), 'false'], - ['--max-tokens-budget', t('docs.scanFlag4Desc'), '0'], - ['--no-plan', t('docs.scanFlag5Desc'), 'false'], - ['--no-dedup', t('docs.scanFlag6Desc'), 'false'], - ['--no-summary', t('docs.scanFlag7Desc'), 'false'], - ['--batch', t('docs.scanFlag8Desc'), 'by-language'], - ['-f, --format', t('docs.scanFlag9Desc'), 'text'], - ['--concurrency', t('docs.scanFlag10Desc'), '8'], - ['--timeout', t('docs.scanFlag11Desc'), '10'], - ['--audience', t('docs.scanFlag12Desc'), 'human'], - ['-b, --background', t('docs.scanFlag13Desc'), '—'], - ['--max-tools', t('docs.scanFlag14Desc'), t('docs.scanFlag14Default')], - ['--max-git-procs', t('docs.scanFlag15Desc'), '16'], - ['--rule', t('docs.scanFlag16Desc'), '—'], - ['--tools', t('docs.scanFlag17Desc'), t('docs.scanFlag17Default')], - ['--repo', t('docs.scanFlag18Desc'), t('docs.scanFlag18Default')], - ].map(([flag, d, def], idx, arr) => ( -
-
- {flag} -
-
- {d} -
-
- {def} -
-
- ))} -
-

- {t('docs.scanNote').replace(/<\/?code>/g, '')} -

-
- - {/* ─── Viewer ─── */} -
-

{t('docs.viewerTitle')}

-

{t('docs.viewerDesc')}

- handleCopy('ocr viewer')} /> -

{t('docs.viewerNote')}

-
- - {/* ─── MCP Server ─── */} -
-

{t('docs.mcpTitle')}

-

{t('docs.mcpDesc')}

- -

{t('docs.mcpConfig')}

- handleCopy(mcpAddCommands)} /> -

{t('docs.mcpConfigLocation')}

- -

{t('docs.mcpDelete')}

- handleCopy(mcpDeleteCommands)} /> - -

{t('docs.mcpFields')}

-
-
-
{t('docs.mcpFieldCol')}
-
{t('docs.mcpRequiredCol')}
-
{t('docs.mcpDescCol')}
-
- {[ - ['command', t('docs.mcpYes'), t('docs.mcpFieldCommandDesc')], - ['args', t('docs.mcpNo'), t('docs.mcpFieldArgsDesc')], - ['tools', t('docs.mcpNo'), t('docs.mcpFieldToolsDesc')], - ['setup', t('docs.mcpNo'), t('docs.mcpFieldSetupDesc')], - ['env', t('docs.mcpNo'), t('docs.mcpFieldEnvDesc')], - ].map(([field, required, d], idx, arr) => ( -
-
- {field} -
-
- {required} -
-
- {d} -
-
- ))} -
- -

- {t('docs.mcpNote')} -

- -

{t('docs.mcpExample')}

- handleCopy(mcpCodeGraphCommands)} /> -
- - {/* ─── Claude Code Integration ─── */} -
-

{t('docs.envTitle')}

-

- {t('docs.envDesc').replace(/<\/?code>/g, '')} -

- handleCopy('export ANTHROPIC_BASE_URL=https://api.anthropic.com\nexport ANTHROPIC_AUTH_TOKEN=sk-ant-xxxxx\nexport ANTHROPIC_MODEL=claude-opus-4-6')} - /> -

- {t('docs.envNote').replace(/<\/?code>/g, '')} -

-
+ {/* ─── Prev / Next pagination ─── */} +
+ {prevDoc ? ( + + ) : } + {nextDoc ? ( + + ) : }
- {/* ─── Right sidebar: CONTENTS (fixed) ─── */} - {!isMobile && ( -
+ {/* ─── Right sidebar: page TOC ─── */} + {!isMobile && headings.length > 0 && ( +
- - {t('docs.toc')} + + + {t('docs.toc')} +
- {sections.map((s) => ( - - ))} + {headings.map((h, i) => { + const isActive = h.id === activeHeadingId; + const isHovered = h.id === hoveredHeadingId; + return ( + + ); + })}
)}
- + + {/* Search Modal */} + {searchOpen && ( +
setSearchOpen(false)} + > +
e.stopPropagation()} + > + {/* Search input */} +
+ + setSearchQuery(e.target.value)} + onKeyDown={handleSearchKeyDown} + placeholder={t('docs.search.placeholder')} + style={{ + flex: 1, + marginLeft: 12, + background: 'transparent', + border: 'none', + outline: 'none', + color: '#ffffff', + fontSize: 14, + fontFamily, + }} + /> +
+ {/* Results */} +
+ {searchQuery && searchResults.length === 0 && ( +
+ {t('docs.search.noResults')} +
+ )} + {searchResults.map((result, idx) => ( + + ))} +
+ {/* Footer hints */} + {searchResults.length > 0 && ( +
+
+ + + + {t('docs.search.hint.select')} + + + + {t('docs.search.hint.open')} + +
+ + esc + {t('docs.search.hint.close')} + +
+ )} +
+
+ )}
); }; diff --git a/pages/src/styles/docs-markdown.css b/pages/src/styles/docs-markdown.css new file mode 100644 index 0000000..6a8968e --- /dev/null +++ b/pages/src/styles/docs-markdown.css @@ -0,0 +1,242 @@ +/* Docs Markdown dark theme styles — matching DocsPage design */ +.docs-markdown { + font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 24px; + color: rgba(255, 255, 255, 0.7); +} + +.docs-markdown h1 { + font-size: 28px; + font-weight: 700; + color: #FFFFFF; + margin: 0 0 24px 0; + line-height: 36px; +} + +.docs-markdown h2 { + font-size: 20px; + font-weight: 600; + color: #FFFFFF; + margin: 40px 0 16px 0; + line-height: 28px; +} + +.docs-markdown h3 { + font-size: 16px; + font-weight: 600; + color: #FFFFFF; + margin: 28px 0 12px 0; + line-height: 24px; +} + +.docs-markdown h4 { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); + margin: 20px 0 8px 0; + line-height: 22px; +} + +.docs-markdown p { + margin: 0 0 12px 0; + color: rgba(255, 255, 255, 0.6); + line-height: 24px; +} + +.docs-markdown a { + color: #2BDE5E; + text-decoration: none; + transition: opacity 0.2s; +} + +.docs-markdown a:hover { + opacity: 0.8; + text-decoration: underline; +} + +/* Inline code */ +.docs-markdown code { + font-family: 'Menlo', 'Monaco', 'Consolas', monospace; + font-size: 13px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 4px; + padding: 2px 6px; + color: rgba(255, 255, 255, 0.85); +} + +/* Code blocks */ +.docs-markdown pre { + background: #000000; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 6px; + padding: 4px 16px; + margin: 0 0 16px 0; + overflow-x: auto; + display: flex; + align-self: stretch; + justify-content: space-between; + align-items: center; +} + +.docs-markdown pre code { + background: none; + border: none; + padding: 0; + font-size: 13px; + line-height: 22px; + color: rgba(255, 255, 255, 0.8); + white-space: pre; + display: block; + flex: 1; + min-width: 0; +} + +/* Lists */ +.docs-markdown ul, +.docs-markdown ol { + margin: 0 0 16px 0; + padding-left: 24px; +} + +.docs-markdown li { + margin-bottom: 6px; + color: rgba(255, 255, 255, 0.6); + line-height: 24px; +} + +.docs-markdown li > ul, +.docs-markdown li > ol { + margin-top: 6px; + margin-bottom: 0; +} + +/* Tables */ +.docs-markdown table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + margin: 0 0 16px 0; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.16); +} + +.docs-markdown thead th { + background: rgba(255, 255, 255, 0.04); + font-size: 13px; + font-weight: 500; + color: rgba(255, 255, 255, 0.6); + text-align: left; + padding: 12px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.16); +} + +.docs-markdown thead th:first-child { + border-top-left-radius: 8px; +} + +.docs-markdown thead th:last-child { + border-top-right-radius: 8px; +} + +.docs-markdown tbody td { + font-size: 13px; + color: rgba(255, 255, 255, 0.6); + padding: 12px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + vertical-align: top; +} + +.docs-markdown tbody tr:last-child td { + border-bottom: none; +} + +.docs-markdown tbody tr:last-child td:first-child { + border-bottom-left-radius: 8px; +} + +.docs-markdown tbody tr:last-child td:last-child { + border-bottom-right-radius: 8px; +} + +/* Blockquotes */ +.docs-markdown blockquote { + margin: 0 0 16px 0; + padding: 12px 16px; + border-left: 2px solid #2BDE5E; + background: rgba(255, 255, 255, 0.06); + border-radius: 0 6px 6px 0; +} + +.docs-markdown blockquote p { + margin: 0; + color: rgba(255, 255, 255, 0.6); + font-size: 13px; +} + +.docs-markdown blockquote p + p { + margin-top: 8px; +} + +/* Horizontal rule */ +.docs-markdown hr { + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.1); + margin: 32px 0; +} + +/* Strong / bold */ +.docs-markdown strong { + color: rgba(255, 255, 255, 0.9); + font-weight: 600; +} + +/* Em / italic */ +.docs-markdown em { + font-style: italic; + color: rgba(255, 255, 255, 0.7); +} + +/* Images */ +.docs-markdown img { + max-width: 100%; + border-radius: 8px; + margin: 8px 0; +} + +/* Mermaid code blocks — hide raw source (will be replaced by rendered SVG) */ +.docs-markdown pre code.language-mermaid { + display: none; +} + +/* Rendered mermaid diagrams */ +.docs-markdown .mermaid-rendered { + margin: 16px 0; + padding: 24px 16px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + overflow-x: auto; + display: flex; + justify-content: center; +} + +.docs-markdown .mermaid-rendered svg { + max-width: 100%; + height: auto; +} + +/* Scrollbar styling for code blocks */ +.docs-markdown pre::-webkit-scrollbar { + height: 6px; +} + +.docs-markdown pre::-webkit-scrollbar-track { + background: transparent; +} + +.docs-markdown pre::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 3px; +} diff --git a/pages/src/utils/headingId.ts b/pages/src/utils/headingId.ts new file mode 100644 index 0000000..0a87854 --- /dev/null +++ b/pages/src/utils/headingId.ts @@ -0,0 +1,10 @@ +/** + * Shared utility to generate heading IDs from text. + * Used by both extractHeadings (DocsPage TOC) and MarkdownRenderer (heading renderer) + * to ensure consistent anchor IDs. + */ +export function generateHeadingId(text: string): string { + // Strip HTML tags first (from marked output), then strip markdown formatting chars + const plain = text.replace(/<[^>]+>/g, '').replace(/[`*_\[\]()]/g, '').trim(); + return plain.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-').replace(/^-|-$/g, ''); +} diff --git a/pages/webpack.config.js b/pages/webpack.config.js index 55816da..2228fb2 100644 --- a/pages/webpack.config.js +++ b/pages/webpack.config.js @@ -36,6 +36,10 @@ module.exports = { { test: /\.(png|jpg|jpeg|gif)$/, type: 'asset/resource' + }, + { + test: /\.md$/, + type: 'asset/source' } ] },