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
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 }
|