// Documentation / Wiki screen — keyword search + article + TOC function Documentation({ user, perms }) { const [articleId, setArticleId] = useState("gs-overview"); const [query, setQuery] = useState(""); const [category, setCategory] = useState(null); // null = all // Search const q = query.trim().toLowerCase(); const searchResults = q ? DOC_INDEX.filter(a => a.searchText.includes(q)).slice(0, 20) : null; const article = DOC_ARTICLES.find(a => a.id === articleId) || DOC_ARTICLES[0]; return (
} /> {/* Hero search */}
TCMS Knowledge Base
{DOC_ARTICLES.length} articles · {DOC_CATEGORIES.length} categories · keyword indexed
setQuery(e.target.value)} placeholder="Search articles, tags, content…" style={{ width: 360, height: 36, fontSize: 13 }} autoFocus />
{q ? ( { setArticleId(id); setQuery(""); }} /> ) : (
{/* LEFT — category tree */} {/* CENTER — article */} {/* RIGHT — TOC + related */}
)}
); } function DocsSidebar({ articleId, onSelect }) { return (
{DOC_CATEGORIES.map(cat => { const items = DOC_ARTICLES.filter(a => a.category === cat.id); return (
{cat.label}
{items.map(a => ( ))}
); })}
); } function ArticleView({ article, onNavigate }) { if (!article) return null; const cat = DOC_CATEGORIES.find(c => c.id === article.category); return (
{cat?.label} · Updated {article.updated} {article.author && <>·{article.author}} {article.version && <>·v{article.version}}

{article.title}

{article.excerpt}
{article.tags?.map(t => ( #{t} ))}
{article.blocks.map((b, i) => )}
{/* Footer nav */}
Article ID: {article.id} Edit on internal wiki ↗
); } function Block({ block }) { switch (block.type) { case "p": return

{block.text}

; case "h2": return

{block.text}

; case "h3": return

{block.text}

; case "ul": return ( ); case "ol": return (
    {block.items.map((it, i) =>
  1. {it}
  2. )}
); case "code": return (
          {block.text}
        
); case "kv": return ( {block.rows.map(([k, v], i) => ( ))}
{k} {v}
); case "table": return (
{block.headers.map((h, i) => )} {block.rows.map((row, ri) => ( {row.map((c, ci) => )} ))}
{h}
{c}
); case "callout": { const tones = { info: { bg: "var(--teal-soft)", bd: "#c8e3ea", color: "var(--teal)", icon: "shield" }, warn: { bg: "var(--amber-soft)", bd: "#f0d8a8", color: "var(--amber)", icon: "bell" }, tip: { bg: "var(--green-soft)", bd: "#bfd9c4", color: "var(--green)", icon: "sparkle" }, }; const t = tones[block.tone] || tones.info; return (
{block.title && {block.title}.}{" "} {block.text}
); } case "permMatrix": return ; case "divider": return
; default: return null; } } function PermMatrix() { const features = [ { k: "viewRates", l: "View rates (own scope)" }, { k: "edit", l: "Create / update rates" }, { k: "import", l: "Import rates (template / AI)" }, { k: "export", l: "Export rate sheet" }, { k: "calculator", l: "Rate Calculator" }, { k: "aiqa", l: "AI Q&A (DIFY)" }, { k: "admin", l: "Admin functions" }, ]; const roles = ["rate_calculator", "read", "edit", "fbp", "procurement", "admin"]; return (
{roles.map(r => )} {features.map(f => ( {roles.map(r => ( ))} ))}
Feature{r}
{f.l} {PERMS[r][f.k] ? : }
); } function ArticleAside({ article, onNavigate }) { const headings = article.blocks.filter(b => b.type === "h2" || b.type === "h3").map(b => ({ text: b.text, level: b.type, id: slug(b.text) })); const related = (article.related || []).map(id => DOC_ARTICLES.find(a => a.id === id)).filter(Boolean); return (
{headings.length > 0 && ( )} {related.length > 0 && (
Related
{related.map(r => ( ))}
)} {article.category === "releases" && article.releaseDate && (
Version
v{article.version}
Released {article.releaseDate}
)}
); } function SearchResults({ results, query, onOpen }) { if (results.length === 0) { return (
No results for "{query}"
Try a shorter keyword, or browse the categories below.
{DOC_CATEGORIES.map(c => ( ))}
); } return (
Found {results.length} article{results.length === 1 ? "" : "s"} matching "{query}"
{results.map(r => { const cat = DOC_CATEGORIES.find(c => c.id === r.category); // Find a snippet around the match const idx = r.searchText.indexOf(query); let snippet = r.excerpt; if (idx >= 0) { const start = Math.max(0, idx - 40); const end = Math.min(r.searchText.length, idx + query.length + 80); snippet = (start > 0 ? "…" : "") + r.searchText.slice(start, end) + (end < r.searchText.length ? "…" : ""); } return (
onOpen(r.id)} onMouseEnter={e => e.currentTarget.style.borderColor = "var(--teal)"} onMouseLeave={e => e.currentTarget.style.borderColor = "var(--line)"}>
{cat?.label} Updated {r.updated} {r.version && v{r.version}}
{r.tags?.slice(0, 6).map(t => ( {t.toLowerCase().includes(query) ? #{t} : `#${t}`} ))}
); })}
); } function Highlight({ text, q }) { if (!q) return <>{text}; const lc = text.toLowerCase(); const parts = []; let i = 0, p = 0; while ((i = lc.indexOf(q, p)) >= 0) { if (i > p) parts.push({text.slice(p, i)}); parts.push({text.slice(i, i + q.length)}); p = i + q.length; } if (p < text.length) parts.push({text.slice(p)}); return <>{parts}; } function slug(s) { return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); } Object.assign(window, { Documentation });