From 50f3af3ea00612d69fe16da5e9f98f0398ec39ed Mon Sep 17 00:00:00 2001 From: Jay Hack Date: Tue, 23 Jun 2026 08:41:54 -0700 Subject: [PATCH 1/2] docs: keep the docs sidebar stable across navigation Move the docs header and sidebar into a persistent app/docs/layout.tsx so they no longer re-mount on every navigation. The sidebar is now a client component that derives the active item from usePathname(), which preserves its scroll position and stops the visible jump when clicking links. Also reserve the scrollbar gutter (scrollbar-gutter: stable) to prevent horizontal layout shift between pages of different heights, and switch scroll-behavior from smooth to auto so scroll-to-top is instantaneous. Co-authored-by: Cursor --- site/app/docs/[[...slug]]/page.tsx | 301 ++++++++--------------------- site/app/docs/layout.tsx | 71 +++++++ site/app/globals.css | 3 +- site/components/docs-sidebar.tsx | 103 ++++++++++ 4 files changed, 261 insertions(+), 217 deletions(-) create mode 100644 site/app/docs/layout.tsx create mode 100644 site/components/docs-sidebar.tsx diff --git a/site/app/docs/[[...slug]]/page.tsx b/site/app/docs/[[...slug]]/page.tsx index c6a0a2665..1b4ab8f97 100644 --- a/site/app/docs/[[...slug]]/page.tsx +++ b/site/app/docs/[[...slug]]/page.tsx @@ -1,4 +1,4 @@ -import { ArrowRight, ChevronRight } from "lucide-react"; +import { ArrowRight } from "lucide-react"; import type { Metadata } from "next"; import { MDXRemote } from "next-mdx-remote/rsc"; import Link from "next/link"; @@ -8,27 +8,18 @@ import rehypePrettyCode from "rehype-pretty-code"; import rehypeSlug from "rehype-slug"; import remarkGfm from "remark-gfm"; -import { DocsSearch } from "@/components/docs-search"; import { DocsToc } from "@/components/docs-toc"; -import { Wordmark } from "@/components/logo"; import { mdxComponents } from "@/components/mdx-components"; -import { ThemeToggle } from "@/components/theme-toggle"; -import { Button } from "@/components/ui/button"; import { auraCodeTheme } from "@/lib/aura-code-theme"; import { - type DocsNavItem, docsHref, - getDocsNav, getDocsPage, - getDocsSearchIndex, getDocsStaticParams, getHeadings, getPrevNext, } from "@/lib/docs"; import { cn } from "@/lib/utils"; -const githubUrl = "https://github.com/codegen-sh/graph-sitter"; - type DocsPageProps = { params: Promise<{ slug?: string[]; @@ -72,220 +63,98 @@ export default async function DocsPage({ params }: DocsPageProps) { const showToc = headings.length >= 2; return ( -
-
-
- - - - +
+
+
+

+ {page.title} +

+ {page.description ? ( +

+ {page.description} +

+ ) : null}
-
- -
- - -
-
- - Documentation menu - - -
- -
-
- -
-
-
-

- {page.title} -

- {page.description ? ( -

- {page.description} -

- ) : null} -
- {page.kind === "mdx" ? ( -
- + -
- ) : ( -
- {page.children.map((child) => ( - - - {child.title} - - {child.description ? ( - - {child.description} - - ) : null} - - ))} -
- )} - - {previous || next ? ( - - ) : null} -
- - {showToc ? ( - - ) : null} + ], + rehypeSlug, + [rehypeAutolinkHeadings, { behavior: "wrap" }], + ], + }, + }} + />
-
-
-
- ); -} - -function SidebarInner({ activeSlug }: { activeSlug: string }) { - return ( - <> - - - - ); -} - -function isActiveTree(item: DocsNavItem, slug: string): boolean { - if (item.slug === slug) { - return true; - } - return Boolean(item.children?.some((child) => isActiveTree(child, slug))); -} + )} -function DocsNavListItem({ - activeSlug, - item, -}: { - activeSlug: string; - item: DocsNavItem; -}) { - const isActive = item.slug === activeSlug; - const isOpen = isActiveTree(item, activeSlug); + {previous || next ? ( + + ) : null} + - return ( -
  • - - {item.title} - - {item.children && isOpen ? ( -
      - {item.children.map((child) => ( - - ))} -
    + {showToc ? ( + ) : null} -
  • + ); } diff --git a/site/app/docs/layout.tsx b/site/app/docs/layout.tsx new file mode 100644 index 000000000..3aca08b71 --- /dev/null +++ b/site/app/docs/layout.tsx @@ -0,0 +1,71 @@ +import { ChevronRight } from "lucide-react"; +import Link from "next/link"; +import type { ReactNode } from "react"; + +import { DocsSidebar } from "@/components/docs-sidebar"; +import { Wordmark } from "@/components/logo"; +import { ThemeToggle } from "@/components/theme-toggle"; +import { Button } from "@/components/ui/button"; +import { getDocsNav, getDocsSearchIndex } from "@/lib/docs"; + +const githubUrl = "https://github.com/codegen-sh/graph-sitter"; + +export default function DocsLayout({ children }: { children: ReactNode }) { + const groups = getDocsNav(); + const searchEntries = getDocsSearchIndex(); + + return ( +
    +
    +
    + + + + +
    +
    + +
    + + +
    +
    + + Documentation menu + + +
    + +
    +
    + + {children} +
    +
    +
    + ); +} diff --git a/site/app/globals.css b/site/app/globals.css index c3cc0480b..7566cdc5b 100644 --- a/site/app/globals.css +++ b/site/app/globals.css @@ -141,8 +141,9 @@ } html { - scroll-behavior: smooth; + scroll-behavior: auto; scroll-padding-top: 5.5rem; + scrollbar-gutter: stable; } body { diff --git a/site/components/docs-sidebar.tsx b/site/components/docs-sidebar.tsx new file mode 100644 index 000000000..471bfcefd --- /dev/null +++ b/site/components/docs-sidebar.tsx @@ -0,0 +1,103 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { DocsSearch } from "@/components/docs-search"; +import type { + DocsNavGroup, + DocsNavItem, + DocsSearchEntry, +} from "@/lib/docs"; +import { cn } from "@/lib/utils"; + +const DEFAULT_SLUG = "introduction/overview"; + +function pathnameToSlug(pathname: string | null) { + if (!pathname || pathname === "/docs" || pathname === "/docs/") { + return DEFAULT_SLUG; + } + + return decodeURIComponent(pathname) + .replace(/^\/docs\//u, "") + .replace(/\/+$/u, ""); +} + +export function DocsSidebar({ + groups, + searchEntries, +}: { + groups: DocsNavGroup[]; + searchEntries: DocsSearchEntry[]; +}) { + const activeSlug = pathnameToSlug(usePathname()); + + return ( + <> + + + + ); +} + +function isActiveTree(item: DocsNavItem, slug: string): boolean { + if (item.slug === slug) { + return true; + } + return Boolean(item.children?.some((child) => isActiveTree(child, slug))); +} + +function DocsNavListItem({ + activeSlug, + item, +}: { + activeSlug: string; + item: DocsNavItem; +}) { + const isActive = item.slug === activeSlug; + const isOpen = isActiveTree(item, activeSlug); + + return ( +
  • + + {item.title} + + {item.children && isOpen ? ( +
      + {item.children.map((child) => ( + + ))} +
    + ) : null} +
  • + ); +} From 65fb878fb359d49f07ad32da535d8e06569c80b8 Mon Sep 17 00:00:00 2001 From: jayhack <2548876+jayhack@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:43:59 +0000 Subject: [PATCH 2/2] Automated pre-commit update --- site/components/docs-sidebar.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/site/components/docs-sidebar.tsx b/site/components/docs-sidebar.tsx index 471bfcefd..ba678eb8f 100644 --- a/site/components/docs-sidebar.tsx +++ b/site/components/docs-sidebar.tsx @@ -4,11 +4,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { DocsSearch } from "@/components/docs-search"; -import type { - DocsNavGroup, - DocsNavItem, - DocsSearchEntry, -} from "@/lib/docs"; +import type { DocsNavGroup, DocsNavItem, DocsSearchEntry } from "@/lib/docs"; import { cn } from "@/lib/utils"; const DEFAULT_SLUG = "introduction/overview";