import type * as PageTree from 'fumadocs-core/page-tree';
import { TreeContextProvider } from 'fumadocs-ui/contexts/tree';
import { SidebarProvider } from 'fumadocs-ui/components/sidebar/base';
import { ArrowRight, Pencil } from 'lucide-react';
import { config } from 'virtual:seemore/config';
import type { RouteEntry } from '../../shared/types.js';
import { usePageModule } from '../lib/pages.js';
import { useRouteUrl } from '../router.js';
import { feature } from '../lib/features.js';
import { pruneTree, usePageTree } from '../lib/tree.js';
import { mdxComponents } from '../mdx/components.js';
import { usePrefetch } from '../features/prefetch.js';
import { PagePreview } from '../features/preview.js';
import { useSearchHighlight } from '../features/highlight.js';
import { useHashScroll } from '../features/anchors.js';
import { InlineEditor } from '../features/edit.js';
import { SeemoreProvider } from './Provider.js';
import { useExternalLinkBridge } from './ExternalLinkBridge.js';
import { Header } from './Header.js';
import { Sidebar } from './Sidebar.js';
import { Breadcrumb } from './Breadcrumb.js';
import { PageActions } from './PageActions.js';
import { BackToTop, PageFooter, SiteFooter } from './Footer.js';
import { SelectionCopyButton } from './SelectionCopyButton.js';
import { IntegratedToc, Toc, TocProvider } from './Toc.js';
/**
* The shell seemore owns. fumadocs-ui supplies the primitives — sidebar,
* TOC, search dialog, MDX components — and one layout, tuned by feature flags, arranges them.
*/
export function DocsLayout({ children }: { children: React.ReactNode }) {
const tree = usePageTree();
useExternalLinkBridge();
return (
{children}
);
}
export function DocPage({ entry }: { entry: RouteEntry }) {
const full = usePageTree();
const url = useRouteUrl();
const tree = feature('navigation.prune') ? pruneTree(full, url) : full;
// `use()` on the cached module promise, inside the hook: already-loaded pages render
// synchronously, which is what makes `renderToString` emit a complete page.
const page = usePageModule(entry);
const Content = page.default;
usePrefetch();
useSearchHighlight();
useHashScroll();
const integrated = feature('toc.integrate');
// Dev only, and off unless asked for: the editor writes to the user's files, and a static
// build has no server to write through. `import.meta.env.DEV` also keeps it out of the bundle.
const editable = import.meta.env.DEV && feature('content.edit');
return (
{integrated ? : undefined}
{feature('navigation.path') ? : undefined}
{editable ? : undefined}
{config.editLink !== undefined && feature('content.action.edit') ? (
{config.editLink.text}
) : undefined}
{feature('navigation.footer') ? : undefined}
{integrated ? undefined :
}
{feature('navigation.top') ?
: undefined}
);
}
export function NotFound() {
return (
Page not found
There is no page at this address.
);
}
/**
* A page whose own markup threw — an `.mdx` file reaching for a component seemore does not
* provide, most often. `seemore build` refuses to write such a page at all; here, in the dev
* server and on client-side navigation, the reason replaces the article, because React
* unmounts the whole app when nothing catches the error and a blank screen says nothing.
*/
export function PageError({ message }: { message: string }) {
return (
This page failed to render
{message}
);
}
/**
* The generated index: when no `index.md` or root `README.md` claims `/`, the home address
* lists every page instead of apologising.
*
* Rendered from the same page tree the sidebar reads, so the order — `meta.json`, frontmatter
* `order`, then title — is the order a reader would click through. Same shell as a page:
* header, sidebar, footer — only the article is generated.
*/
export function Overview() {
const tree = usePageTree();
return (
{config.title}
{config.description !== undefined ? {config.description}
: undefined}
{feature('navigation.top') ?
: undefined}
);
}
/** Pages render as card grids; a folder opens a titled section holding its own cards. */
type Segment = { pages: PageTree.Item[] } | { folder: PageTree.Folder };
/** h2 at the top level, one step smaller per nesting level, capped at h6. */
const SECTION_HEADING_TAGS = ['h2', 'h3', 'h4', 'h5', 'h6'] as const;
function OverviewSections({ nodes, depth = 0 }: { nodes: PageTree.Node[]; depth?: number }) {
const segments: Segment[] = [];
for (const node of nodes) {
if (node.type === 'page') {
const last = segments.at(-1);
if (last !== undefined && 'pages' in last) last.pages.push(node);
else segments.push({ pages: [node] });
} else if (node.type === 'folder') {
segments.push({ folder: node });
}
}
const Heading = SECTION_HEADING_TAGS[Math.min(depth, SECTION_HEADING_TAGS.length - 1)] ?? 'h6';
return (
<>
{segments.map((segment, index) =>
'pages' in segment ? (
) : (
{segment.folder.name}
{typeof segment.folder.description === 'string' && segment.folder.description !== '' ? (
{segment.folder.description}
) : undefined}
{/* A folder stands for its index page when it has one, and lists the rest under it. */}
),
)}
>
);
}
function OverviewGrid({ pages }: { pages: PageTree.Item[] }) {
if (pages.length === 0) return undefined;
return (
);
}
function joinUrl(base: string, path: string): string {
return `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
}