@srcroot/ui 0.0.44 → 0.0.46
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.
- package/package.json +1 -1
- package/src/registry/analytics/google-tag-manager.tsx +1 -1
- package/src/registry/themes/v3/slate.css +1 -1
- package/src/registry/themes/v4/blue.css +34 -36
- package/src/registry/themes/v4/gray.css +36 -38
- package/src/registry/themes/v4/green.css +34 -36
- package/src/registry/themes/v4/neutral.css +36 -38
- package/src/registry/themes/v4/orange.css +34 -36
- package/src/registry/themes/v4/rose.css +34 -36
- package/src/registry/themes/v4/slate.css +37 -39
- package/src/registry/themes/v4/stone.css +36 -38
- package/src/registry/themes/v4/violet.css +36 -38
- package/src/registry/themes/v4/zinc.css +34 -36
- package/src/registry/ui/calendar.tsx +2 -2
- package/src/registry/ui/chart.tsx +17 -15
- package/src/registry/ui/date-picker.tsx +64 -15
- package/src/registry/ui/dropdown-menu.tsx +92 -15
- package/src/registry/ui/popover.tsx +3 -2
- package/src/registry/ui/sidebar.tsx +159 -0
- package/src/registry/ui/theme-switcher.tsx +66 -0
|
@@ -14,6 +14,7 @@ interface DropdownMenuProps {
|
|
|
14
14
|
open?: boolean
|
|
15
15
|
onOpenChange?: (open: boolean) => void
|
|
16
16
|
defaultOpen?: boolean
|
|
17
|
+
className?: string
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -32,7 +33,7 @@ interface DropdownMenuProps {
|
|
|
32
33
|
* </DropdownMenuContent>
|
|
33
34
|
* </DropdownMenu>
|
|
34
35
|
*/
|
|
35
|
-
function DropdownMenu({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: DropdownMenuProps) {
|
|
36
|
+
function DropdownMenu({ children, open: controlledOpen, onOpenChange, defaultOpen = false, className }: DropdownMenuProps) {
|
|
36
37
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
37
38
|
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
38
39
|
|
|
@@ -41,13 +42,14 @@ function DropdownMenu({ children, open: controlledOpen, onOpenChange, defaultOpe
|
|
|
41
42
|
|
|
42
43
|
return (
|
|
43
44
|
<DropdownMenuContext.Provider value={{ open, onOpenChange: setOpen, triggerRef }}>
|
|
44
|
-
<div className="relative inline-block text-left">
|
|
45
|
+
<div className={cn("relative inline-block text-left", className)}>
|
|
45
46
|
{children}
|
|
46
47
|
</div>
|
|
47
48
|
</DropdownMenuContext.Provider>
|
|
48
49
|
)
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
|
|
51
53
|
interface DropdownMenuTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
52
54
|
asChild?: boolean
|
|
53
55
|
}
|
|
@@ -107,6 +109,11 @@ const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContent
|
|
|
107
109
|
const context = React.useContext(DropdownMenuContext)
|
|
108
110
|
if (!context) throw new Error("DropdownMenuContent must be used within DropdownMenu")
|
|
109
111
|
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
112
|
+
const [currentSide, setCurrentSide] = React.useState(side)
|
|
113
|
+
|
|
114
|
+
React.useEffect(() => {
|
|
115
|
+
setCurrentSide(side)
|
|
116
|
+
}, [side])
|
|
110
117
|
|
|
111
118
|
React.useEffect(() => {
|
|
112
119
|
const handleClickOutside = (e: MouseEvent) => {
|
|
@@ -129,17 +136,53 @@ const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContent
|
|
|
129
136
|
}
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
const checkPosition = () => {
|
|
140
|
+
if (context.open && contentRef.current && context.triggerRef.current) {
|
|
141
|
+
const triggerRect = context.triggerRef.current.getBoundingClientRect()
|
|
142
|
+
const contentRect = contentRef.current.getBoundingClientRect()
|
|
143
|
+
const viewportHeight = window.innerHeight
|
|
144
|
+
|
|
145
|
+
const spaceBelow = viewportHeight - triggerRect.bottom
|
|
146
|
+
const spaceAbove = triggerRect.top
|
|
147
|
+
const neededHeight = contentRect.height + sideOffset
|
|
148
|
+
|
|
149
|
+
if (side === 'bottom') {
|
|
150
|
+
if (spaceBelow < neededHeight && spaceAbove > neededHeight) {
|
|
151
|
+
setCurrentSide('top')
|
|
152
|
+
} else {
|
|
153
|
+
setCurrentSide('bottom')
|
|
154
|
+
}
|
|
155
|
+
} else if (side === 'top') {
|
|
156
|
+
if (spaceAbove < neededHeight && spaceBelow > neededHeight) {
|
|
157
|
+
setCurrentSide('bottom')
|
|
158
|
+
} else {
|
|
159
|
+
setCurrentSide('top')
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (context.open) {
|
|
166
|
+
// Check position immediately after render cycle (via timeout to allow ref to populate and layout to occur)
|
|
167
|
+
// Using requestAnimationFrame for better timing in layout cycle
|
|
168
|
+
requestAnimationFrame(checkPosition)
|
|
169
|
+
}
|
|
170
|
+
|
|
132
171
|
const timer = setTimeout(() => {
|
|
133
172
|
document.addEventListener("click", handleClickOutside)
|
|
134
173
|
}, 0)
|
|
135
174
|
document.addEventListener("keydown", handleEscape)
|
|
175
|
+
window.addEventListener("resize", checkPosition)
|
|
176
|
+
window.addEventListener("scroll", checkPosition, true) // Capture scroll to update pos
|
|
136
177
|
|
|
137
178
|
return () => {
|
|
138
179
|
clearTimeout(timer)
|
|
139
180
|
document.removeEventListener("click", handleClickOutside)
|
|
140
181
|
document.removeEventListener("keydown", handleEscape)
|
|
182
|
+
window.removeEventListener("resize", checkPosition)
|
|
183
|
+
window.removeEventListener("scroll", checkPosition, true)
|
|
141
184
|
}
|
|
142
|
-
}, [context.open, context])
|
|
185
|
+
}, [context.open, context, side, sideOffset])
|
|
143
186
|
|
|
144
187
|
if (!context.open) return null
|
|
145
188
|
|
|
@@ -150,12 +193,6 @@ const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContent
|
|
|
150
193
|
end: 'right-0',
|
|
151
194
|
}
|
|
152
195
|
|
|
153
|
-
// Calculate side classes
|
|
154
|
-
const sideClasses = {
|
|
155
|
-
bottom: `top-full mt-${sideOffset}`,
|
|
156
|
-
top: `bottom-full mb-${sideOffset}`,
|
|
157
|
-
}
|
|
158
|
-
|
|
159
196
|
return (
|
|
160
197
|
<div
|
|
161
198
|
ref={(node) => {
|
|
@@ -169,12 +206,12 @@ const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContent
|
|
|
169
206
|
"absolute z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
170
207
|
"animate-in fade-in-0 zoom-in-95",
|
|
171
208
|
alignmentClasses[align],
|
|
172
|
-
|
|
209
|
+
currentSide === 'bottom' ? 'top-full' : 'bottom-full',
|
|
173
210
|
className
|
|
174
211
|
)}
|
|
175
212
|
style={{
|
|
176
|
-
marginTop:
|
|
177
|
-
marginBottom:
|
|
213
|
+
marginTop: currentSide === 'bottom' ? sideOffset : undefined,
|
|
214
|
+
marginBottom: currentSide === 'top' ? sideOffset : undefined,
|
|
178
215
|
}}
|
|
179
216
|
{...props}
|
|
180
217
|
/>
|
|
@@ -183,16 +220,54 @@ const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContent
|
|
|
183
220
|
)
|
|
184
221
|
DropdownMenuContent.displayName = "DropdownMenuContent"
|
|
185
222
|
|
|
223
|
+
const DropdownMenuGroup = React.forwardRef<
|
|
224
|
+
HTMLDivElement,
|
|
225
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
226
|
+
>(({ className, ...props }, ref) => (
|
|
227
|
+
<div ref={ref} className={cn("", className)} {...props} />
|
|
228
|
+
))
|
|
229
|
+
DropdownMenuGroup.displayName = "DropdownMenuGroup"
|
|
230
|
+
|
|
231
|
+
const DropdownMenuPortal = ({ children }: { children: React.ReactNode }) => {
|
|
232
|
+
return <>{children}</>
|
|
233
|
+
}
|
|
234
|
+
DropdownMenuPortal.displayName = "DropdownMenuPortal"
|
|
235
|
+
|
|
186
236
|
const DropdownMenuItem = React.forwardRef<
|
|
187
237
|
HTMLDivElement,
|
|
188
|
-
React.HTMLAttributes<HTMLDivElement> & { inset?: boolean; disabled?: boolean }
|
|
189
|
-
>(({ className, inset, disabled, onClick, ...props }, ref) => {
|
|
238
|
+
React.HTMLAttributes<HTMLDivElement> & { inset?: boolean; disabled?: boolean; asChild?: boolean; closeOnSelect?: boolean }
|
|
239
|
+
>(({ className, inset, disabled, onClick, asChild = false, closeOnSelect = true, ...props }, ref) => {
|
|
190
240
|
const context = React.useContext(DropdownMenuContext)
|
|
191
241
|
|
|
192
242
|
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
193
243
|
if (disabled) return
|
|
194
244
|
onClick?.(e)
|
|
195
|
-
|
|
245
|
+
if (closeOnSelect) {
|
|
246
|
+
context?.onOpenChange(false)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (asChild) {
|
|
251
|
+
const child = React.Children.only(props.children) as React.ReactElement<any>
|
|
252
|
+
return React.cloneElement(child, {
|
|
253
|
+
ref,
|
|
254
|
+
className: cn(
|
|
255
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
|
|
256
|
+
inset && "pl-8",
|
|
257
|
+
disabled && "pointer-events-none opacity-50",
|
|
258
|
+
className,
|
|
259
|
+
child.props.className
|
|
260
|
+
),
|
|
261
|
+
onClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
|
262
|
+
handleClick(e)
|
|
263
|
+
child.props.onClick?.(e)
|
|
264
|
+
},
|
|
265
|
+
"data-disabled": disabled || undefined,
|
|
266
|
+
"data-inset": inset || undefined,
|
|
267
|
+
tabIndex: disabled ? -1 : 0,
|
|
268
|
+
...props,
|
|
269
|
+
children: child.props.children
|
|
270
|
+
})
|
|
196
271
|
}
|
|
197
272
|
|
|
198
273
|
return (
|
|
@@ -403,4 +478,6 @@ export {
|
|
|
403
478
|
DropdownMenuSub,
|
|
404
479
|
DropdownMenuSubTrigger,
|
|
405
480
|
DropdownMenuSubContent,
|
|
481
|
+
DropdownMenuGroup,
|
|
482
|
+
DropdownMenuPortal,
|
|
406
483
|
}
|
|
@@ -83,8 +83,8 @@ PopoverTrigger.displayName = "PopoverTrigger"
|
|
|
83
83
|
|
|
84
84
|
const PopoverContent = React.forwardRef<
|
|
85
85
|
HTMLDivElement,
|
|
86
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
87
|
-
>(({ className, children, ...props }, ref) => {
|
|
86
|
+
React.HTMLAttributes<HTMLDivElement> & { align?: "start" | "center" | "end" }
|
|
87
|
+
>(({ className, children, align = "center", ...props }, ref) => {
|
|
88
88
|
const context = React.useContext(PopoverContext)
|
|
89
89
|
if (!context) throw new Error("PopoverContent must be used within Popover")
|
|
90
90
|
|
|
@@ -121,6 +121,7 @@ const PopoverContent = React.forwardRef<
|
|
|
121
121
|
ref={ref}
|
|
122
122
|
className={cn(
|
|
123
123
|
"absolute z-50 mt-2 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none",
|
|
124
|
+
align === "end" ? "right-0" : align === "start" ? "left-0" : "left-1/2 -translate-x-1/2",
|
|
124
125
|
className
|
|
125
126
|
)}
|
|
126
127
|
onClick={(e) => e.stopPropagation()}
|
|
@@ -492,6 +492,159 @@ const SidebarMenuButton = React.forwardRef<
|
|
|
492
492
|
)
|
|
493
493
|
SidebarMenuButton.displayName = "SidebarMenuButton"
|
|
494
494
|
|
|
495
|
+
const SidebarMenuAction = React.forwardRef<
|
|
496
|
+
HTMLButtonElement,
|
|
497
|
+
React.ButtonHTMLAttributes<HTMLButtonElement> & { asChild?: boolean; showOnHover?: boolean }
|
|
498
|
+
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
|
499
|
+
if (asChild) {
|
|
500
|
+
const child = React.Children.only(props.children) as React.ReactElement<any>
|
|
501
|
+
return React.cloneElement(child, {
|
|
502
|
+
ref,
|
|
503
|
+
"data-sidebar": "menu-action",
|
|
504
|
+
className: cn(
|
|
505
|
+
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
|
506
|
+
// Increases the hit area of the button on mobile.
|
|
507
|
+
"after:absolute after:-inset-2 after:md:hidden",
|
|
508
|
+
"peer-data-[size=sm]/menu-button:top-1",
|
|
509
|
+
"peer-data-[size=default]/menu-button:top-1.5",
|
|
510
|
+
"peer-data-[size=lg]/menu-button:top-2.5",
|
|
511
|
+
"group-data-[collapsible=icon]:hidden",
|
|
512
|
+
showOnHover &&
|
|
513
|
+
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
|
514
|
+
className,
|
|
515
|
+
child.props.className
|
|
516
|
+
),
|
|
517
|
+
...props,
|
|
518
|
+
children: child.props.children
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
<button
|
|
524
|
+
ref={ref}
|
|
525
|
+
data-sidebar="menu-action"
|
|
526
|
+
className={cn(
|
|
527
|
+
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
|
528
|
+
// Increases the hit area of the button on mobile.
|
|
529
|
+
"after:absolute after:-inset-2 after:md:hidden",
|
|
530
|
+
"peer-data-[size=sm]/menu-button:top-1",
|
|
531
|
+
"peer-data-[size=default]/menu-button:top-1.5",
|
|
532
|
+
"peer-data-[size=lg]/menu-button:top-2.5",
|
|
533
|
+
"group-data-[collapsible=icon]:hidden",
|
|
534
|
+
showOnHover &&
|
|
535
|
+
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
|
536
|
+
className
|
|
537
|
+
)}
|
|
538
|
+
{...props}
|
|
539
|
+
/>
|
|
540
|
+
)
|
|
541
|
+
})
|
|
542
|
+
SidebarMenuAction.displayName = "SidebarMenuAction"
|
|
543
|
+
|
|
544
|
+
const SidebarMenuSub = React.forwardRef<
|
|
545
|
+
HTMLUListElement,
|
|
546
|
+
React.HTMLAttributes<HTMLUListElement>
|
|
547
|
+
>(({ className, ...props }, ref) => (
|
|
548
|
+
<ul
|
|
549
|
+
ref={ref}
|
|
550
|
+
data-sidebar="menu-sub"
|
|
551
|
+
className={cn(
|
|
552
|
+
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
|
|
553
|
+
"group-data-[collapsible=icon]:hidden",
|
|
554
|
+
className
|
|
555
|
+
)}
|
|
556
|
+
{...props}
|
|
557
|
+
/>
|
|
558
|
+
))
|
|
559
|
+
SidebarMenuSub.displayName = "SidebarMenuSub"
|
|
560
|
+
|
|
561
|
+
const SidebarMenuSubItem = React.forwardRef<
|
|
562
|
+
HTMLLIElement,
|
|
563
|
+
React.LiHTMLAttributes<HTMLLIElement>
|
|
564
|
+
>(({ ...props }, ref) => <li ref={ref} {...props} />)
|
|
565
|
+
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
|
|
566
|
+
|
|
567
|
+
const SidebarMenuSubButton = React.forwardRef<
|
|
568
|
+
HTMLAnchorElement,
|
|
569
|
+
React.AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
570
|
+
asChild?: boolean
|
|
571
|
+
size?: "sm" | "md"
|
|
572
|
+
isActive?: boolean
|
|
573
|
+
}
|
|
574
|
+
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
|
575
|
+
if (asChild) {
|
|
576
|
+
const child = React.Children.only(props.children) as React.ReactElement<any>
|
|
577
|
+
return React.cloneElement(child, {
|
|
578
|
+
ref,
|
|
579
|
+
"data-sidebar": "menu-sub-button",
|
|
580
|
+
"data-size": size,
|
|
581
|
+
"data-active": isActive,
|
|
582
|
+
className: cn(
|
|
583
|
+
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring 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 group-data-[collapsible=icon]:hidden",
|
|
584
|
+
size === "sm" && "text-xs",
|
|
585
|
+
size === "md" && "text-sm",
|
|
586
|
+
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
|
587
|
+
className,
|
|
588
|
+
child.props.className
|
|
589
|
+
),
|
|
590
|
+
...props,
|
|
591
|
+
children: child.props.children
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<a
|
|
597
|
+
ref={ref}
|
|
598
|
+
data-sidebar="menu-sub-button"
|
|
599
|
+
data-size={size}
|
|
600
|
+
data-active={isActive}
|
|
601
|
+
className={cn(
|
|
602
|
+
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring 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 group-data-[collapsible=icon]:hidden",
|
|
603
|
+
size === "sm" && "text-xs",
|
|
604
|
+
size === "md" && "text-sm",
|
|
605
|
+
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
|
606
|
+
className
|
|
607
|
+
)}
|
|
608
|
+
{...props}
|
|
609
|
+
/>
|
|
610
|
+
)
|
|
611
|
+
})
|
|
612
|
+
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
|
|
613
|
+
|
|
614
|
+
const SidebarInput = React.forwardRef<
|
|
615
|
+
HTMLInputElement,
|
|
616
|
+
React.InputHTMLAttributes<HTMLInputElement>
|
|
617
|
+
>(({ className, ...props }, ref) => {
|
|
618
|
+
return (
|
|
619
|
+
<input
|
|
620
|
+
ref={ref}
|
|
621
|
+
data-sidebar="input"
|
|
622
|
+
className={cn(
|
|
623
|
+
"flex h-8 w-full bg-background rounded-md px-3 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 group-data-[collapsible=icon]:w-0 group-data-[collapsible=icon]:p-0 group-data-[collapsible=icon]:border-0 group-data-[collapsible=icon]:hidden",
|
|
624
|
+
className
|
|
625
|
+
)}
|
|
626
|
+
{...props}
|
|
627
|
+
/>
|
|
628
|
+
)
|
|
629
|
+
})
|
|
630
|
+
SidebarInput.displayName = "SidebarInput"
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
const SidebarSeparator = React.forwardRef<
|
|
634
|
+
HTMLDivElement,
|
|
635
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
636
|
+
>(({ className, ...props }, ref) => {
|
|
637
|
+
return (
|
|
638
|
+
<div
|
|
639
|
+
ref={ref}
|
|
640
|
+
data-sidebar="separator"
|
|
641
|
+
className={cn("mx-2 w-auto bg-sidebar-border h-px", className)}
|
|
642
|
+
{...props}
|
|
643
|
+
/>
|
|
644
|
+
)
|
|
645
|
+
})
|
|
646
|
+
SidebarSeparator.displayName = "SidebarSeparator"
|
|
647
|
+
|
|
495
648
|
export {
|
|
496
649
|
Sidebar,
|
|
497
650
|
SidebarContent,
|
|
@@ -500,12 +653,18 @@ export {
|
|
|
500
653
|
SidebarGroupContent,
|
|
501
654
|
SidebarGroupLabel,
|
|
502
655
|
SidebarHeader,
|
|
656
|
+
SidebarInput,
|
|
503
657
|
SidebarInset,
|
|
504
658
|
SidebarMenu,
|
|
659
|
+
SidebarMenuAction,
|
|
505
660
|
SidebarMenuButton,
|
|
506
661
|
SidebarMenuItem,
|
|
662
|
+
SidebarMenuSub,
|
|
663
|
+
SidebarMenuSubButton,
|
|
664
|
+
SidebarMenuSubItem,
|
|
507
665
|
SidebarProvider,
|
|
508
666
|
SidebarRail,
|
|
667
|
+
SidebarSeparator,
|
|
509
668
|
SidebarTrigger,
|
|
510
669
|
useSidebar,
|
|
511
670
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useTheme } from "next-themes"
|
|
5
|
+
import { FiSun, FiMoon, FiMonitor } from "react-icons/fi"
|
|
6
|
+
|
|
7
|
+
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* ThemeSwitcher component for use within a DropdownMenu
|
|
11
|
+
* Toggles between light, dark, and system themes
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <DropdownMenu>
|
|
15
|
+
* <DropdownMenuContent>
|
|
16
|
+
* <ThemeSwitcher />
|
|
17
|
+
* </DropdownMenuContent>
|
|
18
|
+
* </DropdownMenu>
|
|
19
|
+
*/
|
|
20
|
+
export function ThemeSwitcher() {
|
|
21
|
+
const { setTheme, theme, resolvedTheme } = useTheme()
|
|
22
|
+
const [mounted, setMounted] = React.useState(false)
|
|
23
|
+
|
|
24
|
+
// Avoid hydration mismatch
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
setMounted(true)
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
const toggleTheme = () => {
|
|
30
|
+
// Cycle: light -> dark -> system -> light
|
|
31
|
+
if (theme === "light") {
|
|
32
|
+
setTheme("dark")
|
|
33
|
+
} else if (theme === "dark") {
|
|
34
|
+
setTheme("system")
|
|
35
|
+
} else {
|
|
36
|
+
setTheme("light")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getIcon = () => {
|
|
41
|
+
if (!mounted) {
|
|
42
|
+
return <FiSun className="mr-2 h-4 w-4" />
|
|
43
|
+
}
|
|
44
|
+
if (theme === "system") {
|
|
45
|
+
return <FiMonitor className="mr-2 h-4 w-4" />
|
|
46
|
+
}
|
|
47
|
+
if (resolvedTheme === "dark") {
|
|
48
|
+
return <FiMoon className="mr-2 h-4 w-4" />
|
|
49
|
+
}
|
|
50
|
+
return <FiSun className="mr-2 h-4 w-4" />
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const getLabel = () => {
|
|
54
|
+
if (!mounted) return "Theme"
|
|
55
|
+
if (theme === "system") return "System"
|
|
56
|
+
if (theme === "dark") return "Dark"
|
|
57
|
+
return "Light"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<DropdownMenuItem onClick={toggleTheme} closeOnSelect={false}>
|
|
62
|
+
{getIcon()}
|
|
63
|
+
<span>Theme: {getLabel()}</span>
|
|
64
|
+
</DropdownMenuItem>
|
|
65
|
+
)
|
|
66
|
+
}
|