@srcroot/ui 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,512 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { PanelLeft } from "lucide-react"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { Button } from "@/components/ui/button"
9
+ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
10
+
11
+ const SIDEBAR_WIDTH = "16rem"
12
+ const SIDEBAR_WIDTH_MOBILE = "18rem"
13
+ const SIDEBAR_WIDTH_ICON = "3rem"
14
+
15
+ const SidebarContext = React.createContext<{
16
+ state: "expanded" | "collapsed"
17
+ open: boolean
18
+ setOpen: (open: boolean) => void
19
+ openMobile: boolean
20
+ setOpenMobile: (open: boolean) => void
21
+ isMobile: boolean
22
+ toggleSidebar: () => void
23
+ } | null>(null)
24
+
25
+ function useSidebar() {
26
+ const context = React.useContext(SidebarContext)
27
+ if (!context) {
28
+ throw new Error("useSidebar must be used within a SidebarProvider")
29
+ }
30
+ return context
31
+ }
32
+
33
+ const SidebarProvider = React.forwardRef<
34
+ HTMLDivElement,
35
+ React.HTMLAttributes<HTMLDivElement> & {
36
+ defaultOpen?: boolean
37
+ open?: boolean
38
+ onOpenChange?: (open: boolean) => void
39
+ }
40
+ >(({ className, style, children, defaultOpen = true, open: openProp, onOpenChange: setOpenProp, ...props }, ref) => {
41
+ const [isMobile, setIsMobile] = React.useState(false)
42
+ const [openMobile, setOpenMobile] = React.useState(false)
43
+
44
+ // Using internal state for "expanded/collapsed" on desktop
45
+ const [_open, _setOpen] = React.useState(defaultOpen)
46
+ const open = openProp ?? _open
47
+ const setOpen = React.useCallback(
48
+ (value: boolean | ((value: boolean) => boolean)) => {
49
+ if (setOpenProp) {
50
+ return setOpenProp(typeof value === "function" ? value(open) : value)
51
+ }
52
+ _setOpen(value)
53
+ },
54
+ [setOpenProp, open]
55
+ )
56
+
57
+
58
+ // Collapsible state helper
59
+ const state = open ? "expanded" : "collapsed"
60
+
61
+ const toggleSidebar = React.useCallback(() => {
62
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
63
+ }, [isMobile, setOpen, setOpenMobile])
64
+
65
+
66
+ React.useEffect(() => {
67
+ const checkMobile = () => {
68
+ setIsMobile(window.innerWidth < 1024) // lg breakpoint
69
+ }
70
+ checkMobile()
71
+ window.addEventListener("resize", checkMobile)
72
+ return () => window.removeEventListener("resize", checkMobile)
73
+ }, [])
74
+
75
+ return (
76
+ <SidebarContext.Provider
77
+ value={{
78
+ state,
79
+ open,
80
+ setOpen,
81
+ isMobile,
82
+ openMobile,
83
+ setOpenMobile,
84
+ toggleSidebar,
85
+ }}
86
+ >
87
+ <div
88
+ style={
89
+ {
90
+ "--sidebar-width": SIDEBAR_WIDTH,
91
+ "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
92
+ ...style,
93
+ } as React.CSSProperties
94
+ }
95
+ ref={ref}
96
+ className={cn(
97
+ "group/sidebar-wrapper flex min-h-screen w-full has-[[data-variant=inset]]:bg-sidebar",
98
+ className
99
+ )}
100
+ {...props}
101
+ >
102
+ {children}
103
+ </div>
104
+ </SidebarContext.Provider>
105
+ )
106
+ })
107
+ SidebarProvider.displayName = "SidebarProvider"
108
+
109
+ const Sidebar = React.forwardRef<
110
+ HTMLDivElement,
111
+ React.HTMLAttributes<HTMLDivElement> & {
112
+ side?: "left" | "right"
113
+ variant?: "sidebar" | "floating" | "inset"
114
+ collapsible?: "offcanvas" | "icon" | "none"
115
+ }
116
+ >(
117
+ (
118
+ {
119
+ side = "left",
120
+ variant = "sidebar",
121
+ collapsible = "offcanvas",
122
+ className,
123
+ children,
124
+ ...props
125
+ },
126
+ ref
127
+ ) => {
128
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
129
+
130
+ if (collapsible === "none") {
131
+ return (
132
+ <div
133
+ className={cn(
134
+ "flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
135
+ className
136
+ )}
137
+ ref={ref}
138
+ {...props}
139
+ >
140
+ {children}
141
+ </div>
142
+ )
143
+ }
144
+
145
+ if (isMobile) {
146
+ return (
147
+ <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
148
+ <SheetContent
149
+ data-sidebar="sidebar"
150
+ data-mobile="true"
151
+ className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
152
+ style={
153
+ {
154
+ "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
155
+ } as React.CSSProperties
156
+ }
157
+ side={side}
158
+ >
159
+ {/* Hidden Headers for Accessibility */}
160
+ <div className="sr-only">
161
+ <SheetHeader>
162
+ <SheetTitle>Menu</SheetTitle>
163
+ <SheetDescription>Navigation Menu</SheetDescription>
164
+ </SheetHeader>
165
+ </div>
166
+ <div className="flex h-full w-full flex-col">{children}</div>
167
+ </SheetContent>
168
+ </Sheet>
169
+ )
170
+ }
171
+
172
+ return (
173
+ <div
174
+ ref={ref}
175
+ className="group peer hidden md:block text-sidebar-foreground"
176
+ data-state={state}
177
+ data-collapsible={state === "collapsed" ? collapsible : ""}
178
+ data-variant={variant}
179
+ data-side={side}
180
+ >
181
+ {/* Visual Gap for Sidebar (Fixed placeholder) */}
182
+ <div
183
+ className={cn(
184
+ "duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear",
185
+ "group-data-[collapsible=offcanvas]:w-0",
186
+ "group-data-[side=right]:rotate-180",
187
+ variant === "floating" || variant === "inset"
188
+ ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
189
+ : "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
190
+ )}
191
+ />
192
+
193
+ {/* Actual Fixed Sidebar */}
194
+ <div
195
+ className={cn(
196
+ "duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
197
+ side === "left"
198
+ ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
199
+ : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
200
+ // Adjustments for floating/inset
201
+ variant === "floating" || variant === "inset"
202
+ ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+_2px)]"
203
+ : "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
204
+ className
205
+ )}
206
+ {...props}
207
+ >
208
+ <div
209
+ data-sidebar="sidebar"
210
+ className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow"
211
+ >
212
+ {children}
213
+ </div>
214
+ </div>
215
+ </div>
216
+ )
217
+ }
218
+ )
219
+ Sidebar.displayName = "Sidebar"
220
+
221
+ const SidebarTrigger = React.forwardRef<
222
+ HTMLButtonElement,
223
+ React.ButtonHTMLAttributes<HTMLButtonElement>
224
+ >(({ className, onClick, ...props }, ref) => {
225
+ const { toggleSidebar } = useSidebar()
226
+
227
+ return (
228
+ <Button
229
+ ref={ref}
230
+ data-sidebar="trigger"
231
+ variant="ghost"
232
+ size="icon"
233
+ className={cn("h-7 w-7", className)}
234
+ onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
235
+ onClick?.(event)
236
+ toggleSidebar()
237
+ }}
238
+ {...props}
239
+ >
240
+ <PanelLeft />
241
+ <span className="sr-only">Toggle Sidebar</span>
242
+ </Button>
243
+ )
244
+ })
245
+ SidebarTrigger.displayName = "SidebarTrigger"
246
+
247
+ const SidebarRail = React.forwardRef<
248
+ HTMLButtonElement,
249
+ React.ButtonHTMLAttributes<HTMLButtonElement>
250
+ >(({ className, ...props }, ref) => {
251
+ const { toggleSidebar } = useSidebar()
252
+
253
+ return (
254
+ <button
255
+ ref={ref}
256
+ data-sidebar="rail"
257
+ aria-label="Toggle Sidebar"
258
+ tabIndex={-1}
259
+ onClick={toggleSidebar}
260
+ title="Toggle Sidebar"
261
+ className={cn(
262
+ "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
263
+ "[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
264
+ "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
265
+ "group-data-[collapsible=offcanvas]:hover:bg-sidebar",
266
+ "group-data-[collapsible=icon]:w-2 group-data-[collapsible=icon]:group-data-[side=left]:-right-3 group-data-[collapsible=icon]:group-data-[side=right]:left-0",
267
+ className
268
+ )}
269
+ {...props}
270
+ />
271
+ )
272
+ })
273
+ SidebarRail.displayName = "SidebarRail"
274
+
275
+ const SidebarInset = React.forwardRef<
276
+ HTMLDivElement,
277
+ React.HTMLAttributes<HTMLDivElement>
278
+ >(({ className, ...props }, ref) => {
279
+ return (
280
+ <main
281
+ ref={ref}
282
+ className={cn(
283
+ "relative flex min-h-svh flex-1 flex-col bg-background",
284
+ // Width calculation based on sidebar state
285
+ "w-full md:w-[calc(100%-var(--sidebar-width))]",
286
+ "md:peer-data-[state=collapsed]:w-[calc(100%-var(--sidebar-width-icon))]",
287
+ "md:peer-data-[collapsible=offcanvas]:peer-data-[state=collapsed]:w-full",
288
+ // Transition for smooth resize
289
+ "transition-[width] duration-200 ease-linear",
290
+ // Inset variant styles
291
+ "peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
292
+ className
293
+ )}
294
+ {...props}
295
+ />
296
+ )
297
+ })
298
+ SidebarInset.displayName = "SidebarInset"
299
+
300
+ const SidebarHeader = React.forwardRef<
301
+ HTMLDivElement,
302
+ React.HTMLAttributes<HTMLDivElement>
303
+ >(({ className, ...props }, ref) => (
304
+ <div
305
+ ref={ref}
306
+ data-sidebar="header"
307
+ className={cn("flex flex-col gap-2 p-2", className)}
308
+ {...props}
309
+ />
310
+ ))
311
+ SidebarHeader.displayName = "SidebarHeader"
312
+
313
+ const SidebarFooter = React.forwardRef<
314
+ HTMLDivElement,
315
+ React.HTMLAttributes<HTMLDivElement>
316
+ >(({ className, ...props }, ref) => (
317
+ <div
318
+ ref={ref}
319
+ data-sidebar="footer"
320
+ className={cn("flex flex-col gap-2 p-2", className)}
321
+ {...props}
322
+ />
323
+ ))
324
+ SidebarFooter.displayName = "SidebarFooter"
325
+
326
+ const SidebarContent = React.forwardRef<
327
+ HTMLDivElement,
328
+ React.HTMLAttributes<HTMLDivElement>
329
+ >(({ className, ...props }, ref) => (
330
+ <div
331
+ ref={ref}
332
+ data-sidebar="content"
333
+ className={cn(
334
+ "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
335
+ className
336
+ )}
337
+ {...props}
338
+ />
339
+ ))
340
+ SidebarContent.displayName = "SidebarContent"
341
+
342
+ const SidebarGroup = React.forwardRef<
343
+ HTMLDivElement,
344
+ React.HTMLAttributes<HTMLDivElement>
345
+ >(({ className, ...props }, ref) => (
346
+ <div
347
+ ref={ref}
348
+ data-sidebar="group"
349
+ className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
350
+ {...props}
351
+ />
352
+ ))
353
+ SidebarGroup.displayName = "SidebarGroup"
354
+
355
+ const SidebarGroupLabel = React.forwardRef<
356
+ HTMLDivElement,
357
+ React.HTMLAttributes<HTMLDivElement> & { asChild?: boolean }
358
+ >(({ className, asChild = false, ...props }, ref) => {
359
+ // Only supporting div rendering for now
360
+ if (asChild) {
361
+ const child = React.Children.only(props.children) as React.ReactElement<any>
362
+ return React.cloneElement(child, {
363
+ ref,
364
+ "data-sidebar": "group-label",
365
+ className: cn(
366
+ "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
367
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
368
+ className,
369
+ child.props.className
370
+ ),
371
+ ...props,
372
+ children: child.props.children,
373
+ })
374
+ }
375
+
376
+ return (
377
+ <div
378
+ ref={ref}
379
+ data-sidebar="group-label"
380
+ className={cn(
381
+ "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
382
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
383
+ className
384
+ )}
385
+ {...props}
386
+ />
387
+ )
388
+ })
389
+ SidebarGroupLabel.displayName = "SidebarGroupLabel"
390
+
391
+ const SidebarGroupContent = React.forwardRef<
392
+ HTMLDivElement,
393
+ React.HTMLAttributes<HTMLDivElement>
394
+ >(({ className, ...props }, ref) => (
395
+ <div
396
+ ref={ref}
397
+ data-sidebar="group-content"
398
+ className={cn("w-full text-sm", className)}
399
+ {...props}
400
+ />
401
+ ))
402
+ SidebarGroupContent.displayName = "SidebarGroupContent"
403
+
404
+ const SidebarMenu = React.forwardRef<
405
+ HTMLUListElement,
406
+ React.HTMLAttributes<HTMLUListElement>
407
+ >(({ className, ...props }, ref) => (
408
+ <ul
409
+ ref={ref}
410
+ data-sidebar="menu"
411
+ className={cn("flex w-full min-w-0 flex-col gap-1", className)}
412
+ {...props}
413
+ />
414
+ ))
415
+ SidebarMenu.displayName = "SidebarMenu"
416
+
417
+ const SidebarMenuItem = React.forwardRef<
418
+ HTMLLIElement,
419
+ React.LiHTMLAttributes<HTMLLIElement>
420
+ >(({ className, ...props }, ref) => (
421
+ <li
422
+ ref={ref}
423
+ data-sidebar="menu-item"
424
+ className={cn("group/menu-item relative", className)}
425
+ {...props}
426
+ />
427
+ ))
428
+ SidebarMenuItem.displayName = "SidebarMenuItem"
429
+
430
+ const SidebarMenuButton = React.forwardRef<
431
+ HTMLButtonElement,
432
+ React.ButtonHTMLAttributes<HTMLButtonElement> & {
433
+ asChild?: boolean
434
+ isActive?: boolean
435
+ tooltip?: string | React.ComponentProps<any>
436
+ variant?: "default" | "ghost" | "outline" | "secondary" | "destructive" | "link" | null | undefined
437
+ size?: "default" | "sm" | "lg" | "icon" | null | undefined
438
+ }
439
+ >(
440
+ (
441
+ {
442
+ asChild = false,
443
+ isActive = false,
444
+ variant = "default",
445
+ size = "default",
446
+ tooltip,
447
+ className,
448
+ ...props
449
+ },
450
+ ref
451
+ ) => {
452
+ const Comp = "button"
453
+ // manual asChild handling
454
+ // const Comp = asChild ? Slot : "button"
455
+ // But we want to avoid Slot if possible per user request?
456
+ // Actually, if I can use the same cloneElement approach:
457
+
458
+ const buttonClass = cn(
459
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
460
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground",
461
+ "data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground",
462
+ "group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:group-data-[collapsible=icon]:hidden",
463
+ className
464
+ )
465
+
466
+ // If tooltip is needed, we should implement it. For now, ignoring complexity of tooltip.
467
+
468
+ if (asChild) {
469
+ const child = React.Children.only(props.children) as React.ReactElement<any>
470
+
471
+ return React.cloneElement(child, {
472
+ ref,
473
+ className: cn(buttonClass, child.props.className),
474
+ "data-active": isActive,
475
+ "data-sidebar": "menu-button",
476
+ "data-size": size,
477
+ ...props,
478
+ children: child.props.children
479
+ })
480
+ }
481
+
482
+ return (
483
+ <button
484
+ ref={ref}
485
+ data-sidebar="menu-button"
486
+ data-size={size}
487
+ data-active={isActive}
488
+ className={buttonClass}
489
+ {...props}
490
+ />
491
+ )
492
+ }
493
+ )
494
+ SidebarMenuButton.displayName = "SidebarMenuButton"
495
+
496
+ export {
497
+ Sidebar,
498
+ SidebarContent,
499
+ SidebarFooter,
500
+ SidebarGroup,
501
+ SidebarGroupContent,
502
+ SidebarGroupLabel,
503
+ SidebarHeader,
504
+ SidebarInset,
505
+ SidebarMenu,
506
+ SidebarMenuButton,
507
+ SidebarMenuItem,
508
+ SidebarProvider,
509
+ SidebarRail,
510
+ SidebarTrigger,
511
+ useSidebar,
512
+ }