@snapdragonsnursery/react-components 1.7.0 → 1.8.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapdragonsnursery/react-components",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -40,8 +40,14 @@
40
40
  "@headlessui/react": "^2.2.4",
41
41
  "@heroicons/react": "^2.2.0",
42
42
  "@popperjs/core": "^2.11.8",
43
+ "@radix-ui/react-avatar": "^1.1.10",
44
+ "@radix-ui/react-collapsible": "^1.1.12",
45
+ "@radix-ui/react-dialog": "^1.1.15",
46
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
43
47
  "@radix-ui/react-popover": "^1.1.14",
48
+ "@radix-ui/react-separator": "^1.1.7",
44
49
  "@radix-ui/react-slot": "^1.2.3",
50
+ "@radix-ui/react-tooltip": "^1.2.8",
45
51
  "@tanstack/react-table": "^8.21.3",
46
52
  "class-variance-authority": "^0.7.1",
47
53
  "clsx": "^2.1.1",
@@ -0,0 +1,205 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ AudioWaveform,
6
+ BookOpen,
7
+ Bot,
8
+ Command,
9
+ Frame,
10
+ GalleryVerticalEnd,
11
+ Map,
12
+ PieChart,
13
+ Settings2,
14
+ SquareTerminal,
15
+ } from "lucide-react"
16
+
17
+ import { NavMain } from "./nav-main"
18
+ import { NavProjects } from "./nav-projects"
19
+ import { NavUser } from "./nav-user"
20
+ import { TeamSwitcher } from "./team-switcher"
21
+ import { SiteSwitcher } from "./site-switcher"
22
+ import { RoomSwitcher } from "./room-switcher"
23
+ import { ThemeModeToggle } from "./theme-mode-toggle"
24
+ import {
25
+ Sidebar,
26
+ SidebarContent,
27
+ SidebarFooter,
28
+ SidebarHeader,
29
+ SidebarRail,
30
+ SidebarTrigger,
31
+ } from "./ui/sidebar"
32
+
33
+ // This is sample data.
34
+ const data = {
35
+ user: {
36
+ name: "shadcn",
37
+ email: "m@example.com",
38
+ avatar: "/avatars/shadcn.jpg",
39
+ },
40
+ teams: [
41
+ {
42
+ name: "Acme Inc",
43
+ logo: GalleryVerticalEnd,
44
+ plan: "Enterprise",
45
+ },
46
+ {
47
+ name: "Acme Corp.",
48
+ logo: AudioWaveform,
49
+ plan: "Startup",
50
+ },
51
+ {
52
+ name: "Evil Corp.",
53
+ logo: Command,
54
+ plan: "Free",
55
+ },
56
+ ],
57
+ navMain: [
58
+ {
59
+ title: "Playground",
60
+ url: "#",
61
+ icon: SquareTerminal,
62
+ isActive: true,
63
+ items: [
64
+ {
65
+ title: "History",
66
+ url: "#",
67
+ },
68
+ {
69
+ title: "Starred",
70
+ url: "#",
71
+ },
72
+ {
73
+ title: "Settings",
74
+ url: "#",
75
+ },
76
+ ],
77
+ },
78
+ {
79
+ title: "Models",
80
+ url: "#",
81
+ icon: Bot,
82
+ items: [
83
+ {
84
+ title: "Genesis",
85
+ url: "#",
86
+ },
87
+ {
88
+ title: "Explorer",
89
+ url: "#",
90
+ },
91
+ {
92
+ title: "Quantum",
93
+ url: "#",
94
+ },
95
+ ],
96
+ },
97
+ {
98
+ title: "Documentation",
99
+ url: "#",
100
+ icon: BookOpen,
101
+ items: [
102
+ {
103
+ title: "Introduction",
104
+ url: "#",
105
+ },
106
+ {
107
+ title: "Get Started",
108
+ url: "#",
109
+ },
110
+ {
111
+ title: "Tutorials",
112
+ url: "#",
113
+ },
114
+ {
115
+ title: "Changelog",
116
+ url: "#",
117
+ },
118
+ ],
119
+ },
120
+ {
121
+ title: "Settings",
122
+ url: "#",
123
+ icon: Settings2,
124
+ items: [
125
+ {
126
+ title: "General",
127
+ url: "#",
128
+ },
129
+ {
130
+ title: "Team",
131
+ url: "#",
132
+ },
133
+ {
134
+ title: "Billing",
135
+ url: "#",
136
+ },
137
+ {
138
+ title: "Limits",
139
+ url: "#",
140
+ },
141
+ ],
142
+ },
143
+ ],
144
+ projects: [
145
+ {
146
+ name: "Design Engineering",
147
+ url: "#",
148
+ icon: Frame,
149
+ },
150
+ {
151
+ name: "Sales & Marketing",
152
+ url: "#",
153
+ icon: PieChart,
154
+ },
155
+ {
156
+ name: "Travel",
157
+ url: "#",
158
+ icon: Map,
159
+ },
160
+ ],
161
+ }
162
+
163
+ export function AppSidebar({
164
+ sites,
165
+ activeSiteId,
166
+ onSiteChange,
167
+ sitesLoading,
168
+ rooms,
169
+ activeRoomId,
170
+ onRoomChange,
171
+ roomsLoading,
172
+ roomBaseColor,
173
+ navItems,
174
+ projects,
175
+ user,
176
+ ...props
177
+ }) {
178
+ return (
179
+ <Sidebar collapsible="icon" {...props}>
180
+ <SidebarHeader>
181
+ {sites?.length || sitesLoading ? (
182
+ <SiteSwitcher items={sites} activeId={activeSiteId} onChange={onSiteChange} isLoading={sitesLoading} />
183
+ ) : null}
184
+ {rooms?.length || roomsLoading ? (
185
+ <RoomSwitcher items={rooms} activeId={activeRoomId} onChange={onRoomChange} isLoading={roomsLoading} baseColor={roomBaseColor} />
186
+ ) : null}
187
+ {!sites?.length && !rooms?.length ? (
188
+ <TeamSwitcher teams={data.teams} />
189
+ ) : null}
190
+ </SidebarHeader>
191
+ <SidebarContent>
192
+ <NavMain items={navItems ?? data.navMain} />
193
+ <NavProjects projects={projects ?? data.projects} />
194
+ </SidebarContent>
195
+ <SidebarFooter>
196
+ <div className="flex items-center justify-end gap-1 group-data-[collapsible=icon]:flex-col group-data-[collapsible=icon]:items-end group-data-[collapsible=icon]:gap-2">
197
+ <ThemeModeToggle />
198
+ <SidebarTrigger aria-label="Toggle sidebar" />
199
+ </div>
200
+ <NavUser user={user ?? data.user} />
201
+ </SidebarFooter>
202
+ <SidebarRail />
203
+ </Sidebar>
204
+ );
205
+ }
@@ -0,0 +1,60 @@
1
+ import { ChevronRight } from "lucide-react";
2
+
3
+ import {
4
+ Collapsible,
5
+ CollapsibleContent,
6
+ CollapsibleTrigger,
7
+ } from "./ui/collapsible"
8
+ import {
9
+ SidebarGroup,
10
+ SidebarGroupLabel,
11
+ SidebarMenu,
12
+ SidebarMenuButton,
13
+ SidebarMenuItem,
14
+ SidebarMenuSub,
15
+ SidebarMenuSubButton,
16
+ SidebarMenuSubItem,
17
+ } from "./ui/sidebar"
18
+
19
+ export function NavMain({
20
+ items
21
+ }) {
22
+ return (
23
+ <SidebarGroup>
24
+ <SidebarGroupLabel>Platform</SidebarGroupLabel>
25
+ <SidebarMenu>
26
+ {items.map((item) => (
27
+ <Collapsible
28
+ key={item.title}
29
+ asChild
30
+ defaultOpen={item.isActive}
31
+ className="group/collapsible">
32
+ <SidebarMenuItem>
33
+ <CollapsibleTrigger asChild>
34
+ <SidebarMenuButton tooltip={item.title}>
35
+ {item.icon && <item.icon />}
36
+ <span>{item.title}</span>
37
+ <ChevronRight
38
+ className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
39
+ </SidebarMenuButton>
40
+ </CollapsibleTrigger>
41
+ <CollapsibleContent>
42
+ <SidebarMenuSub>
43
+ {item.items?.map((subItem) => (
44
+ <SidebarMenuSubItem key={subItem.title}>
45
+ <SidebarMenuSubButton asChild>
46
+ <a href={subItem.url}>
47
+ <span>{subItem.title}</span>
48
+ </a>
49
+ </SidebarMenuSubButton>
50
+ </SidebarMenuSubItem>
51
+ ))}
52
+ </SidebarMenuSub>
53
+ </CollapsibleContent>
54
+ </SidebarMenuItem>
55
+ </Collapsible>
56
+ ))}
57
+ </SidebarMenu>
58
+ </SidebarGroup>
59
+ );
60
+ }
@@ -0,0 +1,76 @@
1
+ "use client"
2
+
3
+ import { Folder, Forward, MoreHorizontal, Trash2 } from "lucide-react";
4
+
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuSeparator,
10
+ DropdownMenuTrigger,
11
+ } from "./ui/dropdown-menu"
12
+ import {
13
+ SidebarGroup,
14
+ SidebarGroupLabel,
15
+ SidebarMenu,
16
+ SidebarMenuAction,
17
+ SidebarMenuButton,
18
+ SidebarMenuItem,
19
+ useSidebar,
20
+ } from "./ui/sidebar"
21
+
22
+ export function NavProjects({
23
+ projects
24
+ }) {
25
+ const { isMobile } = useSidebar()
26
+
27
+ return (
28
+ <SidebarGroup className="group-data-[collapsible=icon]:hidden">
29
+ <SidebarGroupLabel>Projects</SidebarGroupLabel>
30
+ <SidebarMenu>
31
+ {projects.map((item) => (
32
+ <SidebarMenuItem key={item.name}>
33
+ <SidebarMenuButton asChild>
34
+ <a href={item.url}>
35
+ <item.icon />
36
+ <span>{item.name}</span>
37
+ </a>
38
+ </SidebarMenuButton>
39
+ <DropdownMenu>
40
+ <DropdownMenuTrigger asChild>
41
+ <SidebarMenuAction showOnHover>
42
+ <MoreHorizontal />
43
+ <span className="sr-only">More</span>
44
+ </SidebarMenuAction>
45
+ </DropdownMenuTrigger>
46
+ <DropdownMenuContent
47
+ className="w-48 rounded-lg"
48
+ side={isMobile ? "bottom" : "right"}
49
+ align={isMobile ? "end" : "start"}>
50
+ <DropdownMenuItem>
51
+ <Folder className="text-muted-foreground" />
52
+ <span>View Project</span>
53
+ </DropdownMenuItem>
54
+ <DropdownMenuItem>
55
+ <Forward className="text-muted-foreground" />
56
+ <span>Share Project</span>
57
+ </DropdownMenuItem>
58
+ <DropdownMenuSeparator />
59
+ <DropdownMenuItem>
60
+ <Trash2 className="text-muted-foreground" />
61
+ <span>Delete Project</span>
62
+ </DropdownMenuItem>
63
+ </DropdownMenuContent>
64
+ </DropdownMenu>
65
+ </SidebarMenuItem>
66
+ ))}
67
+ <SidebarMenuItem>
68
+ <SidebarMenuButton className="text-sidebar-foreground/70">
69
+ <MoreHorizontal className="text-sidebar-foreground/70" />
70
+ <span>More</span>
71
+ </SidebarMenuButton>
72
+ </SidebarMenuItem>
73
+ </SidebarMenu>
74
+ </SidebarGroup>
75
+ );
76
+ }
@@ -0,0 +1,104 @@
1
+ import {
2
+ BadgeCheck,
3
+ Bell,
4
+ ChevronsUpDown,
5
+ CreditCard,
6
+ LogOut,
7
+ Sparkles,
8
+ } from "lucide-react"
9
+
10
+ import {
11
+ Avatar,
12
+ AvatarFallback,
13
+ AvatarImage,
14
+ } from "./ui/avatar"
15
+ import {
16
+ DropdownMenu,
17
+ DropdownMenuContent,
18
+ DropdownMenuGroup,
19
+ DropdownMenuItem,
20
+ DropdownMenuLabel,
21
+ DropdownMenuSeparator,
22
+ DropdownMenuTrigger,
23
+ } from "./ui/dropdown-menu"
24
+ import {
25
+ SidebarMenu,
26
+ SidebarMenuButton,
27
+ SidebarMenuItem,
28
+ useSidebar,
29
+ } from "./ui/sidebar"
30
+
31
+ export function NavUser({
32
+ user
33
+ }) {
34
+ const { isMobile } = useSidebar()
35
+
36
+ return (
37
+ <SidebarMenu>
38
+ <SidebarMenuItem>
39
+ <DropdownMenu>
40
+ <DropdownMenuTrigger asChild>
41
+ <SidebarMenuButton
42
+ size="lg"
43
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
44
+ <Avatar className="h-8 w-8 rounded-lg">
45
+ <AvatarImage src={user.avatar} alt={user.name} />
46
+ <AvatarFallback className="rounded-lg">CN</AvatarFallback>
47
+ </Avatar>
48
+ <div className="grid flex-1 text-left text-sm leading-tight">
49
+ <span className="truncate font-semibold">{user.name}</span>
50
+ <span className="truncate text-xs">{user.email}</span>
51
+ </div>
52
+ <ChevronsUpDown className="ml-auto size-4" />
53
+ </SidebarMenuButton>
54
+ </DropdownMenuTrigger>
55
+ <DropdownMenuContent
56
+ className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
57
+ side={isMobile ? "bottom" : "right"}
58
+ align="end"
59
+ sideOffset={4}>
60
+ <DropdownMenuLabel className="p-0 font-normal">
61
+ <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
62
+ <Avatar className="h-8 w-8 rounded-lg">
63
+ <AvatarImage src={user.avatar} alt={user.name} />
64
+ <AvatarFallback className="rounded-lg">CN</AvatarFallback>
65
+ </Avatar>
66
+ <div className="grid flex-1 text-left text-sm leading-tight">
67
+ <span className="truncate font-semibold">{user.name}</span>
68
+ <span className="truncate text-xs">{user.email}</span>
69
+ </div>
70
+ </div>
71
+ </DropdownMenuLabel>
72
+ <DropdownMenuSeparator />
73
+ <DropdownMenuGroup>
74
+ <DropdownMenuItem>
75
+ <Sparkles />
76
+ Upgrade to Pro
77
+ </DropdownMenuItem>
78
+ </DropdownMenuGroup>
79
+ <DropdownMenuSeparator />
80
+ <DropdownMenuGroup>
81
+ <DropdownMenuItem>
82
+ <BadgeCheck />
83
+ Account
84
+ </DropdownMenuItem>
85
+ <DropdownMenuItem>
86
+ <CreditCard />
87
+ Billing
88
+ </DropdownMenuItem>
89
+ <DropdownMenuItem>
90
+ <Bell />
91
+ Notifications
92
+ </DropdownMenuItem>
93
+ </DropdownMenuGroup>
94
+ <DropdownMenuSeparator />
95
+ <DropdownMenuItem>
96
+ <LogOut />
97
+ Log out
98
+ </DropdownMenuItem>
99
+ </DropdownMenuContent>
100
+ </DropdownMenu>
101
+ </SidebarMenuItem>
102
+ </SidebarMenu>
103
+ );
104
+ }
@@ -0,0 +1,116 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronsUpDown, DoorOpen } from "lucide-react"
5
+
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuLabel,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuTrigger,
13
+ } from "./ui/dropdown-menu"
14
+ import {
15
+ SidebarMenu,
16
+ SidebarMenuButton,
17
+ SidebarMenuItem,
18
+ useSidebar,
19
+ } from "./ui/sidebar"
20
+
21
+ // RoomSwitcher: shows a list of rooms with custom icons; no "Add" entry.
22
+ // Props:
23
+ // - items: Array<{ id?: string, name: string, icon?: React.ComponentType<any> }>
24
+ // - activeId?: string
25
+ // - onChange?: (item) => void
26
+ // - label?: string (default: "Rooms")
27
+ export function RoomSwitcher({ items = [], activeId, onChange, label = "Rooms", isLoading = false, baseColor }) {
28
+ const { isMobile } = useSidebar()
29
+ const initial = React.useMemo(() => {
30
+ if (!items?.length) return null
31
+ return activeId
32
+ ? items.find((i) => i.id === activeId) ?? items[0]
33
+ : items[0]
34
+ }, [items, activeId])
35
+ const [active, setActive] = React.useState(initial)
36
+
37
+ React.useEffect(() => {
38
+ if (!items?.length) return
39
+ if (activeId) {
40
+ const next = items.find((i) => i.id === activeId)
41
+ if (next && next !== active) setActive(next)
42
+ }
43
+ }, [activeId, items])
44
+
45
+ if (isLoading) {
46
+ return (
47
+ <SidebarMenu>
48
+ <SidebarMenuItem>
49
+ <SidebarMenuButton size="lg" className="opacity-60">
50
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary/60 text-sidebar-primary-foreground" />
51
+ <div className="grid flex-1 text-left text-sm leading-tight">
52
+ <span className="truncate font-semibold">Loading…</span>
53
+ <span className="truncate text-xs">{label}</span>
54
+ </div>
55
+ </SidebarMenuButton>
56
+ </SidebarMenuItem>
57
+ </SidebarMenu>
58
+ )
59
+ }
60
+
61
+ if (!active || !items?.length) return null
62
+
63
+ const ActiveIcon = active.icon || DoorOpen
64
+
65
+ return (
66
+ <SidebarMenu>
67
+ <SidebarMenuItem>
68
+ <DropdownMenu>
69
+ <DropdownMenuTrigger asChild>
70
+ <SidebarMenuButton
71
+ size="lg"
72
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
73
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground" style={baseColor ? { backgroundColor: baseColor } : undefined}>
74
+ <ActiveIcon className="size-4" />
75
+ </div>
76
+ <div className="grid flex-1 text-left text-sm leading-tight">
77
+ <span className="truncate font-semibold">{active.name}</span>
78
+ <span className="truncate text-xs">{label}</span>
79
+ </div>
80
+ <ChevronsUpDown className="ml-auto" />
81
+ </SidebarMenuButton>
82
+ </DropdownMenuTrigger>
83
+ <DropdownMenuContent
84
+ className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
85
+ align="start"
86
+ side={isMobile ? "bottom" : "right"}
87
+ sideOffset={4}>
88
+ <DropdownMenuLabel className="text-xs text-muted-foreground">
89
+ {label}
90
+ </DropdownMenuLabel>
91
+ {items.map((room) => {
92
+ const Icon = room.icon || DoorOpen
93
+ return (
94
+ <DropdownMenuItem
95
+ key={room.id ?? room.name}
96
+ onClick={() => {
97
+ setActive(room)
98
+ onChange?.(room)
99
+ }}
100
+ className="gap-2 p-2">
101
+ <div className="flex size-6 items-center justify-center rounded-sm border">
102
+ <Icon className="size-4 shrink-0" />
103
+ </div>
104
+ {room.name}
105
+ </DropdownMenuItem>
106
+ )
107
+ })}
108
+ <DropdownMenuSeparator />
109
+ </DropdownMenuContent>
110
+ </DropdownMenu>
111
+ </SidebarMenuItem>
112
+ </SidebarMenu>
113
+ )
114
+ }
115
+
116
+ export default RoomSwitcher
@@ -0,0 +1,116 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ChevronsUpDown, Building2 } from "lucide-react"
5
+
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuLabel,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuTrigger,
13
+ } from "./ui/dropdown-menu"
14
+ import {
15
+ SidebarMenu,
16
+ SidebarMenuButton,
17
+ SidebarMenuItem,
18
+ useSidebar,
19
+ } from "./ui/sidebar"
20
+
21
+ // SiteSwitcher: shows a list of sites with custom icons; no "Add" entry.
22
+ // Props:
23
+ // - items: Array<{ id?: string, name: string, icon?: React.ComponentType<any> }>
24
+ // - activeId?: string
25
+ // - onChange?: (item) => void
26
+ // - label?: string (default: "Sites")
27
+ export function SiteSwitcher({ items = [], activeId, onChange, label = "Sites", isLoading = false }) {
28
+ const { isMobile } = useSidebar()
29
+ const initial = React.useMemo(() => {
30
+ if (!items?.length) return null
31
+ return activeId
32
+ ? items.find((i) => i.id === activeId) ?? items[0]
33
+ : items[0]
34
+ }, [items, activeId])
35
+ const [active, setActive] = React.useState(initial)
36
+
37
+ React.useEffect(() => {
38
+ if (!items?.length) return
39
+ if (activeId) {
40
+ const next = items.find((i) => i.id === activeId)
41
+ if (next && next !== active) setActive(next)
42
+ }
43
+ }, [activeId, items])
44
+
45
+ if (isLoading) {
46
+ return (
47
+ <SidebarMenu>
48
+ <SidebarMenuItem>
49
+ <SidebarMenuButton size="lg" className="opacity-60">
50
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary/60 text-sidebar-primary-foreground" />
51
+ <div className="grid flex-1 text-left text-sm leading-tight">
52
+ <span className="truncate font-semibold">Loading…</span>
53
+ <span className="truncate text-xs">{label}</span>
54
+ </div>
55
+ </SidebarMenuButton>
56
+ </SidebarMenuItem>
57
+ </SidebarMenu>
58
+ )
59
+ }
60
+
61
+ if (!active || !items?.length) return null
62
+
63
+ const ActiveIcon = active.icon || active.logo || Building2
64
+
65
+ return (
66
+ <SidebarMenu>
67
+ <SidebarMenuItem>
68
+ <DropdownMenu>
69
+ <DropdownMenuTrigger asChild>
70
+ <SidebarMenuButton
71
+ size="lg"
72
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground">
73
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
74
+ <ActiveIcon className="size-4" />
75
+ </div>
76
+ <div className="grid flex-1 text-left text-sm leading-tight">
77
+ <span className="truncate font-semibold">{active.name}</span>
78
+ <span className="truncate text-xs">{label}</span>
79
+ </div>
80
+ <ChevronsUpDown className="ml-auto" />
81
+ </SidebarMenuButton>
82
+ </DropdownMenuTrigger>
83
+ <DropdownMenuContent
84
+ className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
85
+ align="start"
86
+ side={isMobile ? "bottom" : "right"}
87
+ sideOffset={4}>
88
+ <DropdownMenuLabel className="text-xs text-muted-foreground">
89
+ {label}
90
+ </DropdownMenuLabel>
91
+ {items.map((site) => {
92
+ const Icon = site.icon || site.logo || Building2
93
+ return (
94
+ <DropdownMenuItem
95
+ key={site.id ?? site.name}
96
+ onClick={() => {
97
+ setActive(site)
98
+ onChange?.(site)
99
+ }}
100
+ className="gap-2 p-2">
101
+ <div className="flex size-6 items-center justify-center rounded-sm border">
102
+ <Icon className="size-4 shrink-0" />
103
+ </div>
104
+ {site.name}
105
+ </DropdownMenuItem>
106
+ )
107
+ })}
108
+ <DropdownMenuSeparator />
109
+ </DropdownMenuContent>
110
+ </DropdownMenu>
111
+ </SidebarMenuItem>
112
+ </SidebarMenu>
113
+ )
114
+ }
115
+
116
+ export default SiteSwitcher