Skip to content

Commit dbce261

Browse files
authored
GH-148 Refactor docs sidebar. (#148)
1 parent a9127eb commit dbce261

File tree

10 files changed

+195
-384
lines changed

10 files changed

+195
-384
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { EditOnGitHub } from "@/components/page/docs/content/EditOnGitHub";
1414
import { ErrorBoundary } from "@/components/page/docs/content/ErrorBoundary";
1515
import { ReadingTime } from "@/components/page/docs/content/ReadingTime";
1616
import { ShortLink } from "@/components/page/docs/content/ShortLink";
17-
import { docsStructure } from "@/components/page/docs/sidebar-structure";
17+
import { docsStructure } from "@/components/page/docs/sidebar/sidebar-structure";
1818

1919
interface DocMeta {
2020
title: string;

app/docs/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { redirect } from "next/navigation";
22

3-
import { docsStructure } from "@/components/page/docs/sidebar-structure";
3+
import { docsStructure } from "@/components/page/docs/sidebar/sidebar-structure";
44

55
function flattenDocs(structure: typeof docsStructure) {
66
return structure.flatMap(item =>
Lines changed: 72 additions & 238 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,72 @@
11
"use client";
22

3-
import { motion, AnimatePresence } from "framer-motion";
43
import { Menu, X } from "lucide-react";
5-
import Link from "next/link";
64
import { usePathname } from "next/navigation";
7-
import React, { useMemo, useCallback, useEffect } from "react";
5+
import React, { useEffect } from "react";
86

9-
import Folder from "@/components/icons/folder";
107
import { cn } from "@/lib/utils";
118

12-
import { docsStructure, DocItem } from "../sidebar-structure";
13-
14-
import {
15-
sidebarStaggerContainer,
16-
sidebarFadeInLeft,
17-
sidebarFadeInUp,
18-
sidebarItemHover,
19-
} from "./animations";
20-
import { useMobileSidebar } from "./useMobileSidebar";
21-
22-
/**
23-
* Props for the DocSidebar component
24-
*/
25-
interface DocSidebarProps {
26-
/** Additional CSS classes to apply */
27-
className?: string;
28-
/** Callback function when a sidebar item is clicked */
29-
onItemClick?: (path: string) => void;
30-
/** Whether the sidebar has already been animated */
31-
hasAnimated?: boolean;
32-
}
33-
34-
/**
35-
* Props for the DocItemComponent
36-
*/
37-
interface DocItemProps {
38-
/** The documentation item to render */
39-
item: DocItem;
40-
/** The nesting level of the item */
41-
level: number;
42-
/** Whether the item is currently active */
43-
isActive: boolean;
44-
/** Callback function when the item is clicked */
45-
onItemClick?: (path: string) => void;
46-
/** Index of the item in its parent's children array */
47-
index: number;
48-
}
49-
50-
/**
51-
* Component for rendering a single documentation item in the sidebar
52-
*/
53-
const DocItemComponent: React.FC<DocItemProps> = React.memo(
54-
({ item, level, isActive, onItemClick, index }) => {
55-
const pathname = usePathname();
56-
const hasChildren = item.children && item.children.length > 0;
57-
58-
const isPathActive = useCallback(
59-
(path: string) => pathname === path || pathname.startsWith(`${path}/`),
60-
[pathname]
61-
);
62-
63-
const handleClick = useCallback(() => {
64-
onItemClick?.(item.path);
65-
}, [item.path, onItemClick]);
66-
67-
if (hasChildren) {
68-
return (
69-
<motion.div
70-
className={cn("mb-3", level === 0 && "first:mt-0")}
71-
variants={sidebarFadeInUp}
72-
custom={index}
73-
layout
9+
import { docsStructure } from "./sidebar-structure";
10+
import SidebarItem from "./SidebarItem";
11+
import { DocSidebarProps } from "./types";
12+
import { useMobileSidebar } from './useMobileSidebar';
13+
14+
const DocSidebar: React.FC<DocSidebarProps> = ({ className = "", onItemClick }) => {
15+
const pathname = usePathname();
16+
const { isOpen, isMobile, toggleSidebar, sidebarRef, toggleButtonRef, setIsOpen } = useMobileSidebar();
17+
18+
useEffect(() => {
19+
const handleEscapeKey = (event: KeyboardEvent) => {
20+
if (event.key === "Escape" && isOpen && isMobile) {
21+
toggleSidebar();
22+
}
23+
};
24+
25+
document.addEventListener("keydown", handleEscapeKey);
26+
return () => document.removeEventListener("keydown", handleEscapeKey);
27+
}, [isOpen, isMobile, toggleSidebar]);
28+
29+
return (
30+
<>
31+
{isMobile && (
32+
<button
33+
ref={toggleButtonRef}
34+
onClick={toggleSidebar}
35+
className="mb-4 flex w-full items-center justify-center gap-2 rounded-md bg-gray-100 px-3 py-2 text-sm font-medium text-gray-900 dark:bg-gray-800 dark:text-white lg:hidden"
36+
aria-expanded={isOpen}
37+
aria-controls="doc-sidebar"
7438
>
75-
<motion.div
76-
className={cn(
77-
"mb-1 text-sm font-semibold",
78-
level > 0 && "pl-4",
79-
"tracking-wide text-gray-900 dark:text-white"
80-
)}
81-
role="heading"
82-
aria-level={level + 1}
83-
whileHover={sidebarItemHover}
84-
layout
85-
>
86-
<Folder
87-
className="-mt-0.5 mr-2 inline-block h-4 w-4 align-middle text-gray-500 dark:text-gray-400"
88-
aria-hidden="true"
89-
/>
90-
{item.title}
91-
</motion.div>
92-
<motion.ul className="list-none space-y-1" variants={sidebarStaggerContainer} layout>
93-
{item.children?.map((child, childIndex) => (
94-
<li key={child.path}>
95-
<DocItemComponent
96-
item={child}
97-
level={level + 1}
98-
isActive={isPathActive(child.path)}
99-
onItemClick={onItemClick}
100-
index={childIndex}
101-
/>
102-
</li>
103-
))}
104-
</motion.ul>
105-
</motion.div>
106-
);
107-
}
39+
{isOpen ? (
40+
<>
41+
<X className="h-4 w-4" />
42+
<span>Close navigation</span>
43+
</>
44+
) : (
45+
<>
46+
<Menu className="h-4 w-4" />
47+
<span>Open navigation</span>
48+
</>
49+
)}
50+
</button>
51+
)}
10852

109-
return (
110-
<motion.div variants={sidebarFadeInUp} custom={index} layout>
111-
<Link
112-
href={item.path}
113-
onClick={handleClick}
53+
{(isOpen || !isMobile) && (
54+
<nav
55+
ref={sidebarRef}
56+
id="doc-sidebar"
11457
className={cn(
115-
"relative block rounded-lg py-1 pl-4 pr-8 text-sm font-medium transition-colors",
116-
isActive
117-
? "bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white"
118-
: "text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
58+
"w-full",
59+
isMobile
60+
? "fixed inset-y-0 left-0 z-50 w-64 overflow-y-auto bg-white p-4 shadow-lg dark:bg-gray-900"
61+
: className
11962
)}
120-
aria-current={isActive ? "page" : undefined}
121-
>
122-
<motion.span className="flex w-full items-start" whileHover={sidebarItemHover} layout>
123-
<span className="block flex-1">{item.title}</span>
124-
</motion.span>
125-
</Link>
126-
</motion.div>
127-
);
128-
}
129-
);
130-
131-
DocItemComponent.displayName = "DocItemComponent";
132-
133-
/**
134-
* Documentation sidebar component
135-
*
136-
* Renders the navigation sidebar for the documentation pages
137-
* with support for both desktop and mobile views
138-
*/
139-
const DocSidebar: React.FC<DocSidebarProps> = React.memo(
140-
({ className = "", onItemClick, hasAnimated = false }) => {
141-
const pathname = usePathname();
142-
const { isOpen, isMobile, toggleSidebar, sidebarRef, toggleButtonRef, setIsOpen } =
143-
useMobileSidebar();
144-
145-
// Add event listener for Escape key to close sidebar (for accessibility)
146-
useEffect(() => {
147-
const handleEscapeKey = (event: KeyboardEvent) => {
148-
if (event.key === "Escape" && isOpen && isMobile) {
149-
toggleSidebar();
150-
}
151-
};
152-
153-
// Add event listener
154-
document.addEventListener("keydown", handleEscapeKey);
155-
156-
// Clean up event listener on unmount
157-
return () => {
158-
document.removeEventListener("keydown", handleEscapeKey);
159-
};
160-
}, [isOpen, isMobile, toggleSidebar]);
161-
162-
const sidebarContent = useMemo(
163-
() => (
164-
<motion.ul
165-
className="list-none space-y-1"
166-
variants={sidebarStaggerContainer}
167-
initial={!hasAnimated ? "hidden" : false}
168-
animate={!hasAnimated ? "visible" : false}
169-
layout
63+
role="navigation"
64+
aria-label="Documentation navigation"
17065
>
171-
{docsStructure.map((item, index) => (
172-
<li key={item.path}>
173-
<DocItemComponent
66+
<div className="space-y-1">
67+
{docsStructure.map((item, index) => (
68+
<SidebarItem
69+
key={item.path}
17470
item={item}
17571
level={0}
17672
isActive={pathname === item.path}
@@ -180,82 +76,20 @@ const DocSidebar: React.FC<DocSidebarProps> = React.memo(
18076
}}
18177
index={index}
18278
/>
183-
</li>
184-
))}
185-
</motion.ul>
186-
),
187-
[pathname, onItemClick, isMobile, hasAnimated, setIsOpen]
188-
);
189-
190-
return (
191-
<>
192-
{/* Mobile sidebar toggle button */}
193-
{isMobile && (
194-
<button
195-
ref={toggleButtonRef}
196-
id="sidebar-toggle"
197-
onClick={toggleSidebar}
198-
className="mb-4 flex w-full items-center justify-center gap-2 rounded-md bg-gray-100 px-3 py-2 text-sm font-medium text-gray-900 dark:bg-gray-800 dark:text-white"
199-
aria-expanded={isOpen}
200-
aria-controls="doc-sidebar"
201-
>
202-
{isOpen ? (
203-
<>
204-
<X className="h-4 w-4" />
205-
<span>Close Navigation</span>
206-
</>
207-
) : (
208-
<>
209-
<Menu className="h-4 w-4" />
210-
<span>Open Navigation</span>
211-
</>
212-
)}
213-
</button>
214-
)}
215-
216-
{/* Sidebar navigation */}
217-
<AnimatePresence>
218-
{(isOpen || !isMobile) && (
219-
<motion.nav
220-
ref={sidebarRef}
221-
id="doc-sidebar"
222-
className={cn(
223-
"w-full",
224-
isMobile
225-
? "fixed inset-y-0 left-0 z-50 w-64 overflow-y-auto bg-white p-4 shadow-lg dark:bg-gray-900"
226-
: className
227-
)}
228-
role="navigation"
229-
aria-label="Documentation navigation"
230-
variants={sidebarFadeInLeft}
231-
initial={!hasAnimated ? "hidden" : false}
232-
animate={!hasAnimated ? "visible" : false}
233-
exit={{ opacity: 0, x: -20 }}
234-
transition={{ duration: 0.2 }}
235-
layout
236-
>
237-
{sidebarContent}
238-
</motion.nav>
239-
)}
240-
</AnimatePresence>
241-
242-
{/* Mobile sidebar backdrop */}
243-
{isMobile && isOpen && (
244-
<motion.div
245-
className="fixed inset-0 z-40 bg-black bg-opacity-50"
246-
onClick={toggleSidebar}
247-
aria-hidden="true"
248-
initial={{ opacity: 0 }}
249-
animate={{ opacity: 1 }}
250-
exit={{ opacity: 0 }}
251-
transition={{ duration: 0.2 }}
252-
/>
253-
)}
254-
</>
255-
);
256-
}
257-
);
258-
259-
DocSidebar.displayName = "DocSidebar";
79+
))}
80+
</div>
81+
</nav>
82+
)}
83+
84+
{isMobile && isOpen && (
85+
<div
86+
className="fixed inset-0 z-40 bg-black bg-opacity-50 lg:hidden"
87+
onClick={toggleSidebar}
88+
aria-hidden="true"
89+
/>
90+
)}
91+
</>
92+
);
93+
};
26094

26195
export default DocSidebar;

0 commit comments

Comments
 (0)