Skip to content

Commit a5012f8

Browse files
committed
feat: implement sidebar showcase
1 parent e88a6f0 commit a5012f8

File tree

4 files changed

+1017
-0
lines changed

4 files changed

+1017
-0
lines changed

src/components/layout/DocsLayout.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,14 @@ export function DocsLayout({ children, className }: DocsLayoutProps) {
364364
Navigation
365365
</p>
366366
<ul className="space-y-2">
367+
<li>
368+
<Link
369+
className="text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] transition-colors"
370+
href="/components/sidebar"
371+
>
372+
Sidebar
373+
</Link>
374+
</li>
367375
<li>
368376
<Link
369377
className="text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] transition-colors"

src/components/ui/Sidebar.tsx

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import { Slot } from "@radix-ui/react-slot";
5+
import { cva, type VariantProps } from "class-variance-authority";
6+
import { cn } from "@/lib/utils";
7+
import {
8+
ArrowLeft01Icon,
9+
ArrowRight01Icon,
10+
MoreHorizontalIcon,
11+
} from "hugeicons-react";
12+
import {
13+
Tooltip,
14+
TooltipContent,
15+
TooltipProvider,
16+
TooltipTrigger,
17+
} from "./Tooltip";
18+
19+
// Context for sidebar state
20+
interface SidebarContextValue {
21+
collapsed: boolean;
22+
setCollapsed: (collapsed: boolean) => void;
23+
collapsible: boolean;
24+
}
25+
26+
const SidebarContext = React.createContext<SidebarContextValue | null>(null);
27+
28+
function useSidebar() {
29+
const context = React.useContext(SidebarContext);
30+
if (!context) {
31+
throw new Error("useSidebar must be used within a SidebarProvider");
32+
}
33+
return context;
34+
}
35+
36+
// Sidebar Provider
37+
interface SidebarProviderProps {
38+
children: React.ReactNode;
39+
defaultCollapsed?: boolean;
40+
collapsible?: boolean;
41+
}
42+
43+
function SidebarProvider({
44+
children,
45+
defaultCollapsed = false,
46+
collapsible = true,
47+
}: SidebarProviderProps) {
48+
const [collapsed, setCollapsed] = React.useState(defaultCollapsed);
49+
50+
return (
51+
<SidebarContext.Provider value={{ collapsed, setCollapsed, collapsible }}>
52+
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
53+
</SidebarContext.Provider>
54+
);
55+
}
56+
57+
// Sidebar Root
58+
const sidebarVariants = cva(
59+
"flex flex-col bg-[hsl(var(--background))] border-r border-[hsl(var(--border))] transition-all duration-300 ease-in-out h-full",
60+
{
61+
variants: {
62+
variant: {
63+
default: "bg-[hsl(var(--background))]",
64+
inset:
65+
"bg-[hsl(var(--muted))] border-0 rounded-[var(--radius)] m-2 h-[calc(100%-1rem)]",
66+
floating:
67+
"bg-[hsl(var(--background))] border border-[hsl(var(--border))] rounded-[var(--radius)] m-2 h-[calc(100%-1rem)] shadow-lg",
68+
},
69+
},
70+
defaultVariants: {
71+
variant: "default",
72+
},
73+
}
74+
);
75+
76+
interface SidebarProps
77+
extends React.HTMLAttributes<HTMLDivElement>,
78+
VariantProps<typeof sidebarVariants> {
79+
collapsedWidth?: string;
80+
expandedWidth?: string;
81+
}
82+
83+
const Sidebar = React.forwardRef<HTMLDivElement, SidebarProps>(
84+
(
85+
{
86+
className,
87+
variant,
88+
collapsedWidth = "4rem",
89+
expandedWidth = "16rem",
90+
style,
91+
...props
92+
},
93+
ref
94+
) => {
95+
const { collapsed } = useSidebar();
96+
97+
return (
98+
<div
99+
ref={ref}
100+
data-collapsed={collapsed}
101+
className={cn(sidebarVariants({ variant }), className)}
102+
style={{
103+
width: collapsed ? collapsedWidth : expandedWidth,
104+
minWidth: collapsed ? collapsedWidth : expandedWidth,
105+
...style,
106+
}}
107+
{...props}
108+
/>
109+
);
110+
}
111+
);
112+
Sidebar.displayName = "Sidebar";
113+
114+
// Sidebar Header
115+
const SidebarHeader = React.forwardRef<
116+
HTMLDivElement,
117+
React.HTMLAttributes<HTMLDivElement>
118+
>(({ className, ...props }, ref) => (
119+
<div
120+
ref={ref}
121+
className={cn("flex items-center gap-2 px-4 py-4 min-h-[60px]", className)}
122+
{...props}
123+
/>
124+
));
125+
SidebarHeader.displayName = "SidebarHeader";
126+
127+
// Sidebar Content (scrollable area)
128+
const SidebarContent = React.forwardRef<
129+
HTMLDivElement,
130+
React.HTMLAttributes<HTMLDivElement>
131+
>(({ className, ...props }, ref) => (
132+
<div
133+
ref={ref}
134+
className={cn("flex-1 overflow-y-auto overflow-x-hidden px-3 py-2", className)}
135+
{...props}
136+
/>
137+
));
138+
SidebarContent.displayName = "SidebarContent";
139+
140+
// Sidebar Footer
141+
const SidebarFooter = React.forwardRef<
142+
HTMLDivElement,
143+
React.HTMLAttributes<HTMLDivElement>
144+
>(({ className, ...props }, ref) => (
145+
<div
146+
ref={ref}
147+
className={cn(
148+
"mt-auto flex items-center gap-2 px-3 py-4 border-t border-[hsl(var(--border))]",
149+
className
150+
)}
151+
{...props}
152+
/>
153+
));
154+
SidebarFooter.displayName = "SidebarFooter";
155+
156+
// Sidebar Group
157+
interface SidebarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
158+
label?: string;
159+
}
160+
161+
const SidebarGroup = React.forwardRef<HTMLDivElement, SidebarGroupProps>(
162+
({ className, label, children, ...props }, ref) => {
163+
const { collapsed } = useSidebar();
164+
165+
return (
166+
<div ref={ref} className={cn("py-2", className)} {...props}>
167+
{label && !collapsed && (
168+
<span className="px-3 text-xs font-semibold text-[hsl(var(--muted-foreground))] uppercase tracking-wider mb-2 block">
169+
{label}
170+
</span>
171+
)}
172+
{label && collapsed && (
173+
<div className="flex justify-center py-1">
174+
<MoreHorizontalIcon className="h-4 w-4 text-[hsl(var(--muted-foreground))]" />
175+
</div>
176+
)}
177+
<nav className="space-y-1">{children}</nav>
178+
</div>
179+
);
180+
}
181+
);
182+
SidebarGroup.displayName = "SidebarGroup";
183+
184+
// Sidebar Item
185+
const sidebarItemVariants = cva(
186+
"group flex items-center gap-3 rounded-[var(--radius)] px-3 py-2.5 text-sm font-medium transition-all duration-150",
187+
{
188+
variants: {
189+
variant: {
190+
default:
191+
"text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent))]",
192+
active:
193+
"text-[hsl(var(--primary))] bg-[hsl(var(--primary))]/10 hover:bg-[hsl(var(--primary))]/15",
194+
},
195+
},
196+
defaultVariants: {
197+
variant: "default",
198+
},
199+
}
200+
);
201+
202+
interface SidebarItemProps
203+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
204+
VariantProps<typeof sidebarItemVariants> {
205+
icon?: React.ReactNode;
206+
badge?: React.ReactNode;
207+
asChild?: boolean;
208+
}
209+
210+
const SidebarItem = React.forwardRef<HTMLButtonElement, SidebarItemProps>(
211+
(
212+
{ className, variant, icon, badge, children, asChild = false, ...props },
213+
ref
214+
) => {
215+
const { collapsed } = useSidebar();
216+
const Comp = asChild ? Slot : "button";
217+
218+
const content = (
219+
<Comp
220+
ref={ref}
221+
className={cn(
222+
sidebarItemVariants({ variant }),
223+
collapsed && "justify-center px-2",
224+
className
225+
)}
226+
{...props}
227+
>
228+
{icon && (
229+
<span className="flex-shrink-0 h-5 w-5 flex items-center justify-center">
230+
{icon}
231+
</span>
232+
)}
233+
{!collapsed && (
234+
<>
235+
<span className="flex-1 truncate text-left">{children}</span>
236+
{badge && <span className="flex-shrink-0">{badge}</span>}
237+
</>
238+
)}
239+
</Comp>
240+
);
241+
242+
if (collapsed && children) {
243+
return (
244+
<Tooltip>
245+
<TooltipTrigger asChild>{content}</TooltipTrigger>
246+
<TooltipContent side="right" sideOffset={10}>
247+
{children}
248+
</TooltipContent>
249+
</Tooltip>
250+
);
251+
}
252+
253+
return content;
254+
}
255+
);
256+
SidebarItem.displayName = "SidebarItem";
257+
258+
// Sidebar Toggle Button
259+
type SidebarTriggerProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
260+
261+
const SidebarTrigger = React.forwardRef<HTMLButtonElement, SidebarTriggerProps>(
262+
({ className, ...props }, ref) => {
263+
const { collapsed, setCollapsed, collapsible } = useSidebar();
264+
265+
if (!collapsible) return null;
266+
267+
return (
268+
<button
269+
ref={ref}
270+
onClick={() => setCollapsed(!collapsed)}
271+
className={cn(
272+
"flex items-center justify-center h-8 w-8 rounded-[var(--radius)] text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] hover:bg-[hsl(var(--accent))] transition-colors",
273+
className
274+
)}
275+
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
276+
{...props}
277+
>
278+
{collapsed ? (
279+
<ArrowRight01Icon className="h-4 w-4" />
280+
) : (
281+
<ArrowLeft01Icon className="h-4 w-4" />
282+
)}
283+
</button>
284+
);
285+
}
286+
);
287+
SidebarTrigger.displayName = "SidebarTrigger";
288+
289+
// Sidebar Separator
290+
const SidebarSeparator = React.forwardRef<
291+
HTMLDivElement,
292+
React.HTMLAttributes<HTMLDivElement>
293+
>(({ className, ...props }, ref) => (
294+
<div
295+
ref={ref}
296+
className={cn("h-px bg-[hsl(var(--border))] my-2 mx-3", className)}
297+
{...props}
298+
/>
299+
));
300+
SidebarSeparator.displayName = "SidebarSeparator";
301+
302+
// Sidebar Inset (for main content area)
303+
const SidebarInset = React.forwardRef<
304+
HTMLDivElement,
305+
React.HTMLAttributes<HTMLDivElement>
306+
>(({ className, ...props }, ref) => (
307+
<div
308+
ref={ref}
309+
className={cn("flex-1 overflow-auto", className)}
310+
{...props}
311+
/>
312+
));
313+
SidebarInset.displayName = "SidebarInset";
314+
315+
export {
316+
Sidebar,
317+
SidebarProvider,
318+
SidebarHeader,
319+
SidebarContent,
320+
SidebarFooter,
321+
SidebarGroup,
322+
SidebarItem,
323+
SidebarTrigger,
324+
SidebarSeparator,
325+
SidebarInset,
326+
useSidebar,
327+
sidebarVariants,
328+
sidebarItemVariants,
329+
};
330+

src/components/ui/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "./
3636
export { Collapsible, CollapsibleTrigger, CollapsibleContent } from "./Collapsible";
3737
export { ScrollArea, ScrollBar } from "./ScrollArea";
3838
export { Separator } from "./Separator";
39+
export {
40+
Sidebar,
41+
SidebarProvider,
42+
SidebarHeader,
43+
SidebarContent,
44+
SidebarFooter,
45+
SidebarGroup,
46+
SidebarItem,
47+
SidebarTrigger,
48+
SidebarSeparator,
49+
SidebarInset,
50+
useSidebar,
51+
} from "./Sidebar";
3952

4053
// Overlay Components
4154
export {

0 commit comments

Comments
 (0)