Skip to content

Commit 8a0485a

Browse files
jayhackcursoragent
andauthored
docs: stop sidebar jump and make scroll instant (#161)
## Summary Fixes two docs-site annoyances reported after #160: - **Sidebar jumps when clicking a link.** The docs route had no shared layout — `app/docs/[[...slug]]/page.tsx` rendered the header + sidebar + content itself, so every navigation re-mounted the sidebar and reset its scroll position. I moved the header and sidebar into a persistent `app/docs/layout.tsx`, and made the sidebar a client component (`components/docs-sidebar.tsx`) that derives the active item from `usePathname()`. The sidebar now stays mounted across navigations, preserving scroll and active state without a jump. - **Scroll-to-top was animated.** Switched `scroll-behavior: smooth` → `auto` in `globals.css` so scroll-to-top (and in-page anchor jumps) are instantaneous. Also added `scrollbar-gutter: stable` so pages of different heights don't cause a horizontal shift when the viewport scrollbar appears/disappears (another source of the sidebar "moving"). ## Changes - `site/app/docs/layout.tsx` (new): persistent header + sidebar shell + mobile menu. - `site/components/docs-sidebar.tsx` (new): client sidebar nav using `usePathname()` for active highlighting. - `site/app/docs/[[...slug]]/page.tsx`: reduced to just the article + TOC (header/sidebar moved to the layout). - `site/app/globals.css`: `scroll-behavior: auto` + `scrollbar-gutter: stable`. ## Test plan - [x] `npm run build` in `site/` passes (TypeScript + 287 static pages). - [ ] Click between sidebar links and confirm the sidebar no longer jumps and keeps its scroll position. - [ ] Confirm scroll-to-top on navigation and TOC anchor clicks are instant (no animation). Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: jayhack <2548876+jayhack@users.noreply.github.com>
1 parent 32a94aa commit 8a0485a

4 files changed

Lines changed: 257 additions & 217 deletions

File tree

site/app/docs/[[...slug]]/page.tsx

Lines changed: 85 additions & 216 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ArrowRight, ChevronRight } from "lucide-react";
1+
import { ArrowRight } from "lucide-react";
22
import type { Metadata } from "next";
33
import { MDXRemote } from "next-mdx-remote/rsc";
44
import Link from "next/link";
@@ -8,27 +8,18 @@ import rehypePrettyCode from "rehype-pretty-code";
88
import rehypeSlug from "rehype-slug";
99
import remarkGfm from "remark-gfm";
1010

11-
import { DocsSearch } from "@/components/docs-search";
1211
import { DocsToc } from "@/components/docs-toc";
13-
import { Wordmark } from "@/components/logo";
1412
import { mdxComponents } from "@/components/mdx-components";
15-
import { ThemeToggle } from "@/components/theme-toggle";
16-
import { Button } from "@/components/ui/button";
1713
import { auraCodeTheme } from "@/lib/aura-code-theme";
1814
import {
19-
type DocsNavItem,
2015
docsHref,
21-
getDocsNav,
2216
getDocsPage,
23-
getDocsSearchIndex,
2417
getDocsStaticParams,
2518
getHeadings,
2619
getPrevNext,
2720
} from "@/lib/docs";
2821
import { cn } from "@/lib/utils";
2922

30-
const githubUrl = "https://github.com/codegen-sh/graph-sitter";
31-
3223
type DocsPageProps = {
3324
params: Promise<{
3425
slug?: string[];
@@ -72,220 +63,98 @@ export default async function DocsPage({ params }: DocsPageProps) {
7263
const showToc = headings.length >= 2;
7364

7465
return (
75-
<div className="flex min-h-screen flex-col">
76-
<header className="sticky top-0 z-50 border-b border-border/60 bg-background/80 backdrop-blur-xl">
77-
<div className="mx-auto flex h-16 w-full max-w-[90rem] items-center justify-between px-6 lg:px-8">
78-
<Link href="/" aria-label="Graph-sitter home">
79-
<Wordmark />
80-
</Link>
81-
<nav className="flex items-center gap-1">
82-
<Button
83-
asChild
84-
variant="ghost"
85-
size="sm"
86-
className="text-muted-foreground hover:text-foreground"
87-
>
88-
<Link href="/">Home</Link>
89-
</Button>
90-
<Button
91-
asChild
92-
variant="ghost"
93-
size="sm"
94-
className="hidden text-muted-foreground hover:text-foreground sm:inline-flex"
95-
>
96-
<a href={githubUrl} target="_blank" rel="noreferrer">
97-
GitHub
98-
</a>
99-
</Button>
100-
<ThemeToggle />
101-
</nav>
66+
<div className="mx-auto flex w-full max-w-[70rem] gap-10 px-6 py-12 lg:px-10">
67+
<article className="min-w-0 max-w-3xl flex-1">
68+
<div className="mb-8">
69+
<h1 className="text-balance text-3xl font-semibold tracking-tight sm:text-[2.5rem] sm:leading-[1.1]">
70+
{page.title}
71+
</h1>
72+
{page.description ? (
73+
<p className="mt-3 text-lg leading-relaxed text-muted-foreground">
74+
{page.description}
75+
</p>
76+
) : null}
10277
</div>
103-
</header>
104-
105-
<div className="mx-auto flex w-full max-w-[90rem] flex-1">
106-
<aside className="hidden w-[17rem] shrink-0 border-r border-border/60 lg:block">
107-
<div className="sticky top-16 h-[calc(100vh-4rem)] overflow-y-auto px-5 py-7">
108-
<SidebarInner activeSlug={page.slug} />
109-
</div>
110-
</aside>
111-
112-
<main className="min-w-0 flex-1">
113-
<details className="group border-b border-border/60 px-6 py-3 lg:hidden">
114-
<summary className="flex cursor-pointer list-none items-center justify-between text-sm font-medium [&::-webkit-details-marker]:hidden">
115-
Documentation menu
116-
<ChevronRight className="size-4 text-muted-foreground transition-transform group-open:rotate-90" />
117-
</summary>
118-
<div className="mt-5">
119-
<SidebarInner activeSlug={page.slug} />
120-
</div>
121-
</details>
122-
123-
<div className="mx-auto flex w-full max-w-[70rem] gap-10 px-6 py-12 lg:px-10">
124-
<article className="min-w-0 max-w-3xl flex-1">
125-
<div className="mb-8">
126-
<h1 className="text-balance text-3xl font-semibold tracking-tight sm:text-[2.5rem] sm:leading-[1.1]">
127-
{page.title}
128-
</h1>
129-
{page.description ? (
130-
<p className="mt-3 text-lg leading-relaxed text-muted-foreground">
131-
{page.description}
132-
</p>
133-
) : null}
134-
</div>
13578

136-
{page.kind === "mdx" ? (
137-
<div className="docs-prose">
138-
<MDXRemote
139-
source={page.source}
140-
components={mdxComponents}
141-
options={{
142-
mdxOptions: {
143-
remarkPlugins: [remarkGfm],
144-
rehypePlugins: [
145-
[
146-
rehypePrettyCode,
147-
{
148-
keepBackground: false,
149-
theme: auraCodeTheme,
150-
},
151-
],
152-
rehypeSlug,
153-
[rehypeAutolinkHeadings, { behavior: "wrap" }],
154-
],
79+
{page.kind === "mdx" ? (
80+
<div className="docs-prose">
81+
<MDXRemote
82+
source={page.source}
83+
components={mdxComponents}
84+
options={{
85+
mdxOptions: {
86+
remarkPlugins: [remarkGfm],
87+
rehypePlugins: [
88+
[
89+
rehypePrettyCode,
90+
{
91+
keepBackground: false,
92+
theme: auraCodeTheme,
15593
},
156-
}}
157-
/>
158-
</div>
159-
) : (
160-
<div className="grid gap-4 sm:grid-cols-2">
161-
{page.children.map((child) => (
162-
<Link
163-
key={child.slug}
164-
href={child.href}
165-
className="group flex flex-col gap-2 rounded-xl border border-border bg-card p-5 transition-colors hover:border-primary/40"
166-
>
167-
<strong className="font-semibold text-foreground group-hover:text-primary">
168-
{child.title}
169-
</strong>
170-
{child.description ? (
171-
<span className="line-clamp-3 text-sm leading-relaxed text-muted-foreground">
172-
{child.description}
173-
</span>
174-
) : null}
175-
</Link>
176-
))}
177-
</div>
178-
)}
179-
180-
{previous || next ? (
181-
<nav
182-
aria-label="Pagination"
183-
className="mt-14 grid gap-4 border-t border-border pt-8 sm:grid-cols-2"
184-
>
185-
{previous ? (
186-
<PaginationLink
187-
href={docsHref(previous.slug)}
188-
title={previous.title}
189-
direction="previous"
190-
/>
191-
) : (
192-
<span />
193-
)}
194-
{next ? (
195-
<PaginationLink
196-
href={docsHref(next.slug)}
197-
title={next.title}
198-
direction="next"
199-
/>
200-
) : (
201-
<span />
202-
)}
203-
</nav>
204-
) : null}
205-
</article>
206-
207-
{showToc ? (
208-
<aside className="hidden w-56 shrink-0 xl:block">
209-
<div className="sticky top-16 max-h-[calc(100vh-4rem)] overflow-y-auto py-12">
210-
<DocsToc headings={headings} />
211-
</div>
212-
</aside>
213-
) : null}
94+
],
95+
rehypeSlug,
96+
[rehypeAutolinkHeadings, { behavior: "wrap" }],
97+
],
98+
},
99+
}}
100+
/>
214101
</div>
215-
</main>
216-
</div>
217-
</div>
218-
);
219-
}
220-
221-
function SidebarInner({ activeSlug }: { activeSlug: string }) {
222-
return (
223-
<>
224-
<DocsSearch entries={getDocsSearchIndex()} />
225-
<nav className="mt-6 space-y-7">
226-
{getDocsNav().map((group) => (
227-
<div key={group.title}>
228-
<p className="mb-1.5 px-2.5 text-xs font-semibold text-muted-foreground">
229-
{group.title}
230-
</p>
231-
<ul className="space-y-0.5">
232-
{group.items.map((item) => (
233-
<DocsNavListItem
234-
activeSlug={activeSlug}
235-
item={item}
236-
key={item.slug}
237-
/>
238-
))}
239-
</ul>
102+
) : (
103+
<div className="grid gap-4 sm:grid-cols-2">
104+
{page.children.map((child) => (
105+
<Link
106+
key={child.slug}
107+
href={child.href}
108+
className="group flex flex-col gap-2 rounded-xl border border-border bg-card p-5 transition-colors hover:border-primary/40"
109+
>
110+
<strong className="font-semibold text-foreground group-hover:text-primary">
111+
{child.title}
112+
</strong>
113+
{child.description ? (
114+
<span className="line-clamp-3 text-sm leading-relaxed text-muted-foreground">
115+
{child.description}
116+
</span>
117+
) : null}
118+
</Link>
119+
))}
240120
</div>
241-
))}
242-
</nav>
243-
</>
244-
);
245-
}
246-
247-
function isActiveTree(item: DocsNavItem, slug: string): boolean {
248-
if (item.slug === slug) {
249-
return true;
250-
}
251-
return Boolean(item.children?.some((child) => isActiveTree(child, slug)));
252-
}
121+
)}
253122

254-
function DocsNavListItem({
255-
activeSlug,
256-
item,
257-
}: {
258-
activeSlug: string;
259-
item: DocsNavItem;
260-
}) {
261-
const isActive = item.slug === activeSlug;
262-
const isOpen = isActiveTree(item, activeSlug);
123+
{previous || next ? (
124+
<nav
125+
aria-label="Pagination"
126+
className="mt-14 grid gap-4 border-t border-border pt-8 sm:grid-cols-2"
127+
>
128+
{previous ? (
129+
<PaginationLink
130+
href={docsHref(previous.slug)}
131+
title={previous.title}
132+
direction="previous"
133+
/>
134+
) : (
135+
<span />
136+
)}
137+
{next ? (
138+
<PaginationLink
139+
href={docsHref(next.slug)}
140+
title={next.title}
141+
direction="next"
142+
/>
143+
) : (
144+
<span />
145+
)}
146+
</nav>
147+
) : null}
148+
</article>
263149

264-
return (
265-
<li>
266-
<Link
267-
href={item.href}
268-
className={cn(
269-
"block rounded-md px-2.5 py-1.5 text-sm transition-colors",
270-
isActive
271-
? "bg-primary/10 font-medium text-primary"
272-
: "text-muted-foreground hover:bg-accent hover:text-foreground",
273-
)}
274-
>
275-
{item.title}
276-
</Link>
277-
{item.children && isOpen ? (
278-
<ul className="mt-0.5 ml-3 space-y-0.5 border-l border-border pl-2">
279-
{item.children.map((child) => (
280-
<DocsNavListItem
281-
activeSlug={activeSlug}
282-
item={child}
283-
key={child.slug}
284-
/>
285-
))}
286-
</ul>
150+
{showToc ? (
151+
<aside className="hidden w-56 shrink-0 xl:block">
152+
<div className="sticky top-16 max-h-[calc(100vh-4rem)] overflow-y-auto py-12">
153+
<DocsToc headings={headings} />
154+
</div>
155+
</aside>
287156
) : null}
288-
</li>
157+
</div>
289158
);
290159
}
291160

0 commit comments

Comments
 (0)