Skip to content

Commit d2c502a

Browse files
adding sidebar (#110) (#112)
Co-authored-by: vansh kabra <134841334+VANSH3104@users.noreply.github.com>
1 parent 67e6913 commit d2c502a

4 files changed

Lines changed: 789 additions & 1 deletion

File tree

src/app/sidebar-demo/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { SidebarDemo } from '@/components/SidebarDemo';
2+
3+
export default function SidebarDemoPage() {
4+
return <SidebarDemo />;
5+
}

src/components/SidebarDemo.tsx

Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
"use client";
2+
3+
import React, { useState, useEffect } from "react";
4+
import {
5+
Home, Settings, User, FileText, Folder, Star, Bell, Search,
6+
Database, Code, Palette, Layout, ChevronRight, Menu, X
7+
} from "lucide-react";
8+
9+
// Utility function for class names
10+
const cn = (...classes: (string | boolean | undefined)[]) =>
11+
classes.filter(Boolean).join(" ");
12+
13+
// Types
14+
export interface MenuItem {
15+
id: string;
16+
label: string;
17+
icon?: React.ReactNode;
18+
href?: string;
19+
children?: MenuItem[];
20+
badge?: string | number;
21+
}
22+
23+
interface SidebarContextType {
24+
isOpen: boolean;
25+
isMobile: boolean;
26+
toggleSidebar: () => void;
27+
closeSidebar: () => void;
28+
}
29+
30+
const SidebarContext = React.createContext<SidebarContextType | undefined>(
31+
undefined
32+
);
33+
34+
const useSidebar = () => {
35+
const context = React.useContext(SidebarContext);
36+
if (!context) {
37+
throw new Error("useSidebar must be used within a SidebarProvider");
38+
}
39+
return context;
40+
};
41+
42+
// Provider
43+
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
44+
const [isOpen, setIsOpen] = useState(false);
45+
const [isMobile, setIsMobile] = useState(false);
46+
47+
useEffect(() => {
48+
const checkMobile = () => {
49+
const mobile = window.innerWidth < 768;
50+
setIsMobile(mobile);
51+
if (!mobile) setIsOpen(true);
52+
};
53+
54+
checkMobile();
55+
window.addEventListener("resize", checkMobile);
56+
return () => window.removeEventListener("resize", checkMobile);
57+
}, []);
58+
59+
return (
60+
<SidebarContext.Provider
61+
value={{
62+
isOpen,
63+
isMobile,
64+
toggleSidebar: () => setIsOpen(!isOpen),
65+
closeSidebar: () => setIsOpen(false),
66+
}}
67+
>
68+
{children}
69+
</SidebarContext.Provider>
70+
);
71+
};
72+
73+
// Button
74+
const Button: React.FC<{
75+
variant?: "ghost" | "default";
76+
size?: "icon" | "default";
77+
onClick?: () => void;
78+
className?: string;
79+
children: React.ReactNode;
80+
"aria-label"?: string;
81+
}> = ({ variant = "default", size = "default", onClick, className, children, "aria-label": ariaLabel }) => (
82+
<button
83+
onClick={onClick}
84+
aria-label={ariaLabel}
85+
className={cn(
86+
"inline-flex items-center justify-center rounded-lg font-medium transition-all duration-200",
87+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
88+
"disabled:opacity-50 disabled:pointer-events-none",
89+
variant === "ghost" && "hover:bg-gray-100 dark:hover:bg-gray-800",
90+
size === "icon" && "h-10 w-10",
91+
size === "default" && "px-4 py-2",
92+
className
93+
)}
94+
>
95+
{children}
96+
</button>
97+
);
98+
99+
// Sidebar Trigger
100+
export const SidebarTrigger: React.FC<{ className?: string }> = ({ className }) => {
101+
const { isOpen, toggleSidebar } = useSidebar();
102+
103+
return (
104+
<Button
105+
variant="ghost"
106+
size="icon"
107+
onClick={toggleSidebar}
108+
className={cn("md:hidden hover:scale-105 active:scale-95", className)}
109+
aria-label={isOpen ? "Close sidebar" : "Open sidebar"}
110+
>
111+
{isOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
112+
</Button>
113+
);
114+
};
115+
116+
// Menu Item
117+
const MenuItemComponent: React.FC<{
118+
item: MenuItem;
119+
level?: number;
120+
onItemClick?: (item: MenuItem) => void;
121+
}> = ({ item, level = 0, onItemClick }) => {
122+
const [isExpanded, setIsExpanded] = useState(false);
123+
const { closeSidebar, isMobile } = useSidebar();
124+
const hasChildren = item.children && item.children.length > 0;
125+
126+
const handleClick = () => {
127+
if (hasChildren) {
128+
setIsExpanded(!isExpanded);
129+
} else {
130+
onItemClick?.(item);
131+
if (isMobile) closeSidebar();
132+
}
133+
};
134+
135+
return (
136+
<div>
137+
<button
138+
onClick={handleClick}
139+
className={cn(
140+
"w-full flex items-center text-left text-sm transition-all duration-200 rounded-lg group relative",
141+
"hover:bg-gradient-to-r hover:from-blue-50 hover:to-transparent dark:hover:from-blue-950/30",
142+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1",
143+
"active:scale-[0.98]",
144+
"py-2.5 my-0.5"
145+
)}
146+
style={{ paddingLeft: `${16 + level * 16}px`, paddingRight: "12px" }}
147+
aria-expanded={hasChildren ? isExpanded : undefined}
148+
>
149+
<span className="flex-shrink-0 w-5 h-5 mr-3 flex items-center justify-center transition-transform duration-200 group-hover:scale-110">
150+
{item.icon || (level > 0 && (
151+
<span className="w-1.5 h-1.5 rounded-full bg-gray-400 dark:bg-gray-600" />
152+
))}
153+
</span>
154+
<span className="flex-1 truncate font-medium text-gray-700 dark:text-gray-200 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
155+
{item.label}
156+
</span>
157+
{item.badge && (
158+
<span className="flex-shrink-0 ml-2 px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200">
159+
{item.badge}
160+
</span>
161+
)}
162+
{hasChildren && (
163+
<span className={cn("flex-shrink-0 ml-2 transition-all duration-300", isExpanded && "rotate-90")}>
164+
<ChevronRight className="w-4 h-4 text-gray-500 dark:text-gray-400" />
165+
</span>
166+
)}
167+
</button>
168+
169+
{hasChildren && (
170+
<div
171+
className={cn(
172+
"overflow-hidden transition-all duration-300 ease-out",
173+
isExpanded ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0"
174+
)}
175+
>
176+
<div className="py-1 space-y-0.5 border-l-2 border-gray-200 dark:border-gray-700 ml-4">
177+
{item.children?.map((child) => (
178+
<MenuItemComponent key={child.id} item={child} level={level + 1} onItemClick={onItemClick} />
179+
))}
180+
</div>
181+
</div>
182+
)}
183+
</div>
184+
);
185+
};
186+
187+
// Sidebar
188+
export const Sidebar: React.FC<{ items: MenuItem[]; onItemClick?: (item: MenuItem) => void }> = ({ items, onItemClick }) => {
189+
const { isOpen, isMobile, closeSidebar } = useSidebar();
190+
191+
useEffect(() => {
192+
if (isMobile && isOpen) {
193+
document.body.style.overflow = "hidden";
194+
return () => { document.body.style.overflow = "unset"; };
195+
}
196+
}, [isMobile, isOpen]);
197+
198+
return (
199+
<>
200+
{isMobile && isOpen && (
201+
<div
202+
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 md:hidden"
203+
onClick={closeSidebar}
204+
aria-hidden="true"
205+
/>
206+
)}
207+
<aside
208+
className={cn(
209+
"bg-white/95 dark:bg-gray-900/95 backdrop-blur-xl",
210+
"border-r border-gray-200 dark:border-gray-800",
211+
"transition-all duration-300 ease-out shadow-2xl md:shadow-none",
212+
"w-72 h-full",
213+
isMobile
214+
? cn("fixed top-0 left-0 z-50", isOpen ? "translate-x-0" : "-translate-x-full")
215+
: "relative translate-x-0"
216+
)}
217+
aria-label="Sidebar navigation"
218+
>
219+
<div className="flex flex-col h-full">
220+
<div className="flex items-center justify-between px-6 py-5 border-b border-gray-200 dark:border-gray-800 bg-gradient-to-r from-blue-50 to-transparent dark:from-blue-950/20">
221+
<div className="flex items-center gap-3">
222+
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shadow-lg">
223+
<Layout className="w-5 h-5 text-white" />
224+
</div>
225+
<h2 className="text-lg font-bold text-gray-900 dark:text-white">DevUI</h2>
226+
</div>
227+
{isMobile && (
228+
<Button variant="ghost" size="icon" onClick={closeSidebar} className="h-8 w-8" aria-label="Close sidebar">
229+
<X className="h-4 w-4" />
230+
</Button>
231+
)}
232+
</div>
233+
234+
<nav className="flex-1 overflow-y-auto px-3 py-4 scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent">
235+
<div className="space-y-1">
236+
{items.map((item) => (
237+
<MenuItemComponent key={item.id} item={item} onItemClick={onItemClick} />
238+
))}
239+
</div>
240+
</nav>
241+
242+
<div className="px-4 py-4 border-t border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-900/50">
243+
<div className="flex items-center gap-3 px-2 py-2 rounded-lg hover:bg-white dark:hover:bg-gray-800 transition-colors cursor-pointer">
244+
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center text-white font-semibold shadow-lg">
245+
JD
246+
</div>
247+
<div className="flex-1 min-w-0">
248+
<p className="text-sm font-semibold text-gray-900 dark:text-white truncate">John Doe</p>
249+
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">john@example.com</p>
250+
</div>
251+
</div>
252+
</div>
253+
</div>
254+
</aside>
255+
</>
256+
);
257+
};
258+
259+
// Sidebar Content
260+
export const SidebarContent: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className }) => (
261+
<main className={cn("flex-1 flex flex-col min-h-0 transition-all duration-300 ease-out", className)}>
262+
{children}
263+
</main>
264+
);
265+
266+
// Navigation config
267+
const navigationItems: MenuItem[] = [
268+
{ id: "home", label: "Home", icon: <Home className="w-4 h-4" />, href: "/" },
269+
{
270+
id: "components", label: "Components", icon: <Layout className="w-4 h-4" />, badge: "New",
271+
children: [
272+
{
273+
id: "ui-components", label: "UI Components", icon: <Palette className="w-4 h-4" />,
274+
children: [
275+
{ id: "buttons", label: "Buttons", href: "/components/buttons" },
276+
{ id: "inputs", label: "Inputs", href: "/components/inputs" },
277+
{ id: "cards", label: "Cards", href: "/components/cards", badge: "3" },
278+
],
279+
},
280+
{
281+
id: "layout-components", label: "Layout", icon: <Layout className="w-4 h-4" />,
282+
children: [
283+
{ id: "sidebar", label: "Sidebar", href: "/components/sidebar" },
284+
{ id: "header", label: "Header", href: "/components/header" },
285+
],
286+
},
287+
],
288+
},
289+
{
290+
id: "documentation", label: "Documentation", icon: <FileText className="w-4 h-4" />,
291+
children: [
292+
{ id: "getting-started", label: "Getting Started", href: "/docs/getting-started" },
293+
{ id: "installation", label: "Installation", href: "/docs/installation" },
294+
{ id: "theming", label: "Theming", href: "/docs/theming" },
295+
],
296+
},
297+
{
298+
id: "data", label: "Data Management", icon: <Database className="w-4 h-4" />,
299+
children: [
300+
{ id: "api", label: "API", href: "/data/api" },
301+
{ id: "state", label: "State Management", href: "/data/state" },
302+
],
303+
},
304+
{ id: "tools", label: "Developer Tools", icon: <Code className="w-4 h-4" />, href: "/tools" },
305+
{
306+
id: "projects", label: "Projects", icon: <Folder className="w-4 h-4" />,
307+
children: [
308+
{ id: "my-projects", label: "My Projects", href: "/projects/my" },
309+
{ id: "templates", label: "Templates", href: "/projects/templates" },
310+
{ id: "examples", label: "Examples", href: "/projects/examples" },
311+
],
312+
},
313+
{
314+
id: "user", label: "User Account", icon: <User className="w-4 h-4" />,
315+
children: [
316+
{ id: "profile", label: "Profile", icon: <User className="w-3 h-3" />, href: "/user/profile" },
317+
{ id: "notifications", label: "Notifications", icon: <Bell className="w-3 h-3" />, href: "/user/notifications", badge: "5" },
318+
{ id: "favorites", label: "Favorites", icon: <Star className="w-3 h-3" />, href: "/user/favorites" },
319+
{ id: "settings", label: "Settings", icon: <Settings className="w-3 h-3" />, href: "/user/settings" },
320+
],
321+
},
322+
{ id: "search", label: "Search", icon: <Search className="w-4 h-4" />, href: "/search" },
323+
];
324+
325+
// Demo Component
326+
export function SidebarDemo() {
327+
const handleItemClick = (item: MenuItem) => console.log("Clicked:", item.label);
328+
329+
return (
330+
<SidebarProvider>
331+
<div className="flex h-screen w-full bg-gray-50 dark:bg-gray-950 overflow-hidden">
332+
<Sidebar items={navigationItems} onItemClick={handleItemClick} />
333+
334+
<SidebarContent>
335+
<div className="flex flex-col h-full">
336+
<header className="sticky top-0 z-10 flex items-center gap-4 px-6 py-4 border-b border-gray-200 dark:border-gray-800 bg-white/80 dark:bg-gray-900/80 backdrop-blur-xl">
337+
<SidebarTrigger className="flex-shrink-0" />
338+
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent flex-1">
339+
DevUI Component Library
340+
</h1>
341+
</header>
342+
343+
<main className="flex-1 overflow-auto p-6">
344+
<div className="max-w-5xl mx-auto space-y-8">
345+
<section>
346+
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-gray-400 bg-clip-text text-transparent">
347+
Enhanced Sidebar Navigation
348+
</h2>
349+
<p className="text-lg text-gray-600 dark:text-gray-400 leading-relaxed">
350+
A premium sidebar component with glass morphism, smooth animations,
351+
badge support, and delightful micro-interactions.
352+
</p>
353+
</section>
354+
355+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
356+
<FeatureCard title="Modern Design" description="Glass morphism effects, gradients, and contemporary styling" icon="🎨" />
357+
<FeatureCard title="Smooth Animations" description="Fluid transitions and micro-interactions throughout" icon="✨" />
358+
<FeatureCard title="Badge Support" description="Show notifications and counts on menu items" icon="🔔" />
359+
<FeatureCard title="Nested Menus" description="Unlimited nesting levels with visual hierarchy" icon="📂" />
360+
<FeatureCard title="Responsive" description="Perfect on desktop, tablet, and mobile devices" icon="📱" />
361+
<FeatureCard title="Accessible" description="Full keyboard navigation and ARIA support" icon="♿" />
362+
</div>
363+
364+
<section className="p-8 rounded-2xl bg-gradient-to-br from-blue-50 to-purple-50 dark:from-blue-950/30 dark:to-purple-950/30 border border-blue-200 dark:border-blue-800">
365+
<h3 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Try it out!</h3>
366+
<p className="text-gray-700 dark:text-gray-300 leading-relaxed">
367+
Resize your browser to see responsive behavior. Click menu items with arrows
368+
to expand nested sections. Notice the smooth animations, hover effects, and
369+
badge indicators throughout the interface.
370+
</p>
371+
</section>
372+
</div>
373+
</main>
374+
</div>
375+
</SidebarContent>
376+
</div>
377+
</SidebarProvider>
378+
);
379+
}
380+
381+
const FeatureCard: React.FC<{ title: string; description: string; icon: string }> = ({
382+
title, description, icon
383+
}) => (
384+
<div className="group p-6 rounded-xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 hover:shadow-xl hover:scale-[1.02] transition-all duration-300">
385+
<div className="text-4xl mb-4 group-hover:scale-110 transition-transform duration-300">{icon}</div>
386+
<h3 className="text-lg font-bold mb-2 text-gray-900 dark:text-white">{title}</h3>
387+
<p className="text-sm text-gray-600 dark:text-gray-400">{description}</p>
388+
</div>
389+
);

0 commit comments

Comments
 (0)