refactor: replace Fumadocs-owned docs chrome with custom DocsSidebar / DocsToc components

- Add `src/components/docs/docs-sidebar.tsx` — a bespoke sidebar that reads the Fumadocs page tree and renders a `<NavList>` of links using TanStack Router's `<Link>`; shows a `<details>` disclosure on mobile and a sticky `<aside>` rail on `md`+, active item highlighted via the site's `--accent` token
- Add `src/components/docs/docs-toc.tsx` — a bespoke table of contents that wraps Fumadocs' `AnchorProvider`/`useActiveAnchor` in a plain `<ul>` with depth-based padding; hidden below `xl`
- Replace `<DocsLayout>` / `<DocsPage>` / `<DocsTitle>` / `<DocsDescription>` in `docs.$.tsx` with the custom components and plain `<article>` markup so the docs page is fully owned by the site shell rather than Fumadocs' layout primitives; `baseOptions()` and `layout.shared.tsx` are deleted
- Simplify the Fumadocs CSS token override in `styles.css` — the `#nd-sidebar` specificity hack is no longer needed now that Fumadocs renders no sidebar of its own
- Move `bin` from the top-level field into `publishConfig` in both `apps/cli/package.json` and `packages/create-stanza/package.json` so the binary entries only apply to the published artifact, not local workspace resolution
- Add a `tslib` → `tslib/tslib.es6.mjs` alias in `vite.config.ts` to avoid Rolldown's CJS interop issue with tslib's UMD bundle in Vite 8
- Set `[--trees-border-radius-override:0px]` on the file-tree in `FilePreview` so selected rows render as flush rectangles rather than rounded pills
This commit is contained in:
jake committed 2026-05-21 18:58:22 -04:00
1 parent 7b4e99c19d
commit 27f90c1765
9 files changed
+180 -38

No files matched your search

+3 -3
View File
@@ -19,9 +19,6 @@
"url": "git+https://github.com/jakejarvis/stanza.git",
"directory": "apps/cli"
},
"bin": {
"stanza": "./dist/bin.mjs"
},
"files": [
"dist"
],
@@ -32,6 +29,9 @@
".": "./src/index.ts"
},
"publishConfig": {
"bin": {
"stanza": "./dist/bin.mjs"
},
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
@@ -114,7 +114,7 @@ export function FilePreview({
<FileTree
key={resolvedTheme}
model={model}
className="h-full overflow-auto py-2 [--trees-accent-override:var(--ring)] [--trees-bg-override:transparent] [--trees-padding-inline-override:8px] [--trees-selected-bg-override:var(--accent)] [--trees-selected-fg-override:var(--accent-foreground)]"
className="h-full overflow-auto py-2 [--trees-accent-override:var(--ring)] [--trees-bg-override:transparent] [--trees-border-radius-override:0px] [--trees-padding-inline-override:8px] [--trees-selected-bg-override:var(--accent)] [--trees-selected-fg-override:var(--accent-foreground)]"
style={treeStyle}
/>
</ResizablePanel>
@@ -0,0 +1,93 @@
import { Link, useRouterState } from "@tanstack/react-router";
import type { Node, Root } from "fumadocs-core/page-tree";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
function toSplat(url: string): string {
return url.replace(/^\/docs\/?/, "");
}
function normalize(path: string): string {
return path.length > 1 ? path.replace(/\/$/, "") : path;
}
function NavLink({ url, name, pathname }: { url: string; name: ReactNode; pathname: string }) {
const active = normalize(pathname) === normalize(url);
return (
<li>
<Link
to="/docs/$"
params={{ _splat: toSplat(url) }}
className={cn(
"block px-2 py-1.5 text-muted-foreground transition-colors hover:text-foreground",
active && "bg-accent font-medium text-accent-foreground",
)}
>
{name}
</Link>
</li>
);
}
function NavNode({ node, pathname }: { node: Node; pathname: string }) {
if (node.type === "separator") {
return (
<li className="px-2 pt-4 pb-1 text-xs font-medium tracking-wide text-muted-foreground/70">
{node.name}
</li>
);
}
if (node.type === "folder") {
return (
<li className="pt-2">
<p className="px-2 py-1 text-xs font-medium tracking-wide text-muted-foreground/70">
{node.name}
</p>
<ul className="space-y-0.5">
{node.index && (
<NavLink url={node.index.url} name={node.index.name} pathname={pathname} />
)}
{node.children.map((child, i) => (
<NavNode key={child.$id ?? i} node={child} pathname={pathname} />
))}
</ul>
</li>
);
}
return <NavLink url={node.url} name={node.name} pathname={pathname} />;
}
function NavList({ nodes, pathname }: { nodes: Node[]; pathname: string }) {
return (
<ul className="space-y-0.5 text-sm">
{nodes.map((node, i) => (
<NavNode key={node.$id ?? i} node={node} pathname={pathname} />
))}
</ul>
);
}
export function DocsSidebar({ tree }: { tree: Root }) {
const pathname = useRouterState({ select: (s) => s.location.pathname });
const list = <NavList nodes={tree.children} pathname={pathname} />;
return (
<>
{/* Mobile: a disclosure above the content so docs stay navigable < md. */}
<details className="mb-6 border-b border-border pb-4 md:hidden">
<summary className="cursor-pointer px-2 py-1 text-sm font-medium">Documentation</summary>
<div className="mt-3">{list}</div>
</details>
{/* Desktop: sticky left rail aligned to the page frame. */}
<aside className="hidden w-56 shrink-0 md:block">
<div className="sticky top-14 max-h-[calc(100svh-3.5rem)] overflow-y-auto py-8 pr-4">
{list}
</div>
</aside>
</>
);
}
+48
View File
@@ -0,0 +1,48 @@
import { AnchorProvider, useActiveAnchor } from "fumadocs-core/toc";
import type { TOCItemType } from "fumadocs-core/toc";
import { cn } from "@/lib/utils";
function TocList({ toc }: { toc: TOCItemType[] }) {
const active = useActiveAnchor();
return (
<ul className="space-y-0.5 text-sm">
{toc.map((item) => {
const id = item.url.split("#")[1];
const isActive = id === active;
return (
<li key={item.url}>
<a
href={item.url}
style={{ paddingLeft: `${0.5 + Math.max(0, item.depth - 2) * 0.75}rem` }}
className={cn(
"block py-1 text-muted-foreground transition-colors hover:text-foreground",
isActive && "font-medium text-foreground",
)}
>
{item.title}
</a>
</li>
);
})}
</ul>
);
}
export function DocsToc({ toc }: { toc: TOCItemType[] }) {
if (toc.length === 0) return null;
return (
<aside className="hidden w-56 shrink-0 xl:block">
<div className="sticky top-14 max-h-[calc(100svh-3.5rem)] overflow-y-auto py-8">
<p className="mb-2 px-2 text-xs font-medium tracking-wide text-muted-foreground/70">
On this page
</p>
<AnchorProvider toc={toc}>
<TocList toc={toc} />
</AnchorProvider>
</div>
</aside>
);
}
-13
View File
@@ -1,13 +0,0 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
// The stanza site header/footer wrap docs from __root, so Fumadocs renders no
// top nav of its own — only its sidebar + table of contents. Dark mode and
// search are owned by the site shell (ThemeProvider + registry palette), so
// Fumadocs' own theme switch and search trigger stay off.
export function baseOptions(): BaseLayoutProps {
return {
nav: { enabled: false },
themeSwitch: { enabled: false },
searchToggle: { enabled: false },
};
}
+23 -13
View File
@@ -2,13 +2,13 @@ import { createFileRoute, notFound } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import browserCollections from "collections/browser";
import { useFumadocsLoader } from "fumadocs-core/source/client";
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page";
import { DocsBody } from "fumadocs-ui/layouts/docs/page";
import { RootProvider } from "fumadocs-ui/provider/tanstack";
import { Suspense } from "react";
import { DocsSidebar } from "@/components/docs/docs-sidebar";
import { DocsToc } from "@/components/docs/docs-toc";
import { useMDXComponents } from "@/components/mdx";
import { baseOptions } from "@/lib/layout.shared";
import { buildHead } from "@/lib/seo";
import { source } from "@/lib/source";
@@ -31,13 +31,18 @@ const clientLoader = browserCollections.docs.createClientLoader({
id: "docs",
component({ toc, frontmatter, default: MDX }) {
return (
<DocsPage toc={toc}>
<DocsTitle>{frontmatter.title}</DocsTitle>
<DocsDescription>{frontmatter.description}</DocsDescription>
<DocsBody>
<MDX components={useMDXComponents()} />
</DocsBody>
</DocsPage>
<div className="xl:flex xl:gap-8">
<article className="min-w-0 flex-1 py-8">
<h1 className="text-2xl font-semibold tracking-tight">{frontmatter.title}</h1>
{frontmatter.description && (
<p className="mt-2 text-sm text-muted-foreground">{frontmatter.description}</p>
)}
<DocsBody className="mt-8">
<MDX components={useMDXComponents()} />
</DocsBody>
</article>
<DocsToc toc={toc} />
</div>
);
},
});
@@ -65,9 +70,14 @@ function Page() {
return (
<RootProvider theme={{ enabled: false }} search={{ enabled: false }}>
<DocsLayout {...baseOptions()} tree={data.pageTree}>
<Suspense>{clientLoader.useContent(data.path)}</Suspense>
</DocsLayout>
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<div className="md:flex md:gap-8">
<DocsSidebar tree={data.pageTree} />
<div className="min-w-0 flex-1">
<Suspense>{clientLoader.useContent(data.path)}</Suspense>
</div>
</div>
</div>
</RootProvider>
);
}
+3 -5
View File
@@ -8,12 +8,10 @@
@source "../node_modules/fumadocs-ui/dist/**/*.js";
/* Remap Fumadocs' own design tokens onto the stanza (shadcn) palette so the
* docs chrome inherits the site's exact colors. The `#nd-sidebar` selector is
* included to override fumadocs' neutral.css sidebar tweaks at equal
* specificity; values follow stanza's `:root`/`.dark` tokens automatically. */
* docs content (prose, code blocks, callouts) inherits the site's exact colors.
* Values follow stanza's `:root`/`.dark` tokens automatically. */
:root,
.dark,
.dark #nd-sidebar {
.dark {
--color-fd-background: var(--background);
--color-fd-foreground: var(--foreground);
--color-fd-muted: var(--muted);
+6
View File
@@ -17,5 +17,11 @@ export default defineConfig({
],
resolve: {
tsconfigPaths: true,
alias: {
// tslib's CJS UMD sets __esModule: true without providing a default
// export, which breaks Vite 8 / Rolldown's consistent CJS interop.
// Alias to the native ESM build to avoid the interop entirely.
tslib: "tslib/tslib.es6.mjs",
},
},
});
+3 -3
View File
@@ -18,14 +18,14 @@
"url": "git+https://github.com/jakejarvis/stanza.git",
"directory": "packages/create-stanza"
},
"bin": {
"create-stanza": "./dist/bin.mjs"
},
"files": [
"dist"
],
"type": "module",
"publishConfig": {
"bin": {
"create-stanza": "./dist/bin.mjs"
},
"access": "public"
},
"scripts": {