mirror of
https://github.com/jakejarvis/stanza.git
synced 2026-10-16 06:15:43 -04:00
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:
9 files changed
+180
-38
No files matched your search
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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": {
|
||||
|
||||
Reference in new issue
Block a user