@srcroot/ui 0.0.43 → 0.0.45

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.
@@ -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
+ }