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.
- package/README.md +151 -0
- package/package.json +75 -0
- package/src/cinematic-theme-switcher.tsx +303 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/dropdown-menu.tsx +189 -0
- package/src/index.ts +9 -0
- package/src/lib/utils.ts +6 -0
- package/src/sidebar-user-menu.tsx +202 -0
- package/src/theme-dropdown.tsx +238 -0
- package/src/theme-provider.tsx +9 -0
- package/src/theme-toggle.tsx +73 -0
- package/src/themes-shadcn.css +3057 -0
|
@@ -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';
|
package/src/lib/utils.ts
ADDED
|
@@ -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
|
+
}
|