shadcn-theme-menu 1.1.2

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,189 @@
1
+ import * as React from "react"
2
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
3
+ import { Check, ChevronRight, Circle } from "lucide-react"
4
+ import { cn } from "../../lib/utils"
5
+
6
+ const DropdownMenu = DropdownMenuPrimitive.Root
7
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
8
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group
9
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
10
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub
11
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
12
+
13
+ const DropdownMenuSubTrigger = React.forwardRef<
14
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
15
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
16
+ inset?: boolean
17
+ }
18
+ >(({ className, inset, children, ...props }, ref) => (
19
+ <DropdownMenuPrimitive.SubTrigger
20
+ ref={ref}
21
+ className={cn(
22
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
23
+ inset && "pl-8",
24
+ className
25
+ )}
26
+ {...props}
27
+ >
28
+ {children}
29
+ <ChevronRight className="ml-auto h-4 w-4" />
30
+ </DropdownMenuPrimitive.SubTrigger>
31
+ ))
32
+ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
33
+
34
+ const DropdownMenuSubContent = React.forwardRef<
35
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
36
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
37
+ >(({ className, ...props }, ref) => (
38
+ <DropdownMenuPrimitive.SubContent
39
+ ref={ref}
40
+ className={cn(
41
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ ))
47
+ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
48
+
49
+ const DropdownMenuContent = React.forwardRef<
50
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
51
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
52
+ >(({ className, sideOffset = 4, ...props }, ref) => (
53
+ <DropdownMenuPrimitive.Portal>
54
+ <DropdownMenuPrimitive.Content
55
+ ref={ref}
56
+ sideOffset={sideOffset}
57
+ className={cn(
58
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ </DropdownMenuPrimitive.Portal>
64
+ ))
65
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
66
+
67
+ const DropdownMenuItem = React.forwardRef<
68
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
69
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
70
+ inset?: boolean
71
+ }
72
+ >(({ className, inset, ...props }, ref) => (
73
+ <DropdownMenuPrimitive.Item
74
+ ref={ref}
75
+ className={cn(
76
+ "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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
77
+ inset && "pl-8",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ ))
83
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
84
+
85
+ const DropdownMenuCheckboxItem = React.forwardRef<
86
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
87
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
88
+ >(({ className, children, checked, ...props }, ref) => (
89
+ <DropdownMenuPrimitive.CheckboxItem
90
+ ref={ref}
91
+ className={cn(
92
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
93
+ className
94
+ )}
95
+ checked={checked}
96
+ {...props}
97
+ >
98
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
99
+ <DropdownMenuPrimitive.ItemIndicator>
100
+ <Check className="h-4 w-4" />
101
+ </DropdownMenuPrimitive.ItemIndicator>
102
+ </span>
103
+ {children}
104
+ </DropdownMenuPrimitive.CheckboxItem>
105
+ ))
106
+ DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
107
+
108
+ const DropdownMenuRadioItem = React.forwardRef<
109
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
110
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
111
+ >(({ className, children, ...props }, ref) => (
112
+ <DropdownMenuPrimitive.RadioItem
113
+ ref={ref}
114
+ className={cn(
115
+ "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
116
+ className
117
+ )}
118
+ {...props}
119
+ >
120
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
121
+ <DropdownMenuPrimitive.ItemIndicator>
122
+ <Circle className="h-2 w-2 fill-current" />
123
+ </DropdownMenuPrimitive.ItemIndicator>
124
+ </span>
125
+ {children}
126
+ </DropdownMenuPrimitive.RadioItem>
127
+ ))
128
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
129
+
130
+ const DropdownMenuLabel = React.forwardRef<
131
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
132
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
133
+ inset?: boolean
134
+ }
135
+ >(({ className, inset, ...props }, ref) => (
136
+ <DropdownMenuPrimitive.Label
137
+ ref={ref}
138
+ className={cn(
139
+ "px-2 py-1.5 text-sm font-semibold",
140
+ inset && "pl-8",
141
+ className
142
+ )}
143
+ {...props}
144
+ />
145
+ ))
146
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
147
+
148
+ const DropdownMenuSeparator = React.forwardRef<
149
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
150
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
151
+ >(({ className, ...props }, ref) => (
152
+ <DropdownMenuPrimitive.Separator
153
+ ref={ref}
154
+ className={cn("-mx-1 my-1 h-px bg-muted", className)}
155
+ {...props}
156
+ />
157
+ ))
158
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
159
+
160
+ const DropdownMenuShortcut = ({
161
+ className,
162
+ ...props
163
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
164
+ return (
165
+ <span
166
+ className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
167
+ {...props}
168
+ />
169
+ )
170
+ }
171
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
172
+
173
+ export {
174
+ DropdownMenu,
175
+ DropdownMenuTrigger,
176
+ DropdownMenuContent,
177
+ DropdownMenuItem,
178
+ DropdownMenuCheckboxItem,
179
+ DropdownMenuRadioItem,
180
+ DropdownMenuLabel,
181
+ DropdownMenuSeparator,
182
+ DropdownMenuShortcut,
183
+ DropdownMenuGroup,
184
+ DropdownMenuPortal,
185
+ DropdownMenuSub,
186
+ DropdownMenuSubContent,
187
+ DropdownMenuSubTrigger,
188
+ DropdownMenuRadioGroup,
189
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // Theme Components
2
+ export { ThemeProvider } from './theme-provider';
3
+ export { ThemeToggle } from './theme-toggle';
4
+ export { ThemeDropdown, themeNames, themeColors, formatThemeName } from './theme-dropdown';
5
+ export { default as CinematicThemeSwitcher } from './cinematic-theme-switcher';
6
+ export { SidebarUserMenu } from './sidebar-user-menu';
7
+
8
+ // Types
9
+ export type { ThemeProviderProps } from 'next-themes';
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,202 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useRouter } from "next/navigation"
5
+ import { useSession, signOut } from "next-auth/react"
6
+ import {
7
+ ChevronUp,
8
+ User2,
9
+ LogOut,
10
+ CreditCard,
11
+ Bell,
12
+ Palette
13
+ } from 'lucide-react'
14
+ import {
15
+ SidebarMenu,
16
+ SidebarMenuButton,
17
+ SidebarMenuItem,
18
+ } from "@/components/ui/sidebar"
19
+ import {
20
+ DropdownMenu,
21
+ DropdownMenuContent,
22
+ DropdownMenuItem,
23
+ DropdownMenuSeparator,
24
+ DropdownMenuTrigger,
25
+ DropdownMenuSub,
26
+ DropdownMenuSubTrigger,
27
+ DropdownMenuSubContent,
28
+ DropdownMenuPortal,
29
+ } from "@/components/ui/dropdown-menu"
30
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
31
+ import CinematicThemeSwitcher from "./cinematic-theme-switcher"
32
+ import { themeNames, themeColors, formatThemeName } from "./theme-dropdown"
33
+
34
+ interface SettingsDialogProps {
35
+ trigger: React.ReactNode;
36
+ }
37
+
38
+ function SettingsDialog({ trigger }: SettingsDialogProps) {
39
+ return <>{trigger}</>;
40
+ }
41
+
42
+ export function SidebarUserMenu() {
43
+ const router = useRouter()
44
+ const { data: session } = useSession()
45
+ const user = session?.user || { name: "Guest User", email: "guest@example.com", image: null }
46
+ const [colorTheme, setColorTheme] = React.useState("modern-minimal")
47
+ const [mounted, setMounted] = React.useState(false)
48
+ const [previewTheme, setPreviewTheme] = React.useState<string | null>(null)
49
+
50
+ React.useEffect(() => {
51
+ setMounted(true)
52
+ const saved = localStorage.getItem("color-theme")
53
+ if (saved && themeNames.includes(saved)) {
54
+ setColorTheme(saved)
55
+ }
56
+ }, [])
57
+
58
+ const handleThemeChange = (newTheme: string) => {
59
+ setColorTheme(newTheme)
60
+ localStorage.setItem("color-theme", newTheme)
61
+ document.cookie = `color-theme=${newTheme}; path=/; max-age=31536000`
62
+ themeNames.forEach(t => document.documentElement.classList.remove(`theme-${t}`))
63
+ document.documentElement.classList.add(`theme-${newTheme}`)
64
+ setPreviewTheme(null)
65
+ }
66
+
67
+ const handleThemePreview = (themeName: string) => {
68
+ setPreviewTheme(themeName)
69
+ themeNames.forEach(t => document.documentElement.classList.remove(`theme-${t}`))
70
+ document.documentElement.classList.add(`theme-${themeName}`)
71
+ }
72
+
73
+ const handlePreviewEnd = () => {
74
+ if (previewTheme) {
75
+ themeNames.forEach(t => document.documentElement.classList.remove(`theme-${t}`))
76
+ document.documentElement.classList.add(`theme-${colorTheme}`)
77
+ setPreviewTheme(null)
78
+ }
79
+ }
80
+
81
+ const userInitials = user.name
82
+ ?.split(" ")
83
+ .map((n: string) => n[0])
84
+ .join("")
85
+ .toUpperCase()
86
+ .substring(0, 2) || "GU"
87
+
88
+ return (
89
+ <SidebarMenu>
90
+ <SidebarMenuItem>
91
+ <DropdownMenu onOpenChange={(open) => !open && handlePreviewEnd()}>
92
+ <DropdownMenuTrigger asChild>
93
+ <SidebarMenuButton
94
+ size="lg"
95
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
96
+ >
97
+ <Avatar className="h-8 w-8 rounded-lg">
98
+ <AvatarImage src={user.image || undefined} alt={user.name || "User"} />
99
+ <AvatarFallback className="rounded-lg">{userInitials}</AvatarFallback>
100
+ </Avatar>
101
+ <div className="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
102
+ <span className="truncate font-semibold">{user.name || "Guest"}</span>
103
+ <span className="truncate text-xs text-muted-foreground">{user.email}</span>
104
+ </div>
105
+ <ChevronUp className="ml-2 size-4 group-data-[collapsible=icon]:hidden" />
106
+ </SidebarMenuButton>
107
+ </DropdownMenuTrigger>
108
+ <DropdownMenuContent
109
+ className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
110
+ side="bottom"
111
+ align="end"
112
+ sideOffset={4}
113
+ >
114
+ <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
115
+ <Avatar className="h-8 w-8 rounded-lg">
116
+ <AvatarImage src={user.image || undefined} alt={user.name || "User"} />
117
+ <AvatarFallback className="rounded-lg">{userInitials}</AvatarFallback>
118
+ </Avatar>
119
+ <div className="grid flex-1 text-left text-sm leading-tight">
120
+ <span className="truncate font-semibold">{user.name || "Guest"}</span>
121
+ <span className="truncate text-xs text-muted-foreground">{user.email}</span>
122
+ </div>
123
+ </div>
124
+ <DropdownMenuSeparator />
125
+ <SettingsDialog
126
+ trigger={
127
+ <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
128
+ <User2 className="mr-2 h-4 w-4" />
129
+ Profile
130
+ </DropdownMenuItem>
131
+ }
132
+ />
133
+ <DropdownMenuItem>
134
+ <CreditCard className="mr-2 h-4 w-4" />
135
+ Billing
136
+ </DropdownMenuItem>
137
+ <DropdownMenuItem>
138
+ <Bell className="mr-2 h-4 w-4" />
139
+ Notifications
140
+ </DropdownMenuItem>
141
+ <DropdownMenuSeparator />
142
+ <DropdownMenuSub>
143
+ <DropdownMenuSubTrigger>
144
+ <Palette className="mr-2 h-4 w-4" />
145
+ <span>Color Theme</span>
146
+ </DropdownMenuSubTrigger>
147
+ <DropdownMenuPortal>
148
+ <DropdownMenuSubContent className="max-h-[400px] overflow-y-auto">
149
+ <div className="px-2 py-1.5 flex items-center justify-center">
150
+ <CinematicThemeSwitcher />
151
+ </div>
152
+ <DropdownMenuSeparator />
153
+ {themeNames.map((themeName) => {
154
+ const colors = themeColors[themeName];
155
+ return (
156
+ <DropdownMenuItem
157
+ key={themeName}
158
+ onClick={() => handleThemeChange(themeName)}
159
+ onMouseEnter={() => handleThemePreview(themeName)}
160
+ onMouseLeave={handlePreviewEnd}
161
+ className={colorTheme === themeName ? "bg-accent" : ""}
162
+ >
163
+ <div className="flex items-center justify-between w-full">
164
+ <div className="flex items-center gap-2">
165
+ {colors && (
166
+ <div className="flex items-center gap-1">
167
+ <div
168
+ className="w-3 h-3 rounded-full border border-border"
169
+ style={{ backgroundColor: colors.primary }}
170
+ />
171
+ <div
172
+ className="w-3 h-3 rounded-full border border-border"
173
+ style={{ backgroundColor: colors.secondary }}
174
+ />
175
+ </div>
176
+ )}
177
+ <span>{formatThemeName(themeName)}</span>
178
+ </div>
179
+ {colorTheme === themeName && (
180
+ <span className="text-xs">✓</span>
181
+ )}
182
+ </div>
183
+ </DropdownMenuItem>
184
+ );
185
+ })}
186
+ </DropdownMenuSubContent>
187
+ </DropdownMenuPortal>
188
+ </DropdownMenuSub>
189
+ <DropdownMenuSeparator />
190
+ <DropdownMenuItem onClick={async () => {
191
+ await signOut()
192
+ router.push("/")
193
+ }}>
194
+ <LogOut className="mr-2 h-4 w-4" />
195
+ Sign out
196
+ </DropdownMenuItem>
197
+ </DropdownMenuContent>
198
+ </DropdownMenu>
199
+ </SidebarMenuItem>
200
+ </SidebarMenu>
201
+ )
202
+ }
@@ -0,0 +1,238 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import { Moon, Sun, Palette, Monitor } from "lucide-react"
5
+ import { Button } from "./components/ui/button"
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuLabel,
11
+ DropdownMenuSeparator,
12
+ DropdownMenuTrigger,
13
+ } from "./components/ui/dropdown-menu"
14
+ import { useTheme } from "next-themes"
15
+
16
+ export const themeNames = [
17
+ "modern-minimal",
18
+ "elegant-luxury",
19
+ "cyberpunk",
20
+ "twitter",
21
+ "mocha-mousse",
22
+ "bubblegum",
23
+ "amethyst-haze",
24
+ "pink-lemonade",
25
+ "notebook",
26
+ "doom-64",
27
+ "catppuccin",
28
+ "graphite",
29
+ "perpetuity",
30
+ "kodama-grove",
31
+ "cosmic-night",
32
+ "tangerine",
33
+ "quantum-rose",
34
+ "nature",
35
+ "bold-tech",
36
+ "amber-minimal",
37
+ "supabase",
38
+ "neo-brutalism",
39
+ "solar-dusk",
40
+ "claymorphism",
41
+ "pastel-dreams"
42
+ ];
43
+
44
+ export const themeColors: Record<string, { primary: string; secondary: string }> = {
45
+ "modern-minimal": { primary: "#3b82f6", secondary: "#f3f4f6" },
46
+ "elegant-luxury": { primary: "#9b2c2c", secondary: "#fdf2d6" },
47
+ "cyberpunk": { primary: "#ff00c8", secondary: "#f0f0ff" },
48
+ "twitter": { primary: "#1e9df1", secondary: "#0f1419" },
49
+ "mocha-mousse": { primary: "#A37764", secondary: "#BAAB92" },
50
+ "bubblegum": { primary: "#d04f99", secondary: "#8acfd1" },
51
+ "amethyst-haze": { primary: "#8a79ab", secondary: "#dfd9ec" },
52
+ "pink-lemonade": { primary: "#a84370", secondary: "#f1c4e6" },
53
+ "notebook": { primary: "#606060", secondary: "#dedede" },
54
+ "doom-64": { primary: "#b71c1c", secondary: "#556b2f" },
55
+ "catppuccin": { primary: "#8839ef", secondary: "#ccd0da" },
56
+ "graphite": { primary: "#606060", secondary: "#e0e0e0" },
57
+ "perpetuity": { primary: "#06858e", secondary: "#d9eaea" },
58
+ "kodama-grove": { primary: "#8d9d4f", secondary: "#decea0" },
59
+ "cosmic-night": { primary: "#6e56cf", secondary: "#e4dfff" },
60
+ "tangerine": { primary: "#e05d38", secondary: "#f3f4f6" },
61
+ "quantum-rose": { primary: "#e6067a", secondary: "#ffd6ff" },
62
+ "nature": { primary: "#2e7d32", secondary: "#e8f5e9" },
63
+ "bold-tech": { primary: "#8b5cf6", secondary: "#f3f0ff" },
64
+ "amber-minimal": { primary: "#f59e0b", secondary: "#f3f4f6" },
65
+ "supabase": { primary: "#72e3ad", secondary: "#fdfdfd" },
66
+ "neo-brutalism": { primary: "#ff3333", secondary: "#ffff00" },
67
+ "solar-dusk": { primary: "#B45309", secondary: "#E4C090" },
68
+ "claymorphism": { primary: "#6366f1", secondary: "#d6d3d1" },
69
+ "pastel-dreams": { primary: "#a78bfa", secondary: "#e9d8fd" }
70
+ };
71
+
72
+ export const formatThemeName = (name: string) => {
73
+ return name.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
74
+ }
75
+
76
+
77
+ interface ThemeDropdownProps {
78
+ Button?: React.ComponentType<any>
79
+ DropdownMenu?: any
80
+ iconSrc?: string
81
+ onColorThemeChange?: (theme: string) => void
82
+ onModeChange?: (mode: string) => void
83
+ }
84
+
85
+ export function ThemeDropdown({
86
+ Button: CustomButton,
87
+ DropdownMenu: CustomDropdownMenu,
88
+ iconSrc,
89
+ onColorThemeChange,
90
+ onModeChange
91
+ }: ThemeDropdownProps = {}) {
92
+ const { theme, setTheme } = useTheme()
93
+ const [colorTheme, setColorTheme] = useState("modern-minimal")
94
+ const [mounted, setMounted] = useState(false)
95
+ const [previewTheme, setPreviewTheme] = useState<string | null>(null)
96
+
97
+ const ButtonComp = CustomButton || Button
98
+ const MenuComp = CustomDropdownMenu || {
99
+ Root: DropdownMenu,
100
+ Trigger: DropdownMenuTrigger,
101
+ Content: DropdownMenuContent,
102
+ Item: DropdownMenuItem,
103
+ Label: DropdownMenuLabel,
104
+ Separator: DropdownMenuSeparator
105
+ }
106
+
107
+ useEffect(() => {
108
+ setMounted(true)
109
+ const saved = localStorage.getItem("color-theme")
110
+ if (saved && themeNames.includes(saved)) {
111
+ setColorTheme(saved)
112
+ }
113
+ }, [])
114
+
115
+ const handleThemeChange = (newTheme: string) => {
116
+ setColorTheme(newTheme)
117
+ localStorage.setItem("color-theme", newTheme)
118
+ document.cookie = `color-theme=${newTheme}; path=/; max-age=31536000`
119
+
120
+ // Remove all theme classes
121
+ themeNames.forEach(t => document.documentElement.classList.remove(`theme-${t}`))
122
+ // Add new theme class
123
+ document.documentElement.classList.add(`theme-${newTheme}`)
124
+
125
+ setPreviewTheme(null)
126
+ onColorThemeChange?.(newTheme)
127
+ }
128
+
129
+ const handleModeChange = (mode: string) => {
130
+ setTheme(mode)
131
+ onModeChange?.(mode)
132
+ }
133
+
134
+ const handleThemePreview = (themeName: string) => {
135
+ setPreviewTheme(themeName)
136
+ // Remove all theme classes
137
+ themeNames.forEach(t => document.documentElement.classList.remove(`theme-${t}`))
138
+ // Add preview theme class
139
+ document.documentElement.classList.add(`theme-${themeName}`)
140
+ }
141
+
142
+ const handlePreviewEnd = () => {
143
+ if (previewTheme) {
144
+ // Restore the actual selected theme
145
+ themeNames.forEach(t => document.documentElement.classList.remove(`theme-${t}`))
146
+ document.documentElement.classList.add(`theme-${colorTheme}`)
147
+ setPreviewTheme(null)
148
+ }
149
+ }
150
+
151
+ const formatThemeName = (name: string) => {
152
+ return name.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
153
+ }
154
+
155
+ if (!mounted) {
156
+ return null
157
+ }
158
+
159
+ const Root = MenuComp.Root || MenuComp
160
+ const Trigger = MenuComp.Trigger || DropdownMenuTrigger
161
+ const Content = MenuComp.Content || DropdownMenuContent
162
+ const Item = MenuComp.Item || DropdownMenuItem
163
+ const Label = MenuComp.Label || DropdownMenuLabel
164
+ const Separator = MenuComp.Separator || DropdownMenuSeparator
165
+
166
+ return (
167
+ <Root onOpenChange={(open: boolean) => !open && handlePreviewEnd()}>
168
+ <Trigger asChild>
169
+ <ButtonComp variant="ghost" size="icon" className="relative">
170
+ {iconSrc ? (
171
+ <img src={iconSrc} alt="Themes" width={32} height={32} />
172
+ ) : (
173
+ <Palette className="h-5 w-5" />
174
+ )}
175
+ </ButtonComp>
176
+ </Trigger>
177
+ <Content align="end" className="w-56 max-h-[400px] overflow-y-auto">
178
+ <Label>Appearance</Label>
179
+ <Separator />
180
+ <Item onClick={() => handleModeChange("light")} className="cursor-pointer py-1 h-7">
181
+ <Sun className="mr-2 h-3.5 w-3.5" />
182
+ <span className="text-sm">Light</span>
183
+ {theme === "light" && <span className="ml-auto text-xs">✓</span>}
184
+ </Item>
185
+ <Item onClick={() => handleModeChange("dark")} className="cursor-pointer py-1 h-7">
186
+ <Moon className="mr-2 h-3.5 w-3.5" />
187
+ <span className="text-sm">Dark</span>
188
+ {theme === "dark" && <span className="ml-auto text-xs">✓</span>}
189
+ </Item>
190
+ <Item onClick={() => handleModeChange("system")} className="cursor-pointer py-1 h-7">
191
+ <Monitor className="mr-2 h-3.5 w-3.5" />
192
+ <span className="text-sm">System</span>
193
+ {theme === "system" && <span className="ml-auto text-xs">✓</span>}
194
+ </Item>
195
+ <Separator />
196
+ <Label>Color Theme</Label>
197
+ <div className="text-xs text-muted-foreground px-2 py-1.5">
198
+ Current: {formatThemeName(colorTheme)}
199
+ </div>
200
+ <Separator />
201
+ {themeNames.map((themeName) => {
202
+ const colors = themeColors[themeName];
203
+ return (
204
+ <Item
205
+ key={themeName}
206
+ onClick={() => handleThemeChange(themeName)}
207
+ onMouseEnter={() => handleThemePreview(themeName)}
208
+ onMouseLeave={handlePreviewEnd}
209
+ className={`cursor-pointer ${colorTheme === themeName ? "bg-accent" : ""
210
+ }`}
211
+ >
212
+ <div className="flex items-center justify-between w-full">
213
+ <div className="flex items-center gap-2">
214
+ {colors && (
215
+ <div className="flex items-center gap-1">
216
+ <div
217
+ className="w-3 h-3 rounded-full border border-border"
218
+ style={{ backgroundColor: colors.primary }}
219
+ />
220
+ <div
221
+ className="w-3 h-3 rounded-full border border-border"
222
+ style={{ backgroundColor: colors.secondary }}
223
+ />
224
+ </div>
225
+ )}
226
+ <span>{formatThemeName(themeName)}</span>
227
+ </div>
228
+ {colorTheme === themeName && (
229
+ <span className="text-xs">✓</span>
230
+ )}
231
+ </div>
232
+ </Item>
233
+ );
234
+ })}
235
+ </Content>
236
+ </Root>
237
+ )
238
+ }
@@ -0,0 +1,9 @@
1
+ 'use client'
2
+ import {
3
+ ThemeProvider as NextThemesProvider,
4
+ type ThemeProviderProps,
5
+ } from 'next-themes'
6
+
7
+ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
9
+ }