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 ADDED
@@ -0,0 +1,151 @@
1
+ # shadcn-themes
2
+
3
+ Beautiful theme components for shadcn/ui with 24+ color themes, dark/light mode, and animations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # The package includes all required dependencies
9
+ pnpm add shadcn-themes
10
+
11
+ # Peer dependencies (usually already in your project)
12
+ pnpm add react react-dom next-themes lucide-react
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```tsx
18
+ // 1. Import CSS
19
+ import 'shadcn-themes/themes.css';
20
+
21
+ // 2. Wrap app with ThemeProvider
22
+ import { ThemeProvider } from 'shadcn-themes';
23
+
24
+ <ThemeProvider attribute="class" defaultTheme="system">
25
+ {children}
26
+ </ThemeProvider>
27
+
28
+ // 3. Use components
29
+ import { ThemeToggle, ThemeDropdown, CinematicThemeSwitcher } from 'shadcn-themes';
30
+
31
+ <ThemeToggle />
32
+ <ThemeDropdown />
33
+ <CinematicThemeSwitcher />
34
+ ```
35
+
36
+ ## Components
37
+
38
+ ### ThemeToggle
39
+
40
+ Simple light/dark mode toggle.
41
+
42
+ ```tsx
43
+ <ThemeToggle mode="light-dark-system" />
44
+ // or
45
+ <ThemeToggle mode="light-dark" />
46
+ ```
47
+
48
+ **Props:**
49
+ - `mode?` - Include system option (default: `'light-dark-system'`)
50
+ - `Button?` - Custom Button component
51
+ - `DropdownMenu?` - Custom DropdownMenu components
52
+ - `onThemeChange?` - Callback when theme changes
53
+
54
+ ### ThemeDropdown
55
+
56
+ Full dropdown with 24+ color themes and live preview.
57
+
58
+ ```tsx
59
+ <ThemeDropdown
60
+ iconSrc="/custom-icon.svg"
61
+ onColorThemeChange={(theme) => console.log(theme)}
62
+ onModeChange={(mode) => console.log(mode)}
63
+ />
64
+ ```
65
+
66
+ **Props:**
67
+ - `iconSrc?` - Custom icon path (default: Palette icon)
68
+ - `Button?` - Custom Button component
69
+ - `DropdownMenu?` - Custom DropdownMenu components
70
+ - `onColorThemeChange?` - Callback when color theme changes
71
+ - `onModeChange?` - Callback when light/dark mode changes
72
+
73
+ ### CinematicThemeSwitcher
74
+
75
+ Animated toggle with particle effects.
76
+
77
+ ```tsx
78
+ <CinematicThemeSwitcher />
79
+ ```
80
+
81
+ ### Available Themes
82
+
83
+ 24 themes: `modern-minimal`, `elegant-luxury`, `cyberpunk`, `twitter`, `mocha-mousse`, `bubblegum`, `amethyst-haze`, `pink-lemonade`, `notebook`, `doom-64`, `catppuccin`, `graphite`, `perpetuity`, `kodama-grove`, `cosmic-night`, `tangerine`, `quantum-rose`, `nature`, `bold-tech`, `amber-minimal`, `supabase`, `neo-brutalism`, `solar-dusk`, `claymorphism`, `pastel-dreams`
84
+
85
+ ## Custom Components
86
+
87
+ Pass your own Button or DropdownMenu components:
88
+
89
+ ```tsx
90
+ import { Button } from '@/components/ui/button';
91
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
92
+
93
+ <ThemeDropdown
94
+ Button={Button}
95
+ DropdownMenu={{
96
+ Root: DropdownMenu.Root,
97
+ Trigger: DropdownMenu.Trigger,
98
+ Content: DropdownMenu.Content,
99
+ Item: DropdownMenu.Item,
100
+ Label: DropdownMenu.Label,
101
+ Separator: DropdownMenu.Separator
102
+ }}
103
+ />
104
+ ```
105
+
106
+ ## Programmatic Usage
107
+
108
+ ```tsx
109
+ import { themeNames, themeColors, formatThemeName } from 'shadcn-themes';
110
+
111
+ // Set theme programmatically
112
+ const setTheme = (themeName: string) => {
113
+ localStorage.setItem('color-theme', themeName);
114
+ themeNames.forEach(t => document.documentElement.classList.remove(`theme-${t}`));
115
+ document.documentElement.classList.add(`theme-${themeName}`);
116
+ };
117
+
118
+ // Get theme info
119
+ console.log(themeNames); // Array of all theme names
120
+ console.log(themeColors['cyberpunk']); // { primary: '#ff00c8', secondary: '#f0f0ff' }
121
+ console.log(formatThemeName('modern-minimal')); // 'Modern Minimal'
122
+ ```
123
+
124
+ ## TypeScript
125
+
126
+ Full TypeScript support with exported types:
127
+
128
+ ```tsx
129
+ import type { ThemeProviderProps } from 'shadcn-themes';
130
+ ```
131
+
132
+ ## Demo
133
+
134
+ Run the interactive demo:
135
+
136
+ ```bash
137
+ pnpm demo
138
+ ```
139
+
140
+ Or manually:
141
+ ```bash
142
+ cd demo
143
+ pnpm install
144
+ pnpm dev
145
+ ```
146
+
147
+ Opens at `http://localhost:3001`
148
+
149
+ ## License
150
+
151
+ MIT
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "shadcn-theme-menu",
3
+ "version": "1.1.2",
4
+ "description": "A collection of beautiful shadcn/ui theme components with dynamic theme switching and animations",
5
+ "author": "vtempest",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "types": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.ts",
12
+ "default": "./src/index.ts"
13
+ },
14
+ "./themes.css": "./src/themes-shadcn.css"
15
+ },
16
+ "files": [
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "ship": "npx standard-version --release-as patch; rm CHANGELOG.md; npm publish",
21
+ "demo": "cd demo && pnpm dev"
22
+ },
23
+ "sideEffects": [
24
+ "*.css"
25
+ ],
26
+ "keywords": [
27
+ "shadcn",
28
+ "shadcn-ui",
29
+ "themes",
30
+ "theme-switcher",
31
+ "dark-mode",
32
+ "light-mode",
33
+ "react",
34
+ "nextjs",
35
+ "tailwind"
36
+ ],
37
+ "dependencies": {
38
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
39
+ "@radix-ui/react-slot": "^1.2.4",
40
+ "class-variance-authority": "^0.7.1",
41
+ "clsx": "^2.1.1",
42
+ "tailwind-merge": "^3.5.0"
43
+ },
44
+ "peerDependencies": {
45
+ "framer-motion": ">=11.0.0",
46
+ "lucide-react": ">=0.400.0",
47
+ "next-auth": ">=4.0.0",
48
+ "next-themes": ">=0.3.0",
49
+ "react": ">=18.0.0",
50
+ "react-dom": ">=18.0.0"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "framer-motion": {
54
+ "optional": true
55
+ },
56
+ "next-auth": {
57
+ "optional": true
58
+ },
59
+ "react": {
60
+ "optional": true
61
+ },
62
+ "react-dom": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "devDependencies": {
67
+ "@types/react": "^19.2.14",
68
+ "@types/react-dom": "^19.2.3",
69
+ "@types/react-window": "^2.0.0",
70
+ "@vitejs/plugin-react": "^6.0.1",
71
+ "react": "^19.2.4",
72
+ "react-dom": "^19.2.4",
73
+ "vite": "^8.0.0"
74
+ }
75
+ }
@@ -0,0 +1,303 @@
1
+ 'use client';
2
+
3
+ import { Sun, Moon } from 'lucide-react';
4
+ import { useState, useEffect, useRef } from 'react';
5
+ import { motion } from 'framer-motion';
6
+ import { useTheme } from 'next-themes';
7
+
8
+ interface Particle {
9
+ id: number;
10
+ delay: number;
11
+ duration: number;
12
+ }
13
+
14
+ export default function Component() {
15
+ const { theme, setTheme, resolvedTheme } = useTheme();
16
+
17
+ // State Management
18
+ const [mounted, setMounted] = useState(false);
19
+ const [particles, setParticles] = useState<Particle[]>([]);
20
+ const [isAnimating, setIsAnimating] = useState(false);
21
+
22
+ // Ref to track toggle button DOM element
23
+ const toggleRef = useRef<HTMLButtonElement>(null);
24
+
25
+ // Track whether toggle is in checked (dark) or unchecked (light) position
26
+ const isDark = mounted && (theme === 'dark' || resolvedTheme === 'dark');
27
+
28
+ // Handle hydration - prevent mismatch
29
+ useEffect(() => {
30
+ setMounted(true);
31
+ }, []);
32
+
33
+ // Generate particles with different timing
34
+ const generateParticles = () => {
35
+ const newParticles: Particle[] = [];
36
+ const particleCount = 3; // Multiple layers
37
+
38
+ for (let i = 0; i < particleCount; i++) {
39
+ newParticles.push({
40
+ id: i,
41
+ delay: i * 0.1, // Stagger timing
42
+ duration: 0.6 + i * 0.1, // Different durations for depth
43
+ });
44
+ }
45
+
46
+ setParticles(newParticles);
47
+ setIsAnimating(true);
48
+
49
+ // Clear particles after animation
50
+ setTimeout(() => {
51
+ setIsAnimating(false);
52
+ setParticles([]);
53
+ }, 1000);
54
+ };
55
+
56
+ // Toggle handler - switches theme and triggers particles
57
+ const handleToggle = () => {
58
+ generateParticles();
59
+ setTheme(isDark ? 'light' : 'dark');
60
+ };
61
+
62
+ // Prevent hydration mismatch - show placeholder during SSR
63
+ if (!mounted) {
64
+ return (
65
+ <div className="relative inline-block">
66
+ <div className="relative flex h-[64px] w-[104px] items-center rounded-full bg-gray-200 p-1" />
67
+ </div>
68
+ );
69
+ }
70
+
71
+ return (
72
+ <div className="relative inline-block">
73
+ {/* SVG Filter for Film Grain Texture */}
74
+ <svg className="absolute w-0 h-0">
75
+ <defs>
76
+ {/* Light mode grain - subtle */}
77
+ <filter id="grain-light">
78
+ <feTurbulence
79
+ type="fractalNoise"
80
+ baseFrequency="0.9"
81
+ numOctaves="4"
82
+ result="noise"
83
+ />
84
+ <feColorMatrix
85
+ in="noise"
86
+ type="saturate"
87
+ values="0"
88
+ result="desaturatedNoise"
89
+ />
90
+ <feComponentTransfer in="desaturatedNoise" result="lightGrain">
91
+ <feFuncA type="linear" slope="0.3" />
92
+ </feComponentTransfer>
93
+ <feBlend in="SourceGraphic" in2="lightGrain" mode="overlay" />
94
+ </filter>
95
+
96
+ {/* Dark mode grain - more visible */}
97
+ <filter id="grain-dark">
98
+ <feTurbulence
99
+ type="fractalNoise"
100
+ baseFrequency="0.9"
101
+ numOctaves="4"
102
+ result="noise"
103
+ />
104
+ <feColorMatrix
105
+ in="noise"
106
+ type="saturate"
107
+ values="0"
108
+ result="desaturatedNoise"
109
+ />
110
+ <feComponentTransfer in="desaturatedNoise" result="darkGrain">
111
+ <feFuncA type="linear" slope="0.5" />
112
+ </feComponentTransfer>
113
+ <feBlend in="SourceGraphic" in2="darkGrain" mode="overlay" />
114
+ </filter>
115
+ </defs>
116
+ </svg>
117
+
118
+ {/* Pill-shaped track container */}
119
+ <motion.button
120
+ ref={toggleRef}
121
+ onClick={handleToggle}
122
+ className="relative flex h-[64px] w-[104px] items-center rounded-full p-[6px] transition-all duration-300 focus:outline-none"
123
+ style={{
124
+ background: isDark
125
+ ? 'radial-gradient(ellipse at top left, #1e293b 0%, #0f172a 40%, #020617 100%)'
126
+ : 'radial-gradient(ellipse at top left, #ffffff 0%, #f1f5f9 40%, #cbd5e1 100%)',
127
+ boxShadow: isDark
128
+ ? `
129
+ inset 5px 5px 12px rgba(0, 0, 0, 0.9),
130
+ inset -5px -5px 12px rgba(71, 85, 105, 0.4),
131
+ inset 8px 8px 16px rgba(0, 0, 0, 0.7),
132
+ inset -8px -8px 16px rgba(100, 116, 139, 0.2),
133
+ inset 0 2px 4px rgba(0, 0, 0, 1),
134
+ inset 0 -2px 4px rgba(71, 85, 105, 0.4),
135
+ inset 0 0 20px rgba(0, 0, 0, 0.6),
136
+ 0 1px 1px rgba(255, 255, 255, 0.05),
137
+ 0 2px 4px rgba(0, 0, 0, 0.4),
138
+ 0 8px 16px rgba(0, 0, 0, 0.4),
139
+ 0 16px 32px rgba(0, 0, 0, 0.3),
140
+ 0 24px 48px rgba(0, 0, 0, 0.2)
141
+ `
142
+ : `
143
+ inset 5px 5px 12px rgba(148, 163, 184, 0.5),
144
+ inset -5px -5px 12px rgba(255, 255, 255, 1),
145
+ inset 8px 8px 16px rgba(100, 116, 139, 0.3),
146
+ inset -8px -8px 16px rgba(255, 255, 255, 0.9),
147
+ inset 0 2px 4px rgba(148, 163, 184, 0.4),
148
+ inset 0 -2px 4px rgba(255, 255, 255, 1),
149
+ inset 0 0 20px rgba(203, 213, 225, 0.3),
150
+ 0 1px 2px rgba(255, 255, 255, 1),
151
+ 0 2px 4px rgba(0, 0, 0, 0.1),
152
+ 0 8px 16px rgba(0, 0, 0, 0.08),
153
+ 0 16px 32px rgba(0, 0, 0, 0.06),
154
+ 0 24px 48px rgba(0, 0, 0, 0.04)
155
+ `,
156
+ border: isDark
157
+ ? '2px solid rgba(51, 65, 85, 0.6)'
158
+ : '2px solid rgba(203, 213, 225, 0.6)',
159
+ position: 'relative',
160
+ }}
161
+ aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}
162
+ role="switch"
163
+ aria-checked={isDark}
164
+ whileTap={{ scale: 0.98 }}
165
+ >
166
+ {/* Deep inner groove/rim effect */}
167
+ <div
168
+ className="absolute inset-[3px] rounded-full pointer-events-none"
169
+ style={{
170
+ boxShadow: isDark
171
+ ? 'inset 0 2px 6px rgba(0, 0, 0, 0.9), inset 0 -1px 3px rgba(71, 85, 105, 0.3)'
172
+ : 'inset 0 2px 6px rgba(100, 116, 139, 0.4), inset 0 -1px 3px rgba(255, 255, 255, 0.8)',
173
+ }}
174
+ />
175
+
176
+ {/* Multi-layer glossy overlay */}
177
+ <div
178
+ className="absolute inset-0 rounded-full pointer-events-none"
179
+ style={{
180
+ background: isDark
181
+ ? `
182
+ radial-gradient(ellipse at top, rgba(71, 85, 105, 0.15) 0%, transparent 50%),
183
+ linear-gradient(to bottom, rgba(71, 85, 105, 0.2) 0%, transparent 30%, transparent 70%, rgba(0, 0, 0, 0.3) 100%)
184
+ `
185
+ : `
186
+ radial-gradient(ellipse at top, rgba(255, 255, 255, 0.8) 0%, transparent 50%),
187
+ linear-gradient(to bottom, rgba(255, 255, 255, 0.7) 0%, transparent 30%, transparent 70%, rgba(148, 163, 184, 0.15) 100%)
188
+ `,
189
+ mixBlendMode: 'overlay',
190
+ }}
191
+ />
192
+
193
+ {/* Ambient occlusion effect */}
194
+ <div
195
+ className="absolute inset-0 rounded-full pointer-events-none"
196
+ style={{
197
+ boxShadow: isDark
198
+ ? 'inset 0 0 15px rgba(0, 0, 0, 0.5)'
199
+ : 'inset 0 0 15px rgba(148, 163, 184, 0.2)',
200
+ }}
201
+ />
202
+ {/* Background Icons */}
203
+ <div className="absolute inset-0 flex items-center justify-between px-4">
204
+ <Sun size={20} className={isDark ? 'text-yellow-100' : 'text-amber-600'} />
205
+ <Moon size={20} className={isDark ? 'text-yellow-100' : 'text-slate-700'} />
206
+ </div>
207
+
208
+ {/* Circular Thumb with Bouncy Spring Physics */}
209
+ <motion.div
210
+ className="relative z-10 flex h-[44px] w-[44px] items-center justify-center rounded-full overflow-hidden"
211
+ style={{
212
+ background: isDark
213
+ ? 'linear-gradient(145deg, #64748b 0%, #475569 50%, #334155 100%)'
214
+ : 'linear-gradient(145deg, #ffffff 0%, #fefefe 50%, #f8fafc 100%)',
215
+ boxShadow: isDark
216
+ ? `
217
+ inset 2px 2px 4px rgba(100, 116, 139, 0.4),
218
+ inset -2px -2px 4px rgba(0, 0, 0, 0.8),
219
+ inset 0 1px 1px rgba(255, 255, 255, 0.15),
220
+ 0 1px 2px rgba(255, 255, 255, 0.1),
221
+ 0 8px 32px rgba(0, 0, 0, 0.6),
222
+ 0 4px 12px rgba(0, 0, 0, 0.5),
223
+ 0 2px 4px rgba(0, 0, 0, 0.4)
224
+ `
225
+ : `
226
+ inset 2px 2px 4px rgba(203, 213, 225, 0.3),
227
+ inset -2px -2px 4px rgba(255, 255, 255, 1),
228
+ inset 0 1px 2px rgba(255, 255, 255, 1),
229
+ 0 1px 2px rgba(255, 255, 255, 1),
230
+ 0 8px 32px rgba(0, 0, 0, 0.18),
231
+ 0 4px 12px rgba(0, 0, 0, 0.12),
232
+ 0 2px 4px rgba(0, 0, 0, 0.08)
233
+ `,
234
+ border: isDark
235
+ ? '2px solid rgba(148, 163, 184, 0.3)'
236
+ : '2px solid rgba(255, 255, 255, 0.9)',
237
+ }}
238
+ animate={{
239
+ x: isDark ? 46 : 0,
240
+ }}
241
+ transition={{
242
+ type: 'spring',
243
+ stiffness: 300, // Fast, responsive movement
244
+ damping: 20, // Bouncy feel with slight overshoot
245
+ }}
246
+ >
247
+ {/* Glossy shine overlay on thumb */}
248
+ <div
249
+ className="absolute inset-0 rounded-full pointer-events-none"
250
+ style={{
251
+ background: 'linear-gradient(to bottom, rgba(255, 255, 255, 0.4) 0%, transparent 40%, rgba(0, 0, 0, 0.1) 100%)',
252
+ mixBlendMode: 'overlay',
253
+ }}
254
+ />
255
+ {/* Particle Layer - expanding circles from center with grainy texture */}
256
+ {isAnimating && particles.map((particle) => (
257
+ <motion.div
258
+ key={particle.id}
259
+ className="absolute inset-0 flex items-center justify-center pointer-events-none"
260
+ >
261
+ <motion.div
262
+ className="absolute rounded-full"
263
+ style={{
264
+ width: '10px',
265
+ height: '10px',
266
+ background: isDark
267
+ ? 'radial-gradient(circle, rgba(147, 197, 253, 0.5) 0%, rgba(147, 197, 253, 0) 70%)'
268
+ : 'radial-gradient(circle, rgba(251, 191, 36, 0.7) 0%, rgba(251, 191, 36, 0) 70%)',
269
+ mixBlendMode: 'normal',
270
+ }}
271
+ initial={{ scale: 0, opacity: 0 }}
272
+ animate={{ scale: isDark ? 6 : 8, opacity: [0, 1, 0] }}
273
+ transition={{
274
+ duration: isDark ? 0.5 : particle.duration,
275
+ delay: particle.delay,
276
+ ease: 'easeOut',
277
+ }}
278
+ >
279
+ {/* Grainy texture overlay */}
280
+ <div
281
+ className="absolute inset-0 rounded-full opacity-40"
282
+ style={{
283
+ backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E")`,
284
+ mixBlendMode: 'overlay',
285
+ }}
286
+ />
287
+ </motion.div>
288
+ </motion.div>
289
+ ))}
290
+
291
+ {/* Icon */}
292
+ <div className="relative z-10">
293
+ {isDark ? (
294
+ <Moon size={20} className="text-yellow-200" />
295
+ ) : (
296
+ <Sun size={20} className="text-amber-500" />
297
+ )}
298
+ </div>
299
+ </motion.div>
300
+ </motion.button>
301
+ </div>
302
+ );
303
+ }
@@ -0,0 +1,56 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { cn } from "../../lib/utils"
5
+
6
+ const buttonVariants = cva(
7
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-9 px-4 py-2",
24
+ sm: "h-8 rounded-md px-3 text-xs",
25
+ lg: "h-10 rounded-md px-8",
26
+ icon: "h-9 w-9",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ }
34
+ )
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button"
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+ )
54
+ Button.displayName = "Button"
55
+
56
+ export { Button, buttonVariants }