|
| 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