Skip to content

Commit 1059f6e

Browse files
feat: enhance sidebar with scroll area and mobile responsiveness
- Added ScrollArea component to make sidebar content scrollable - Implemented RemoveScroll functionality to prevent background scrolling on mobile - Created InternalContext to manage sidebar state and nesting levels - Added useMediaQuery hook for responsive behavior - Removed redundant sheet.tsx and sidebar.tsx components - Updated button styling with new variants and rounded corners - Improved folder state management using useOnChange instea
1 parent e27c1c0 commit 1059f6e

File tree

7 files changed

+124
-970
lines changed

7 files changed

+124
-970
lines changed

components/layout/Sidebar.tsx

Lines changed: 99 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
"use client";
2-
import {useTreeContext, useTreePath} from "fumadocs-ui/provider";
3-
import {createContext, ReactNode, useContext, useMemo, useState, useCallback, useEffect} from "react";
2+
import {useSidebar, useTreeContext, useTreePath} from "fumadocs-ui/provider";
3+
import {createContext, ReactNode, useContext, useMemo, useState, useCallback, ReactElement} from "react";
44
import {useDocsNavigation} from "@/app/docs/DocsNavigationProvider";
55
import * as PageTree from "fumadocs-core/page-tree";
66
import Link from "next/link";
77
import {usePathname} from "next/navigation";
88
import {ChevronDown} from "lucide-react";
99
import {cn} from "@/lib/utils";
1010
import {Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/components/ui/collapsible";
11+
import {ScrollArea, ScrollViewport} from "@/components/ui/scroll-area";
12+
import {useOnChange} from "fumadocs-core/utils/use-on-change";
1113
import { ncIsObject } from "@/utils/is";
14+
import { RemoveScroll } from "react-remove-scroll";
15+
import { useMediaQuery } from "@/utils/use-media-query";
1216

1317
interface FolderContextType {
1418
open: boolean;
@@ -25,16 +29,70 @@ const useFolderContext = () => {
2529
return context;
2630
};
2731

32+
interface InternalContext {
33+
level: number;
34+
isMobile?: boolean;
35+
closeSidebar?: () => void;
36+
}
37+
38+
const InternalContext = createContext<InternalContext | null>(null);
39+
40+
const useInternalContext = () => {
41+
const context = useContext(InternalContext);
42+
if (!context) {
43+
throw new Error("useInternalContext must be used within an InternalContext Provider");
44+
}
45+
return context;
46+
};
47+
48+
49+
export const SidebarList = ({
50+
as,
51+
blockScrollingWidth,
52+
removeScrollOn = blockScrollingWidth
53+
? `(width < ${blockScrollingWidth}px)`
54+
: undefined,
55+
...props
56+
}: {
57+
as?: string;
58+
blockScrollingWidth?: number;
59+
removeScrollOn?: string;
60+
children: ReactNode;
61+
} & React.HTMLAttributes<HTMLDivElement>): ReactElement => {
62+
const { open } = useSidebar();
63+
const isBlocking =
64+
useMediaQuery(removeScrollOn ?? '', !removeScrollOn) ?? false;
65+
66+
return (
67+
<RemoveScroll
68+
as={as ?? 'aside'}
69+
data-open={open}
70+
enabled={isBlocking && open}
71+
{...props}
72+
>
73+
{props.children}
74+
</RemoveScroll>
75+
);
76+
}
77+
2878
const Sidebar = ({isMobile}: {isMobile?: boolean}) => {
2979
const {root} = useTreeContext();
3080
const pathname = usePathname();
3181
const { setIsOpen } = useDocsNavigation();
3282

83+
const {open} = useSidebar()
84+
3385
const closeSidebar = useCallback(() => {
3486
if (isMobile) {
3587
setIsOpen(false);
3688
}
3789
}, [isMobile, setIsOpen]);
90+
91+
const context = useMemo<InternalContext>(() => ({
92+
level: 1,
93+
isMobile,
94+
closeSidebar
95+
}), [isMobile, closeSidebar]);
3896

3997
const children = useMemo(() => {
4098
function renderItems(items: PageTree.Node[], level: number) {
@@ -54,11 +112,15 @@ const Sidebar = ({isMobile}: {isMobile?: boolean}) => {
54112
}, [pathname]);
55113

56114
return (
57-
<div className={cn("xl:flex sticky py-4 mr-3 flex-col shrink-0 ", isMobile ? 'block' : 'hidden xl:block', isMobile ? 'w-64' : 'top-[120px] h-[calc(100dvh-120px)] w-64')}>
58-
<div className="flex flex-col gap-2">
59-
{children}
60-
</div>
61-
</div>
115+
<InternalContext.Provider value={context}>
116+
<SidebarList removeScrollOn="(width < 768px)" className={cn("xl:flex sticky py-4 mr-3 flex-col shrink-0 ", open ? 'block' : 'hidden xl:block', isMobile ? 'block' : 'hidden top-[120px] h-[calc(100dvh-120px)] w-64')}>
117+
<ScrollArea className="h-full">
118+
<ScrollViewport>
119+
{children}
120+
</ScrollViewport>
121+
</ScrollArea>
122+
</SidebarList>
123+
</InternalContext.Provider>
62124
);
63125
};
64126

@@ -68,14 +130,14 @@ function SidebarItem({item, children, level,}: {
68130
pathname: string;
69131
level: number;
70132
}) {
133+
const context = useInternalContext();
71134
const path = useTreePath();
72-
const active = path.includes(item);
73135

136+
const active = path.includes(item);
137+
74138
const handleLinkClick = () => {
75-
// Close sidebar on mobile navigation
76-
const sidebar = document.querySelector('[data-sidebar="sidebar"]');
77-
if (sidebar && window.innerWidth < 768) {
78-
// Mobile sidebar close logic will be handled by Shadcn sidebar
139+
if (context.closeSidebar) {
140+
context.closeSidebar();
79141
}
80142
};
81143

@@ -91,7 +153,7 @@ function SidebarItem({item, children, level,}: {
91153
href={item.url}
92154
onClick={handleLinkClick}
93155
>
94-
{ncIsObject(item.icon) ? item.icon : ''}
156+
{ncIsObject(item.icon) ? item.icon : ''}
95157
{item.name}
96158
</Link>
97159
);
@@ -100,12 +162,11 @@ function SidebarItem({item, children, level,}: {
100162
if (item.type === "separator") {
101163
return (
102164
<p className="text-fd-muted-foreground mt-6 mb-2 first:mt-0">
103-
{ncIsObject(item.icon) ? item.icon : ''}
165+
{ncIsObject(item.icon) ? item.icon : ''}
104166
{item.name}
105167
</p>
106168
);
107169
}
108-
109170
if (item.type === "folder") {
110171
return (
111172
<SidebarFolder defaultOpen={(active || (item.defaultOpen ?? false))}>
@@ -118,14 +179,14 @@ function SidebarItem({item, children, level,}: {
118179
active
119180
? "text-nc-content-grey-emphasis"
120181
: "text-nc-content-grey-subtle-2 hover:bg-nc-background-grey-light"
121-
)}
122-
href={item.index.url}
123-
onClick={handleLinkClick}
124-
>
125-
{ncIsObject(item.index.icon) ? item.index.icon : ''}
126-
{item.index.name}
127-
</Link>
128-
</div>
182+
)}
183+
href={item.index.url}
184+
onClick={handleLinkClick}
185+
>
186+
{ncIsObject(item.index.icon) ? item.index.icon : ''}
187+
{item.index.name}
188+
</Link>
189+
</div>
129190
</SidebarFolderTrigger>
130191
) : (
131192
<SidebarFolderTrigger>
@@ -135,7 +196,7 @@ function SidebarItem({item, children, level,}: {
135196
active? "text-nc-content-grey-subtle-2 font-[600]" : ""
136197
)}
137198
>
138-
{ncIsObject(item.icon) ? item.icon : ''}
199+
{ncIsObject(item.icon) ? item.icon : ''}
139200
{item.name}
140201
</div>
141202
</SidebarFolderTrigger>
@@ -160,9 +221,9 @@ function SidebarFolder({defaultOpen = false, children}: {
160221
}) {
161222
const [open, setOpen] = useState(defaultOpen);
162223

163-
useEffect(() => {
164-
if (defaultOpen) setOpen(defaultOpen);
165-
}, [defaultOpen]);
224+
useOnChange(defaultOpen, (v) => {
225+
if (v) setOpen(v);
226+
});
166227

167228
return (
168229
<Collapsible open={open} onOpenChange={setOpen}>
@@ -197,9 +258,19 @@ function SidebarFolderTrigger({children}: { children: ReactNode }) {
197258
}
198259

199260
function SidebarFolderContent({children}: { children: ReactNode }) {
261+
const ctx = useInternalContext();
262+
200263
return (
201264
<CollapsibleContent>
202-
{children}
265+
<InternalContext.Provider
266+
value={useMemo(() => ({
267+
level: ctx.level + 1,
268+
isMobile: ctx.isMobile,
269+
closeSidebar: ctx.closeSidebar
270+
}), [ctx.level, ctx.isMobile, ctx.closeSidebar])}
271+
>
272+
{children}
273+
</InternalContext.Provider>
203274
</CollapsibleContent>
204275
);
205276
}

components/ui/button.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@ import { cva, type VariantProps } from "class-variance-authority"
55
import { cn } from "@/lib/utils"
66

77
const buttonVariants = cva(
8-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
8+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg cursor-pointer text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
99
{
1010
variants: {
1111
variant: {
1212
default: "bg-primary text-primary-foreground hover:bg-primary/90",
1313
destructive:
14-
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
14+
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
1515
outline:
1616
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
1717
secondary:
18-
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
18+
"bg-nc-background-default text-nc-content-grey-subtle shadow-sm hover:bg-nc-background-grey-light border-1 border-nc-content-grey-medium",
1919
ghost:
2020
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
2121
link: "text-primary underline-offset-4 hover:underline",
22+
none: ""
2223
},
2324
size: {
2425
default: "h-9 px-4 py-2 has-[>svg]:px-3",

components/ui/sheet.tsx

Lines changed: 0 additions & 139 deletions
This file was deleted.

0 commit comments

Comments
 (0)