import { useEffect, useMemo, useState } from 'react'; import { marked } from 'marked'; import DOMPurify from 'dompurify'; import { BookOpen, CloudOff, FileText, Search } from 'lucide-react'; import { initialDocs, syncDocs, type DocEntry, type DocsState } from './docs-store'; marked.setOptions({ gfm: true, breaks: false }); function renderMarkdown(markdown: string): string { const html = marked.parse(markdown, { async: false }); return DOMPurify.sanitize(typeof html === 'string' ? html : '', { USE_PROFILES: { html: true }, }); } function excerpt(markdown: string, length = 140): string { const text = markdown .replace(/^#[^\n]*\n/, ' ') .replace(/```[\s\S]*?```/g, ' ') .replace(/[#>*`\-[\]()]/g, ' ') .replace(/\s+/g, ' ') .trim(); return text.length > length ? `${text.slice(0, length)}…` : text; } export function DocsBrowser() { const [state, setState] = useState(() => initialDocs()); const [query, setQuery] = useState(''); const [selectedPath, setSelectedPath] = useState(null); useEffect(() => { syncDocs(setState); }, []); const docs = state.docs; const sections = useMemo(() => { const grouped = new Map(); for (const doc of docs) { const list = grouped.get(doc.section) ?? []; list.push(doc); grouped.set(doc.section, list); } return [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)); }, [docs]); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return null; return docs.filter( (doc) => doc.title.toLowerCase().includes(q) || doc.path.toLowerCase().includes(q) || doc.markdown.toLowerCase().includes(q), ); }, [docs, query]); const selected = useMemo( () => docs.find((doc) => doc.path === selectedPath) ?? null, [docs, selectedPath], ); const selectedHtml = useMemo( () => (selected ? renderMarkdown(selected.markdown) : ''), [selected], ); const sourceBadge = state.source === 'github' ? { label: 'live · github', cls: 'text-[var(--success)]' } : state.source === 'cache' ? { label: `cached · ${state.syncedAt ? new Date(state.syncedAt).toLocaleDateString() : 'offline'}`, cls: 'text-[var(--warning)]' } : { label: 'bundled copy', cls: 'text-[var(--text-tertiary)]' }; return (
setQuery(e.target.value)} placeholder="Search docs…" className="w-full bg-transparent text-[13px] text-[var(--text-primary)] outline-none placeholder:text-[var(--text-tertiary)]" />
{sourceBadge.label} {docs.length} docs
{filtered ? ( filtered.length > 0 ? ( filtered.map((doc) => ( )) ) : (

No docs match “{query}”.

) ) : ( sections.map(([section, entries]) => (

{section}

{entries.map((doc) => ( ))}
)) )}
{selected ? (

{selected.section} / {selected.path}

) : (

Select a document

Documentation is bundled with the app and refreshes from GitHub in the background. {!state.syncedAt ? ' Currently showing the bundled copy.' : ''}

{state.source !== 'github' ? (

Offline-safe — updates apply automatically when a connection is available.

) : null}
)}
{/* Mobile: tapping a doc replaces the list */} {selected ? (
{selected.title}
) : null}
); } function DocListItem({ doc, active, onSelect, showSection = false, }: { doc: DocEntry; active: boolean; onSelect: (path: string) => void; showSection?: boolean; }) { return ( ); }