import React, { useEffect, useMemo, useRef, useState } from "react"; import { Link, Navigate, Route, Routes, useLocation, useNavigate, useParams } from "react-router-dom"; import type { AiCitation, BrandingSettings, ImportJobSummary, IntegrationSummary, PageDetail, PageSummary, SessionUser, WebhookDeliverySummary, WebhookSummary } from "@ledger/shared"; import { CommandSearch } from "./components/CommandSearch"; import { AdminConsole } from "./components/AdminConsole"; import { AskAiPage } from "./components/AskAiPage"; import { DraftsPage } from "./components/DraftsPage"; import { EmptyState } from "./components/EmptyState"; import { FeedbackForm } from "./components/FeedbackForm"; import { Icon } from "./components/Icon"; import { ImportsPage } from "./components/ImportsPage"; import { PageEditor } from "./components/PageEditor"; import { PreferencesPage, type PreferencesState } from "./components/PreferencesPage"; import { DocsSidebar } from "./components/DocsSidebar"; import { SearchPage } from "./components/SearchPage"; import { SearchBar } from "./components/SearchBar"; import { ContentSkeleton, SidebarSkeleton } from "./components/Skeleton"; import { SpacesPage } from "./components/SpacesPage"; import { api } from "./lib/api"; type BrandingResponse = { branding: BrandingSettings; }; const LEDGER_FOOTER = "Powered by Ledger made by ANord.cc"; type SetupStatus = { isInitialized: boolean; branding: { site_name: string; brand_color: string; } | null; }; type Space = { id: string; name: string; key: string; visibility: string; }; type PageRecordMap = Record; type AdminFeedback = Array<{ id: string; page_title: string; helpful: boolean; comment: string | null }>; function readStoredPreference(key: string, fallback: T, parse?: (raw: string) => T): T { if (typeof window === "undefined") { return fallback; } const value = window.localStorage.getItem(key); if (!value) { return fallback; } return parse ? parse(value) : (value as T); } function useSession() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { api .get<{ user: SessionUser | null }>("/api/auth/session") .then((response) => setUser(response.user)) .finally(() => setLoading(false)); }, []); return { user, setUser, loading }; } function useKnowledgeBaseData(enabled: boolean) { const [spaces, setSpaces] = useState([]); const [pagesBySpace, setPagesBySpace] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!enabled) { return; } let active = true; setLoading(true); setError(null); api .get<{ spaces: Space[] }>("/api/spaces") .then(async (response) => { if (!active) return; setSpaces(response.spaces); const entries = await Promise.all( response.spaces.map(async (space) => { const pages = await api.get<{ pages: PageSummary[] }>(`/api/pages/space/${space.key}`); return [space.key, pages.pages] as const; }) ); if (!active) return; setPagesBySpace(Object.fromEntries(entries)); }) .catch((reason) => { if (!active) return; setError(reason instanceof Error ? reason.message : "Could not load documentation."); }) .finally(() => { if (active) setLoading(false); }); return () => { active = false; }; }, [enabled]); return { spaces, pagesBySpace, loading, error }; } function useCommandShortcut(openSearch: () => void) { useEffect(() => { function handleKeyDown(event: KeyboardEvent) { const target = event.target as HTMLElement | null; const isTyping = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target?.isContentEditable; if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); openSearch(); } if (!isTyping && event.key === "/") { event.preventDefault(); openSearch(); } } window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [openSearch]); } function findCurrentSpace(spaces: Space[], page: PageDetail | null, pathname: string) { if (page) { return spaces.find((space) => space.id === page.spaceId); } const match = pathname.match(/^\/space\/([^/]+)/); if (!match) { return undefined; } return spaces.find((space) => space.key === match[1]); } function buildBreadcrumbs(page: PageDetail, pages: PageSummary[], space?: Space) { const byId = new Map(pages.map((item) => [item.id, item])); const trail: Array<{ title: string; slug?: string }> = []; let current: PageSummary | undefined = page; while (current?.parentPageId) { const parent = byId.get(current.parentPageId); if (!parent) break; trail.unshift({ title: parent.title, slug: parent.slug }); current = parent; } return [ { title: "Knowledge base" }, ...(space ? [{ title: space.name }] : []), ...trail, { title: page.title, slug: page.slug } ]; } function addCopyButtons(container: HTMLElement | null) { if (!container) return; container.querySelectorAll("pre").forEach((pre) => { if (pre.querySelector(".code-copy")) { return; } const code = pre.querySelector("code"); if (!code) return; const button = document.createElement("button"); button.type = "button"; button.className = "code-copy"; button.textContent = "Copy"; button.addEventListener("click", async () => { try { await navigator.clipboard.writeText(code.textContent ?? ""); button.textContent = "Copied"; window.setTimeout(() => { button.textContent = "Copy"; }, 1200); } catch { button.textContent = "Failed"; } }); pre.appendChild(button); }); } function DocsShell({ branding, user, spaces, pagesBySpace, loadingNavigation, navigationError, searchResults, searchLoading, onSearch, onLogout, preferences, children }: { branding: BrandingResponse["branding"] | null; user: SessionUser | null; spaces: Space[]; pagesBySpace: PageRecordMap; loadingNavigation: boolean; navigationError: string | null; searchResults: PageSummary[]; searchLoading: boolean; onSearch: (query: string) => void; onLogout: () => Promise; preferences: PreferencesState; children: React.ReactNode; }) { const location = useLocation(); const navigate = useNavigate(); const [searchOpen, setSearchOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false); const currentPageSlug = location.pathname.startsWith("/page/") ? decodeURIComponent(location.pathname.split("/page/")[1] ?? "") : undefined; const canCreate = user?.role === "editor" || user?.role === "admin" || user?.role === "owner"; useCommandShortcut(() => setSearchOpen(true)); const currentPage = useMemo( () => Object.values(pagesBySpace) .flat() .find((page) => page.slug === currentPageSlug), [currentPageSlug, pagesBySpace] ); const currentSpace = currentPage ? spaces.find((space) => space.id === currentPage.spaceId) : findCurrentSpace(spaces, null, location.pathname); return (
setSearchOpen(false)} onSearch={onSearch} spaces={spaces} results={searchResults} isLoading={searchLoading} /> {loadingNavigation ? ( ) : navigationError ? ( ) : ( setSidebarOpen(false)} /> )}
{branding?.siteName ?? "Ledger"} Documentation workspace
{user ? ( <> {canCreate ? ( ) : null} Admin ) : ( Sign in )}
{children}
{LEDGER_FOOTER} {branding?.footerLinks?.length ? ( ) : null}
); } function HomePage({ spaces, pagesBySpace, onSearch }: { spaces: Space[]; pagesBySpace: PageRecordMap; onSearch: (query: string) => void; }) { const navigate = useNavigate(); const [question, setQuestion] = useState(""); const [answer, setAnswer] = useState<{ answer: string; citations: AiCitation[]; disabled?: boolean } | null>(null); const [answerLoading, setAnswerLoading] = useState(false); const allPages = useMemo( () => Object.values(pagesBySpace) .flat() .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt)), [pagesBySpace] ); async function askLedger(event: React.FormEvent) { event.preventDefault(); if (!question.trim()) { return; } setAnswerLoading(true); try { const response = await api.post<{ answer: string; citations: AiCitation[]; disabled?: boolean }>( "/api/ai/answers", { question } ); setAnswer(response); } catch (error) { setAnswer({ answer: error instanceof Error ? error.message : "Could not generate an answer.", citations: [] }); } finally { setAnswerLoading(false); } } return (

Documentation hub

Find trusted answers, onboarding guides, and internal knowledge.

Browse collections, jump into recently updated docs, or search across your knowledge base from one calm workspace.

Collections

Browse by space

{spaces.map((space) => ( ))}

Recently updated

Fresh documentation

{allPages.slice(0, 8).map((page) => (
{page.title} {spaces.find((space) => space.id === page.spaceId)?.name ?? "Collection"}

{page.excerpt ?? "Open this document to read more."}

))} {allPages.length === 0 ? ( ) : null}

AI answers

Ask Ledger

Answers are generated only from pages the current account can read.

{answer ? (
{answer.disabled ? ( ) : ( <>

{answer.answer}

{answer.citations.map((citation) => ( {citation.title} ))}
)}
) : null}
); } function SpacePage({ spaces, pagesBySpace }: { spaces: Space[]; pagesBySpace: PageRecordMap }) { const { spaceKey = "" } = useParams(); const space = spaces.find((entry) => entry.key === spaceKey); const pages = pagesBySpace[spaceKey] ?? []; return (
Knowledge base / {space?.name ?? spaceKey}

Collection

{space?.name ?? spaceKey}

{pages.length} published documents are currently visible in this collection.

{space?.visibility ?? "internal"}
{pages.length === 0 ? ( ) : (
{pages.map((page) => (
{page.title} {page.visibility} - {new Date(page.updatedAt).toLocaleDateString()}

{page.excerpt ?? "Open this document to continue reading."}

))}
)}
); } function PageView({ spaces, pagesBySpace, user }: { spaces: Space[]; pagesBySpace: PageRecordMap; user: SessionUser | null; }) { const { slug = "" } = useParams(); const [page, setPage] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const articleRef = useRef(null); useEffect(() => { setLoading(true); setError(null); api .get<{ page: PageDetail }>(`/api/pages/slug/${slug}`) .then((response) => setPage(response.page)) .catch((reason) => { setError(reason instanceof Error ? reason.message : "Could not load this page."); setPage(null); }) .finally(() => setLoading(false)); }, [slug]); useEffect(() => { if (page) { addCopyButtons(articleRef.current); } }, [page]); if (loading) { return (
); } if (!page) { return (
Back to knowledge base} />
); } const space = spaces.find((entry) => entry.id === page.spaceId); const breadcrumbs = buildBreadcrumbs(page, pagesBySpace[space?.key ?? ""] ?? [], space); async function copyLink() { await navigator.clipboard.writeText(window.location.href); } return (
{breadcrumbs.map((item, index) => ( {index > 0 ? / : null} {item.slug && index < breadcrumbs.length - 1 ? ( {item.title} ) : ( {item.title} )} ))}
{page.visibility} {page.state}

{page.title}

Updated {new Date(page.updatedAt).toLocaleDateString()} by {page.authorName}

{page.source ? (

Imported from {page.source.provider.replace("_", " ")} on{" "} {new Date(page.source.importedAt).toLocaleDateString()}

) : null}
{user && (user.role === "editor" || user.role === "admin" || user.role === "owner") ? ( Edit doc ) : null}
{page.excerpt ?

{page.excerpt}

: null}
); } function EditorPage({ spaces, user }: { spaces: Space[]; user: SessionUser | null; }) { const { slug } = useParams(); const navigate = useNavigate(); const [page, setPage] = useState(null); const [loading, setLoading] = useState(Boolean(slug)); const [error, setError] = useState(null); const canEdit = Boolean(user && ["editor", "admin", "owner"].includes(user.role)); useEffect(() => { if (!slug) { setLoading(false); setPage(null); setError(null); return; } setLoading(true); setError(null); api .get<{ page: PageDetail }>(`/api/pages/slug/${slug}`) .then((response) => setPage(response.page)) .catch((reason) => setError(reason instanceof Error ? reason.message : "Could not load this page for editing.")) .finally(() => setLoading(false)); }, [slug]); if (!canEdit) { return ; } if (loading) { return (
); } if (error && slug) { return (
Back to home} />
); } return (
navigate(`/page/${nextSlug}`)} onCancel={() => navigate(slug ? `/page/${slug}` : "/spaces")} />
); } function LoginPage({ onLogin }: { onLogin: (user: SessionUser) => void }) { const navigate = useNavigate(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); async function submit(event: React.FormEvent) { event.preventDefault(); try { const response = await api.post<{ user: SessionUser }>("/api/auth/login", { email, password }); onLogin(response.user); navigate("/spaces"); } catch (reason) { setError(reason instanceof Error ? reason.message : "Could not sign in"); } } return (

Secure access

Sign in to internal documentation

Access private knowledge, restricted runbooks, and editing tools with your Ledger account.

Authentication

Welcome back

{error ?

{error}

: null}
); } function SetupPage({ onInitialized, initialBranding }: { onInitialized: (user: SessionUser, branding: BrandingResponse["branding"]) => void; initialBranding: SetupStatus["branding"]; }) { const navigate = useNavigate(); const [form, setForm] = useState({ siteName: initialBranding?.site_name ?? "Ledger", brandColor: initialBranding?.brand_color ?? "#245cff", publicKnowledgeBaseEnabled: true, ownerEmail: "", ownerDisplayName: "", password: "" }); const [status, setStatus] = useState(null); async function submit(event: React.FormEvent) { event.preventDefault(); try { const response = await api.post<{ user: SessionUser }>("/api/setup/initialize", form); onInitialized(response.user, { siteName: form.siteName, logoUrl: null, brandColor: form.brandColor, publicKnowledgeBaseEnabled: form.publicKnowledgeBaseEnabled, footerLinks: [] }); navigate("/spaces"); } catch (error) { setStatus(error instanceof Error ? error.message : "Could not initialize Ledger"); } } return (

First-time setup

Create your Ledger knowledge base

Define the base identity for your documentation workspace, create the first owner, and launch into collections, content, and integrations.

Collections and page structure
Search-first reading experience
Ready for branding and integrations

Workspace settings

Initialize Ledger

{status ?

{status}

: null}
); } function AccessDeniedPage() { return (
Back to spaces} />
); } function Dashboard({ user, spaces }: { user: SessionUser; spaces: Space[] }) { const [analytics, setAnalytics] = useState<{ topSearches: Array<{ query: string; count: number }>; noResults: Array<{ query: string; count: number }>; } | null>(null); const [feedback, setFeedback] = useState | null>(null); const [brandingForm, setBrandingForm] = useState({ siteName: "Ledger", logoUrl: "", brandColor: "#245cff", publicKnowledgeBaseEnabled: true }); const [settingsStatus, setSettingsStatus] = useState(null); useEffect(() => { if (user.role === "admin" || user.role === "owner") { api .get<{ topSearches: Array<{ query: string; count: number }>; noResults: Array<{ query: string; count: number }>; }>("/api/admin/search-analytics") .then(setAnalytics); api .get<{ feedback: Array<{ id: string; page_title: string; helpful: boolean; comment: string | null }> }>("/api/admin/feedback") .then((response) => setFeedback(response.feedback)); api .get<{ branding: { site_name: string; logo_url: string | null; brand_color: string; footer_text: string | null; public_knowledge_base_enabled: boolean } }>("/api/settings/admin") .then((response) => { const branding = response.branding; setBrandingForm({ siteName: branding.site_name, logoUrl: branding.logo_url ?? "", brandColor: branding.brand_color, publicKnowledgeBaseEnabled: branding.public_knowledge_base_enabled }); }); } }, [user.role]); async function saveBranding() { try { await api.put("/api/settings/branding", { siteName: brandingForm.siteName, logoUrl: brandingForm.logoUrl || null, brandColor: brandingForm.brandColor, publicKnowledgeBaseEnabled: brandingForm.publicKnowledgeBaseEnabled }); setSettingsStatus("Brand settings saved."); } catch (error) { setSettingsStatus(error instanceof Error ? error.message : "Could not save branding"); } } return (

Manage workspace

{user.displayName}

Create documents, tune branding, and use search analytics to improve the knowledge base.

{user.role} {spaces.length} collections
{user.role === "editor" || user.role === "admin" || user.role === "owner" ? ( ) : null} {user.role === "admin" || user.role === "owner" ? (

Branding

Workspace settings

Footer Powered by Ledger made by ANord.cc
{settingsStatus ?

{settingsStatus}

: null}
) : null} {analytics ? (

Analytics

Search performance

Top searches {analytics.topSearches.map((item) => (

{item.query} ({item.count})

))}
No results {analytics.noResults.map((item) => (

{item.query} ({item.count})

))}
) : null} {feedback ? (

Feedback queue

Reader responses

{feedback.map((item) => (
{item.page_title} {item.helpful ? "Helpful" : "Not helpful"}

{item.comment ?? "No comment provided."}

))}
) : null}
); } export function App() { const { user, setUser, loading } = useSession(); const [branding, setBranding] = useState(null); const [setupStatus, setSetupStatus] = useState(null); const [searchResults, setSearchResults] = useState([]); const [searchLoading, setSearchLoading] = useState(false); const [preferences, setPreferences] = useState(() => ({ theme: readStoredPreference("ledger.theme", "system"), compactSidebar: readStoredPreference("ledger.compactSidebar", false, (raw) => raw === "true"), emailNotifications: readStoredPreference("ledger.emailNotifications", true, (raw) => raw === "true"), productUpdates: readStoredPreference("ledger.productUpdates", true, (raw) => raw === "true"), smartText: readStoredPreference("ledger.smartText", true, (raw) => raw === "true"), showLineNumbers: readStoredPreference("ledger.showLineNumbers", true, (raw) => raw === "true") })); useEffect(() => { api.get("/api/settings/public").then((response) => setBranding(response.branding)); api.get("/api/setup/status").then(setSetupStatus); }, []); useEffect(() => { document.documentElement.dataset.theme = preferences.theme; document.documentElement.dataset.sidebarDensity = preferences.compactSidebar ? "compact" : "default"; window.localStorage.setItem("ledger.theme", preferences.theme); window.localStorage.setItem("ledger.compactSidebar", String(preferences.compactSidebar)); window.localStorage.setItem("ledger.emailNotifications", String(preferences.emailNotifications)); window.localStorage.setItem("ledger.productUpdates", String(preferences.productUpdates)); window.localStorage.setItem("ledger.smartText", String(preferences.smartText)); window.localStorage.setItem("ledger.showLineNumbers", String(preferences.showLineNumbers)); }, [preferences]); const { spaces, pagesBySpace, loading: loadingNavigation, error: navigationError } = useKnowledgeBaseData( Boolean(setupStatus?.isInitialized) ); const canAdmin = user?.role === "admin" || user?.role === "owner"; async function logout() { await api.post("/api/auth/logout"); setUser(null); } async function handleSearch(query: string) { if (!query.trim()) { setSearchResults([]); return; } setSearchLoading(true); try { const response = await api.get<{ pages: PageSummary[] }>(`/api/search?q=${encodeURIComponent(query)}`); setSearchResults(response.pages); } catch { setSearchResults([]); } finally { setSearchLoading(false); } } if (loading || !setupStatus) { return
Loading Ledger...
; } if (!setupStatus.isInitialized) { return (
{ setUser(initializedUser); setBranding(initializedBranding); setSetupStatus({ isInitialized: true, branding: { site_name: initializedBranding.siteName, brand_color: initializedBranding.brandColor } }); }} />
); } return ( } /> } /> } /> ) : ( ) } /> setPreferences((current) => ({ ...current, ...patch }))} onLogout={logout} /> ) : ( ) } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> : ) : } /> : ) : } /> ); }