@space-man/react-theme-animation 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -57
- package/dist/cjs/components/ThemeSwitcher.js +7 -26
- package/dist/cjs/components/ViteThemeProvider.d.ts +19 -0
- package/dist/cjs/components/ViteThemeProvider.js +85 -0
- package/dist/cjs/hooks/use-theme-animation.js +75 -13
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/types/index.d.ts +8 -1
- package/dist/cjs/types/index.js +1 -0
- package/dist/cjs/utils/animations.d.ts +3 -0
- package/dist/cjs/utils/animations.js +34 -1
- package/dist/esm/components/ThemeSwitcher.js +7 -26
- package/dist/esm/components/ViteThemeProvider.d.ts +19 -0
- package/dist/esm/components/ViteThemeProvider.js +80 -0
- package/dist/esm/hooks/use-theme-animation.js +76 -14
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/types/index.d.ts +8 -1
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/utils/animations.d.ts +3 -0
- package/dist/esm/utils/animations.js +32 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
# @space-man/react-theme-animation
|
|
2
2
|
|
|
3
|
+
https://github.com/user-attachments/assets/2819cf70-c474-478e-b821-6b26457c8d4a
|
|
4
|
+
|
|
3
5
|
React theme switching with smooth view transition animations, multi-theme support, and synchronized state management.
|
|
4
6
|
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img width="50%" src="https://github.com/user-attachments/assets/98b7f5cc-705c-4d32-a2f6-47f79b298707" />
|
|
9
|
+
<img width="50%" src="https://github.com/user-attachments/assets/128cc7b0-5708-4b4c-884f-a8310d4e494f" />
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
## Live Demo
|
|
13
|
+
|
|
14
|
+
[Live Demo Link](https://spaceman-rta-vite.netlify.app/)
|
|
15
|
+
|
|
5
16
|
## Features
|
|
6
17
|
|
|
7
18
|
- 🎨 **Smooth Animations**: Beautiful view transition animations for theme switching with customizable origins
|
|
@@ -25,14 +36,17 @@ npm install @space-man/react-theme-animation
|
|
|
25
36
|
|
|
26
37
|
## Usage Patterns
|
|
27
38
|
|
|
28
|
-
This library supports
|
|
39
|
+
This library supports multiple usage patterns:
|
|
29
40
|
|
|
30
41
|
1. **Hook-Only Usage**: Direct hook usage for custom implementations
|
|
31
42
|
2. **Provider Pattern**: Centralized state management (recommended)
|
|
43
|
+
3. **ViteThemeProvider**: Specialized provider for Vite React SPAs
|
|
32
44
|
|
|
33
45
|
📚 **[See the complete Getting Started Guide](./getting-started.md)** for detailed examples, advanced configurations, and best practices.
|
|
34
46
|
|
|
35
|
-
🚀 **[View the Code Example](./example/)** to see this package in action with a complete implementation of both hook and provider pattern.
|
|
47
|
+
🚀 **[View the Next.js Code Example](./example/)** to see this package in action with a complete implementation of both hook and provider pattern.
|
|
48
|
+
|
|
49
|
+
⚡ **[View the Vite React SPA Demo](https://github.com/tonyedgal/spaceman-rta-framework-guides/tree/main/example-vite)** for a complete Vite implementation using ViteThemeProvider.
|
|
36
50
|
|
|
37
51
|
---
|
|
38
52
|
|
|
@@ -80,7 +94,54 @@ function App() {
|
|
|
80
94
|
</header>
|
|
81
95
|
|
|
82
96
|
<aside>
|
|
83
|
-
<ThemeSelector colorThemes={['default', '
|
|
97
|
+
<ThemeSelector colorThemes={['default', 'supabase', 'mono']} />
|
|
98
|
+
</aside>
|
|
99
|
+
|
|
100
|
+
<main>
|
|
101
|
+
<YourAppContent />
|
|
102
|
+
</main>
|
|
103
|
+
</div>
|
|
104
|
+
</SpacemanThemeProvider>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 3. ViteThemeProvider (Vite React SPAs)
|
|
112
|
+
|
|
113
|
+
Specialized theme provider optimized for Vite React single-page applications. Provides essential theme management without animation features.
|
|
114
|
+
|
|
115
|
+
### Basic Vite Setup
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { ViteThemeProvider, useViteTheme } from '@space-man/react-theme-animation'
|
|
119
|
+
|
|
120
|
+
function App() {
|
|
121
|
+
return (
|
|
122
|
+
<ViteThemeProvider defaultTheme="system" storageKey="my-app-theme" attribute="class">
|
|
123
|
+
<div className="app">
|
|
124
|
+
<header>
|
|
125
|
+
<ThemeToggle />
|
|
126
|
+
</header>
|
|
127
|
+
<main>
|
|
128
|
+
<ThemeSection />
|
|
129
|
+
</main>
|
|
130
|
+
</div>
|
|
131
|
+
</ViteThemeProvider>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function ThemeSection() {
|
|
136
|
+
return (
|
|
137
|
+
<SpacemanThemeProvider>
|
|
138
|
+
<div className="app">
|
|
139
|
+
<header>
|
|
140
|
+
<ThemeSwitcher />
|
|
141
|
+
</header>
|
|
142
|
+
|
|
143
|
+
<aside>
|
|
144
|
+
<ThemeSelector colorThemes={['default', 'supabase', 'mono']} />
|
|
84
145
|
</aside>
|
|
85
146
|
|
|
86
147
|
<main>
|
|
@@ -90,6 +151,69 @@ function App() {
|
|
|
90
151
|
</SpacemanThemeProvider>
|
|
91
152
|
)
|
|
92
153
|
}
|
|
154
|
+
|
|
155
|
+
function ThemeToggle() {
|
|
156
|
+
const { theme, setTheme, resolvedTheme } = useViteTheme()
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
|
160
|
+
{resolvedTheme === 'dark' ? '🌞' : '🌙'} {theme}
|
|
161
|
+
</button>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### ViteThemeProvider Props
|
|
167
|
+
|
|
168
|
+
| Prop | Type | Default | Description |
|
|
169
|
+
| --------------------------- | ------------------------------- | -------------- | ------------------------------------------- |
|
|
170
|
+
| `children` | `ReactNode` | - | React children |
|
|
171
|
+
| `attribute` | `'class' \| 'data-theme'` | `'class'` | How to apply theme to DOM |
|
|
172
|
+
| `defaultTheme` | `'light' \| 'dark' \| 'system'` | `'system'` | Default theme |
|
|
173
|
+
| `enableSystem` | `boolean` | `true` | Enable system theme detection |
|
|
174
|
+
| `disableTransitionOnChange` | `boolean` | `false` | Disable CSS transitions during theme change |
|
|
175
|
+
| `storageKey` | `string` | `'vite-theme'` | localStorage key for persistence |
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Defining Application color theme as CSS Variables
|
|
180
|
+
|
|
181
|
+
The library uses CSS custom properties for theming. Define these in your **MAIN** CSS file. _Note_: do not define the other theme variables in a separate CSS file and import it, as the `:root` variables for light mode will override the light mode variables for the other themes. Keep everything in the same file.
|
|
182
|
+
|
|
183
|
+
```css
|
|
184
|
+
/* Base theme variables */
|
|
185
|
+
:root {
|
|
186
|
+
--background: 0 0% 100%;
|
|
187
|
+
--foreground: 222.2 84% 4.9%;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Dark theme */
|
|
191
|
+
.dark {
|
|
192
|
+
--background: 222.2 84% 4.9%;
|
|
193
|
+
--foreground: 210 40% 98%;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* Add other theme variants from design system or tweakcn */
|
|
197
|
+
|
|
198
|
+
.theme-supabase {
|
|
199
|
+
--background: oklch(0.9911 0 0);
|
|
200
|
+
--foreground: oklch(0.2046 0 0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.theme-supabase.dark {
|
|
204
|
+
--background: oklch(0.1822 0 0);
|
|
205
|
+
--foreground: oklch(0.9288 0.0126 255.5078);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.theme-mono {
|
|
209
|
+
--background: oklch(1 0 0);
|
|
210
|
+
--foreground: oklch(0.1448 0 0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.theme-mono.dark {
|
|
214
|
+
--background: oklch(0.1448 0 0);
|
|
215
|
+
--foreground: oklch(1 0 0);
|
|
216
|
+
}
|
|
93
217
|
```
|
|
94
218
|
|
|
95
219
|
---
|
|
@@ -98,7 +222,7 @@ function App() {
|
|
|
98
222
|
|
|
99
223
|
### SpacemanThemeProvider
|
|
100
224
|
|
|
101
|
-
Context provider for centralized theme state management.
|
|
225
|
+
Context provider for centralized theme state management for the ThemeSelector and ThemeSwitcher components. The SpacemanThemeProvider allows you to manage themes and color themes in your application with smooth animations and synchronized state and provide a state reference for the ThemeSelector and ThemeSwitcher components.
|
|
102
226
|
|
|
103
227
|
```tsx
|
|
104
228
|
<SpacemanThemeProvider
|
|
@@ -111,7 +235,7 @@ Context provider for centralized theme state management.
|
|
|
111
235
|
onThemeChange={theme => console.log('Theme:', theme)}
|
|
112
236
|
onColorThemeChange={colorTheme => console.log('Color:', colorTheme)}
|
|
113
237
|
>
|
|
114
|
-
|
|
238
|
+
{children}
|
|
115
239
|
</SpacemanThemeProvider>
|
|
116
240
|
```
|
|
117
241
|
|
|
@@ -157,7 +281,7 @@ Standalone hook for theme management and animations.
|
|
|
157
281
|
const { theme, colorTheme, switchTheme, setColorTheme, toggleTheme, ref } = useThemeAnimation({
|
|
158
282
|
themes: ['light', 'dark', 'system'],
|
|
159
283
|
colorThemes: ['default', 'blue', 'green', 'purple'],
|
|
160
|
-
animationType: ThemeAnimationType.
|
|
284
|
+
animationType: ThemeAnimationType.SLIDE,
|
|
161
285
|
duration: 800,
|
|
162
286
|
onThemeChange: theme => console.log('Theme changed:', theme),
|
|
163
287
|
onColorThemeChange: colorTheme => console.log('Color changed:', colorTheme),
|
|
@@ -166,17 +290,18 @@ const { theme, colorTheme, switchTheme, setColorTheme, toggleTheme, ref } = useT
|
|
|
166
290
|
|
|
167
291
|
#### Options
|
|
168
292
|
|
|
169
|
-
| Property | Type | Default | Description
|
|
170
|
-
| -------------------- | ---------------------------------- | ----------------------------- |
|
|
171
|
-
| `themes` | `Theme[]` | `['light', 'dark', 'system']` | Available themes
|
|
172
|
-
| `colorThemes` | `ColorTheme[]` | `['default']` | Available color themes
|
|
173
|
-
| `theme` | `Theme` | `'system'` | Initial theme
|
|
174
|
-
| `colorTheme` | `ColorTheme` | `'default'` | Initial color theme
|
|
175
|
-
| `animationType` | `ThemeAnimationType` | `ThemeAnimationType.CIRCLE` | Animation type
|
|
176
|
-
| `duration` | `number` | `500` | Animation duration in ms
|
|
177
|
-
| `blurAmount` | `number` | `2` | Blur amount for blur-circle animation
|
|
178
|
-
| `onThemeChange` | `(theme: Theme) => void` | - | Theme change callback
|
|
179
|
-
| `onColorThemeChange` | `(colorTheme: ColorTheme) => void` | - | Color theme change callback
|
|
293
|
+
| Property | Type | Default | Description |
|
|
294
|
+
| -------------------- | ---------------------------------- | ----------------------------- | --------------------------------------------------- |
|
|
295
|
+
| `themes` | `Theme[]` | `['light', 'dark', 'system']` | Available themes |
|
|
296
|
+
| `colorThemes` | `ColorTheme[]` | `['default']` | Available color themes |
|
|
297
|
+
| `theme` | `Theme` | `'system'` | Initial theme |
|
|
298
|
+
| `colorTheme` | `ColorTheme` | `'default'` | Initial color theme |
|
|
299
|
+
| `animationType` | `ThemeAnimationType` | `ThemeAnimationType.CIRCLE` | Animation type. Options: CIRCLE, BLUR_CIRCLE, SLIDE |
|
|
300
|
+
| `duration` | `number` | `500` | Animation duration in ms |
|
|
301
|
+
| `blurAmount` | `number` | `2` | Blur amount for blur-circle animation |
|
|
302
|
+
| `onThemeChange` | `(theme: Theme) => void` | - | Theme change callback |
|
|
303
|
+
| `onColorThemeChange` | `(colorTheme: ColorTheme) => void` | - | Color theme change callback |
|
|
304
|
+
| `slideDirection` | `SlideDirection` | `left` | Animation slide direction |
|
|
180
305
|
|
|
181
306
|
#### Return
|
|
182
307
|
|
|
@@ -316,46 +441,6 @@ interface ThemeSelectorProps {
|
|
|
316
441
|
|
|
317
442
|
---
|
|
318
443
|
|
|
319
|
-
## CSS Variables
|
|
320
|
-
|
|
321
|
-
The library uses CSS custom properties for theming. Define these in your CSS:
|
|
322
|
-
|
|
323
|
-
```css
|
|
324
|
-
/* Base theme variables */
|
|
325
|
-
:root {
|
|
326
|
-
--background: 0 0% 100%;
|
|
327
|
-
--foreground: 222.2 84% 4.9%;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/* Dark theme */
|
|
331
|
-
.dark {
|
|
332
|
-
--background: 222.2 84% 4.9%;
|
|
333
|
-
--foreground: 210 40% 98%;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/* Add other theme variants from design system or tweakcn */
|
|
337
|
-
|
|
338
|
-
.theme-supabase {
|
|
339
|
-
--background: oklch(0.9911 0 0);
|
|
340
|
-
--foreground: oklch(0.2046 0 0);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
.theme-supabase.dark {
|
|
344
|
-
--background: oklch(0.1822 0 0);
|
|
345
|
-
--foreground: oklch(0.9288 0.0126 255.5078);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
.theme-mono {
|
|
349
|
-
--background: oklch(1 0 0);
|
|
350
|
-
--foreground: oklch(0.1448 0 0);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
.theme-mono.dark {
|
|
354
|
-
--background: oklch(0.1448 0 0);
|
|
355
|
-
--foreground: oklch(1 0 0);
|
|
356
|
-
}
|
|
357
|
-
```
|
|
358
|
-
|
|
359
444
|
---
|
|
360
445
|
|
|
361
446
|
## Browser Support
|
|
@@ -11,9 +11,9 @@ const SunIcon = () => ((0, jsx_runtime_1.jsxs)("svg", { width: "16", height: "16
|
|
|
11
11
|
const MoonIcon = () => ((0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" }) }));
|
|
12
12
|
const MonitorIcon = () => ((0, jsx_runtime_1.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)("rect", { width: "20", height: "14", x: "2", y: "3", rx: "2" }), (0, jsx_runtime_1.jsx)("line", { x1: "8", x2: "16", y1: "21", y2: "21" }), (0, jsx_runtime_1.jsx)("line", { x1: "12", x2: "12", y1: "17", y2: "21" })] }));
|
|
13
13
|
const ThemeOption = ({ icon, value, isActive, isHovered, onClick, onMouseEnter, onMouseLeave, buttonRef, }) => {
|
|
14
|
-
return ((0, jsx_runtime_1.jsxs)("button", { ref: isActive ? buttonRef : undefined, className: (0, clsx_1.clsx)('relative flex h-9 w-12 cursor-pointer items-center justify-center
|
|
14
|
+
return ((0, jsx_runtime_1.jsxs)("button", { ref: isActive ? buttonRef : undefined, className: (0, clsx_1.clsx)('relative flex h-9 w-12 cursor-pointer items-center justify-center', 'text-muted-foreground hover:text-foreground', isActive && 'text-foreground font-medium'), role: "radio", "aria-checked": isActive, "aria-label": `Switch to ${value} theme`, onClick: event => onClick(value, event), onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, style: {
|
|
15
15
|
borderRadius: 'var(--radius)',
|
|
16
|
-
}, children: [isHovered && ((0, jsx_runtime_1.jsx)(react_2.motion.div, { layoutId: "theme-hover
|
|
16
|
+
}, children: [isHovered && ((0, jsx_runtime_1.jsx)(react_2.motion.div, { layoutId: "theme-hover", className: "absolute inset-0 bg-muted", style: {
|
|
17
17
|
borderRadius: 'var(--radius)',
|
|
18
18
|
}, transition: {
|
|
19
19
|
type: 'spring',
|
|
@@ -21,18 +21,8 @@ const ThemeOption = ({ icon, value, isActive, isHovered, onClick, onMouseEnter,
|
|
|
21
21
|
stiffness: 100,
|
|
22
22
|
damping: 10,
|
|
23
23
|
duration: 0.3,
|
|
24
|
-
} })), (0, jsx_runtime_1.jsx)(
|
|
25
|
-
scale: isActive ? 1.1 : 1,
|
|
26
|
-
}, transition: {
|
|
27
|
-
type: 'spring',
|
|
28
|
-
bounce: 0.2,
|
|
29
|
-
duration: 0.4,
|
|
30
|
-
}, children: (0, jsx_runtime_1.jsx)("div", { className: "[&_svg]:size-4", children: icon }) }), isActive && ((0, jsx_runtime_1.jsx)(react_2.motion.div, { layoutId: "theme-active-indicator", className: "absolute bottom-0 left-1/2 h-0.5 w-6 -translate-x-1/2 rounded-full", style: {
|
|
24
|
+
} })), (0, jsx_runtime_1.jsx)("div", { className: "relative z-10 flex items-center justify-center", children: (0, jsx_runtime_1.jsx)("div", { className: "[&_svg]:size-4", children: icon }) }), isActive && ((0, jsx_runtime_1.jsx)("div", { className: "absolute bottom-0 left-1/2 h-0.5 w-6 -translate-x-1/2 rounded-full", style: {
|
|
31
25
|
backgroundColor: 'hsl(var(--primary))',
|
|
32
|
-
}, transition: {
|
|
33
|
-
type: 'spring',
|
|
34
|
-
bounce: 0.3,
|
|
35
|
-
duration: 0.6,
|
|
36
26
|
} }))] }));
|
|
37
27
|
};
|
|
38
28
|
const THEME_OPTIONS = [
|
|
@@ -50,15 +40,11 @@ const THEME_OPTIONS = [
|
|
|
50
40
|
},
|
|
51
41
|
];
|
|
52
42
|
const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentTheme, onThemeChange, animationType, duration, className, }) => {
|
|
53
|
-
// Try to use SpacemanTheme context if available (controlled mode)
|
|
54
43
|
let contextTheme = null;
|
|
55
44
|
try {
|
|
56
45
|
contextTheme = (0, SpacemanThemeProvider_1.useSpacemanTheme)();
|
|
57
46
|
}
|
|
58
|
-
catch {
|
|
59
|
-
// Context not available, use standalone mode
|
|
60
|
-
}
|
|
61
|
-
// Use context if available, otherwise fall back to standalone hook
|
|
47
|
+
catch { }
|
|
62
48
|
const standaloneHook = (0, use_theme_animation_1.useThemeAnimation)({
|
|
63
49
|
animationType,
|
|
64
50
|
duration,
|
|
@@ -71,30 +57,25 @@ const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentTheme, onT
|
|
|
71
57
|
? { ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme }
|
|
72
58
|
: standaloneHook;
|
|
73
59
|
const [isMounted, setIsMounted] = (0, react_1.useState)(false);
|
|
60
|
+
const [hoveredTheme, setHoveredTheme] = (0, react_1.useState)(null);
|
|
74
61
|
(0, react_1.useEffect)(() => {
|
|
75
62
|
setIsMounted(true);
|
|
76
63
|
}, []);
|
|
77
64
|
const handleThemeChange = async (newTheme, event) => {
|
|
78
65
|
if (isControlled && contextTheme.switchThemeFromElement && event) {
|
|
79
|
-
// Use controlled mode with animation from clicked element
|
|
80
66
|
await contextTheme.switchThemeFromElement(newTheme, event.currentTarget);
|
|
81
|
-
// Also call the callback if provided
|
|
82
67
|
if (onThemeChange) {
|
|
83
68
|
onThemeChange(newTheme);
|
|
84
69
|
}
|
|
85
70
|
}
|
|
86
71
|
else {
|
|
87
|
-
// Use standalone mode
|
|
88
72
|
await switchTheme(newTheme);
|
|
89
73
|
}
|
|
90
74
|
};
|
|
91
|
-
if (!isMounted) {
|
|
92
|
-
return (0, jsx_runtime_1.jsx)("div", { className: "flex h-9 w-fit" });
|
|
93
|
-
}
|
|
94
75
|
const filteredOptions = THEME_OPTIONS.filter(option => themes.includes(option.value));
|
|
95
|
-
return ((0, jsx_runtime_1.jsx)(
|
|
76
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: (0, clsx_1.clsx)('inline-flex h-9 items-center overflow-hidden bg-background shadow-sm border border-border', isMounted ? 'opacity-100' : 'opacity-0', className), style: {
|
|
96
77
|
borderRadius: 'var(--radius)',
|
|
97
78
|
backgroundColor: 'hsl(var(--background))',
|
|
98
|
-
}, role: "radiogroup", children: filteredOptions.map(option => ((0, jsx_runtime_1.jsx)(ThemeOption, { icon: option.icon, value: option.value, isActive: theme === option.value, isHovered:
|
|
79
|
+
}, role: "radiogroup", onMouseLeave: () => setHoveredTheme(null), children: filteredOptions.map(option => ((0, jsx_runtime_1.jsx)(ThemeOption, { icon: option.icon, value: option.value, isActive: theme === option.value, isHovered: hoveredTheme === option.value, onClick: handleThemeChange, onMouseEnter: () => setHoveredTheme(option.value), onMouseLeave: () => { }, buttonRef: theme === option.value ? ref : undefined }, option.value))) }));
|
|
99
80
|
};
|
|
100
81
|
exports.ThemeSwitcher = ThemeSwitcher;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type Theme = 'light' | 'dark' | 'system';
|
|
3
|
+
export interface ViteThemeContextType {
|
|
4
|
+
theme: Theme;
|
|
5
|
+
resolvedTheme: 'light' | 'dark';
|
|
6
|
+
setTheme: (theme: Theme) => void;
|
|
7
|
+
systemTheme: 'light' | 'dark';
|
|
8
|
+
}
|
|
9
|
+
interface ViteThemeProviderProps {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
attribute?: 'class' | 'data-theme';
|
|
12
|
+
defaultTheme?: Theme;
|
|
13
|
+
enableSystem?: boolean;
|
|
14
|
+
disableTransitionOnChange?: boolean;
|
|
15
|
+
storageKey?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const ViteThemeProvider: React.FC<ViteThemeProviderProps>;
|
|
18
|
+
export declare const useViteTheme: () => ViteThemeContextType;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useViteTheme = exports.ViteThemeProvider = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const ViteThemeContext = (0, react_1.createContext)(undefined);
|
|
7
|
+
const ViteThemeProvider = ({ children, attribute = 'class', defaultTheme = 'system', enableSystem = true, disableTransitionOnChange = false, storageKey = 'vite-theme', }) => {
|
|
8
|
+
const [mounted, setMounted] = (0, react_1.useState)(false);
|
|
9
|
+
const [systemTheme, setSystemTheme] = (0, react_1.useState)('light');
|
|
10
|
+
const [theme, setThemeState] = (0, react_1.useState)(() => {
|
|
11
|
+
if (typeof window === 'undefined')
|
|
12
|
+
return defaultTheme;
|
|
13
|
+
try {
|
|
14
|
+
const savedTheme = localStorage.getItem(storageKey);
|
|
15
|
+
return savedTheme && ['light', 'dark', 'system'].includes(savedTheme)
|
|
16
|
+
? savedTheme
|
|
17
|
+
: defaultTheme;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return defaultTheme;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
(0, react_1.useEffect)(() => {
|
|
24
|
+
if (!enableSystem)
|
|
25
|
+
return;
|
|
26
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
27
|
+
const updateSystemTheme = () => {
|
|
28
|
+
setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
|
|
29
|
+
};
|
|
30
|
+
updateSystemTheme();
|
|
31
|
+
mediaQuery.addEventListener('change', updateSystemTheme);
|
|
32
|
+
return () => mediaQuery.removeEventListener('change', updateSystemTheme);
|
|
33
|
+
}, [enableSystem]);
|
|
34
|
+
const resolvedTheme = theme === 'system' ? systemTheme : theme;
|
|
35
|
+
(0, react_1.useEffect)(() => {
|
|
36
|
+
if (!mounted)
|
|
37
|
+
return;
|
|
38
|
+
const root = document.documentElement;
|
|
39
|
+
if (disableTransitionOnChange) {
|
|
40
|
+
const css = document.createElement('style');
|
|
41
|
+
css.textContent = `*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`;
|
|
42
|
+
document.head.appendChild(css);
|
|
43
|
+
window.getComputedStyle(document.body);
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
document.head.removeChild(css);
|
|
46
|
+
}, 1);
|
|
47
|
+
}
|
|
48
|
+
if (attribute === 'class') {
|
|
49
|
+
root.classList.remove('light', 'dark');
|
|
50
|
+
root.classList.add(resolvedTheme);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
root.setAttribute(attribute, resolvedTheme);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
localStorage.setItem(storageKey, theme);
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
}, [theme, resolvedTheme, mounted, attribute, disableTransitionOnChange, storageKey]);
|
|
60
|
+
(0, react_1.useEffect)(() => {
|
|
61
|
+
setMounted(true);
|
|
62
|
+
}, []);
|
|
63
|
+
const setTheme = (0, react_1.useCallback)((newTheme) => {
|
|
64
|
+
setThemeState(newTheme);
|
|
65
|
+
}, []);
|
|
66
|
+
if (!mounted) {
|
|
67
|
+
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children });
|
|
68
|
+
}
|
|
69
|
+
const value = {
|
|
70
|
+
theme,
|
|
71
|
+
resolvedTheme,
|
|
72
|
+
setTheme,
|
|
73
|
+
systemTheme,
|
|
74
|
+
};
|
|
75
|
+
return (0, jsx_runtime_1.jsx)(ViteThemeContext.Provider, { value: value, children: children });
|
|
76
|
+
};
|
|
77
|
+
exports.ViteThemeProvider = ViteThemeProvider;
|
|
78
|
+
const useViteTheme = () => {
|
|
79
|
+
const context = (0, react_1.useContext)(ViteThemeContext);
|
|
80
|
+
if (context === undefined) {
|
|
81
|
+
throw new Error('useViteTheme must be used within a ViteThemeProvider');
|
|
82
|
+
}
|
|
83
|
+
return context;
|
|
84
|
+
};
|
|
85
|
+
exports.useViteTheme = useViteTheme;
|
|
@@ -7,7 +7,9 @@ const types_1 = require("../types");
|
|
|
7
7
|
const animations_1 = require("../utils/animations");
|
|
8
8
|
const isBrowser = typeof window !== 'undefined';
|
|
9
9
|
const useThemeAnimation = (props = {}) => {
|
|
10
|
-
const { duration: propsDuration = 750, easing = 'ease-in-out', animationType = types_1.ThemeAnimationType.CIRCLE, blurAmount = 2, styleId = 'spaceman-theme-style', themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', globalClassName = 'dark', colorThemePrefix = 'theme-', storageKey = 'theme', colorStorageKey = 'color-theme', theme: externalTheme, colorTheme: externalColorTheme, onThemeChange, onColorThemeChange,
|
|
10
|
+
const { duration: propsDuration = 750, easing = 'ease-in-out', animationType = types_1.ThemeAnimationType.CIRCLE, blurAmount = 2, styleId = 'spaceman-theme-style', themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', globalClassName = 'dark', colorThemePrefix = 'theme-', storageKey = 'theme', colorStorageKey = 'color-theme', theme: externalTheme, colorTheme: externalColorTheme, onThemeChange, onColorThemeChange,
|
|
11
|
+
// Slide animation options
|
|
12
|
+
slideDirection = 'left', slideFromX, slideFromY, slideToX = 0, slideToY = 0, } = props;
|
|
11
13
|
const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
|
|
12
14
|
const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
|
|
13
15
|
const [mounted, setMounted] = (0, react_1.useState)(false);
|
|
@@ -100,21 +102,69 @@ const useThemeAnimation = (props = {}) => {
|
|
|
100
102
|
setTheme(newTheme);
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
// Helper function to convert direction to from coordinates
|
|
106
|
+
const getSlideFromCoords = (direction) => {
|
|
107
|
+
switch (direction) {
|
|
108
|
+
case 'left':
|
|
109
|
+
return { a: -100, b: 0 };
|
|
110
|
+
case 'right':
|
|
111
|
+
return { a: 100, b: 0 };
|
|
112
|
+
case 'top':
|
|
113
|
+
return { a: 0, b: -100 };
|
|
114
|
+
case 'bottom':
|
|
115
|
+
return { a: 0, b: 100 };
|
|
116
|
+
case 'top-left':
|
|
117
|
+
return { a: -100, b: -100 };
|
|
118
|
+
case 'top-right':
|
|
119
|
+
return { a: 100, b: -100 };
|
|
120
|
+
case 'bottom-left':
|
|
121
|
+
return { a: -100, b: 100 };
|
|
122
|
+
case 'bottom-right':
|
|
123
|
+
return { a: 100, b: 100 };
|
|
124
|
+
default:
|
|
125
|
+
return { a: -100, b: 0 };
|
|
126
|
+
}
|
|
114
127
|
};
|
|
128
|
+
let animationConfig;
|
|
129
|
+
if (animationType === types_1.ThemeAnimationType.SLIDE) {
|
|
130
|
+
// Use custom coordinates if provided, otherwise use direction
|
|
131
|
+
const fromCoords = slideFromX !== undefined && slideFromY !== undefined
|
|
132
|
+
? { a: slideFromX, b: slideFromY }
|
|
133
|
+
: getSlideFromCoords(slideDirection);
|
|
134
|
+
animationConfig = {
|
|
135
|
+
a: fromCoords.a,
|
|
136
|
+
b: fromCoords.b,
|
|
137
|
+
x: slideToX,
|
|
138
|
+
y: slideToY,
|
|
139
|
+
duration,
|
|
140
|
+
easing,
|
|
141
|
+
animationType,
|
|
142
|
+
blurAmount,
|
|
143
|
+
styleId,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// For circle and blur circle animations, use button center
|
|
148
|
+
const { top, left, width, height } = ref.current.getBoundingClientRect();
|
|
149
|
+
const x = left + width / 2;
|
|
150
|
+
const y = top + height / 2;
|
|
151
|
+
animationConfig = {
|
|
152
|
+
x,
|
|
153
|
+
y,
|
|
154
|
+
duration,
|
|
155
|
+
easing,
|
|
156
|
+
animationType,
|
|
157
|
+
blurAmount,
|
|
158
|
+
styleId,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
115
161
|
if (animationType === types_1.ThemeAnimationType.BLUR_CIRCLE) {
|
|
116
162
|
(0, animations_1.createBlurCircleAnimation)(animationConfig);
|
|
117
163
|
}
|
|
164
|
+
if (animationType === types_1.ThemeAnimationType.SLIDE) {
|
|
165
|
+
(0, animations_1.createSlideAnimation)(animationConfig);
|
|
166
|
+
}
|
|
167
|
+
// Start the view transition
|
|
118
168
|
await document.startViewTransition(() => {
|
|
119
169
|
(0, react_dom_1.flushSync)(() => {
|
|
120
170
|
setTheme(newTheme);
|
|
@@ -123,7 +173,19 @@ const useThemeAnimation = (props = {}) => {
|
|
|
123
173
|
if (animationType === types_1.ThemeAnimationType.CIRCLE) {
|
|
124
174
|
(0, animations_1.createCircleAnimation)(animationConfig);
|
|
125
175
|
}
|
|
126
|
-
}, [
|
|
176
|
+
}, [
|
|
177
|
+
setTheme,
|
|
178
|
+
duration,
|
|
179
|
+
easing,
|
|
180
|
+
animationType,
|
|
181
|
+
blurAmount,
|
|
182
|
+
styleId,
|
|
183
|
+
slideDirection,
|
|
184
|
+
slideFromX,
|
|
185
|
+
slideFromY,
|
|
186
|
+
slideToX,
|
|
187
|
+
slideToY,
|
|
188
|
+
]);
|
|
127
189
|
const toggleTheme = (0, react_1.useCallback)(async () => {
|
|
128
190
|
const newTheme = resolvedTheme === 'dark' ? 'light' : 'dark';
|
|
129
191
|
await switchTheme(newTheme);
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { useThemeAnimation } from './hooks/use-theme-animation';
|
|
|
2
2
|
export { ThemeSwitcher } from './components/ThemeSwitcher';
|
|
3
3
|
export { ThemeSelector } from './components/ThemeSelector';
|
|
4
4
|
export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
|
|
5
|
+
export { ViteThemeProvider, useViteTheme } from './components/ViteThemeProvider';
|
|
5
6
|
export type { UseThemeAnimationProps, UseThemeAnimationReturn, ThemeSwitcherProps, ThemeSelectorProps, Theme, ColorTheme, } from './types';
|
|
7
|
+
export type { ViteThemeContextType } from './components/ViteThemeProvider';
|
|
6
8
|
export { ThemeAnimationType } from './types';
|
|
7
9
|
export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.prefersReducedMotion = exports.supportsViewTransitions = exports.resolveTheme = exports.getSystemTheme = exports.injectBaseStyles = exports.ThemeAnimationType = exports.useSpacemanTheme = exports.SpacemanThemeProvider = exports.ThemeSelector = exports.ThemeSwitcher = exports.useThemeAnimation = void 0;
|
|
3
|
+
exports.prefersReducedMotion = exports.supportsViewTransitions = exports.resolveTheme = exports.getSystemTheme = exports.injectBaseStyles = exports.ThemeAnimationType = exports.useViteTheme = exports.ViteThemeProvider = exports.useSpacemanTheme = exports.SpacemanThemeProvider = exports.ThemeSelector = exports.ThemeSwitcher = exports.useThemeAnimation = void 0;
|
|
4
4
|
var use_theme_animation_1 = require("./hooks/use-theme-animation");
|
|
5
5
|
Object.defineProperty(exports, "useThemeAnimation", { enumerable: true, get: function () { return use_theme_animation_1.useThemeAnimation; } });
|
|
6
6
|
var ThemeSwitcher_1 = require("./components/ThemeSwitcher");
|
|
@@ -10,6 +10,9 @@ Object.defineProperty(exports, "ThemeSelector", { enumerable: true, get: functio
|
|
|
10
10
|
var SpacemanThemeProvider_1 = require("./components/SpacemanThemeProvider");
|
|
11
11
|
Object.defineProperty(exports, "SpacemanThemeProvider", { enumerable: true, get: function () { return SpacemanThemeProvider_1.SpacemanThemeProvider; } });
|
|
12
12
|
Object.defineProperty(exports, "useSpacemanTheme", { enumerable: true, get: function () { return SpacemanThemeProvider_1.useSpacemanTheme; } });
|
|
13
|
+
var ViteThemeProvider_1 = require("./components/ViteThemeProvider");
|
|
14
|
+
Object.defineProperty(exports, "ViteThemeProvider", { enumerable: true, get: function () { return ViteThemeProvider_1.ViteThemeProvider; } });
|
|
15
|
+
Object.defineProperty(exports, "useViteTheme", { enumerable: true, get: function () { return ViteThemeProvider_1.useViteTheme; } });
|
|
13
16
|
var types_1 = require("./types");
|
|
14
17
|
Object.defineProperty(exports, "ThemeAnimationType", { enumerable: true, get: function () { return types_1.ThemeAnimationType; } });
|
|
15
18
|
var animations_1 = require("./utils/animations");
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { RefObject } from 'react';
|
|
2
2
|
export declare enum ThemeAnimationType {
|
|
3
3
|
CIRCLE = "circle",
|
|
4
|
-
BLUR_CIRCLE = "blur-circle"
|
|
4
|
+
BLUR_CIRCLE = "blur-circle",
|
|
5
|
+
SLIDE = "slide"
|
|
5
6
|
}
|
|
7
|
+
export type SlideDirection = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
6
8
|
export type Theme = 'light' | 'dark' | 'system';
|
|
7
9
|
export type ColorTheme = string;
|
|
8
10
|
export interface UseThemeAnimationProps {
|
|
@@ -23,6 +25,11 @@ export interface UseThemeAnimationProps {
|
|
|
23
25
|
colorTheme?: ColorTheme;
|
|
24
26
|
onThemeChange?: (theme: Theme) => void;
|
|
25
27
|
onColorThemeChange?: (colorTheme: ColorTheme) => void;
|
|
28
|
+
slideDirection?: SlideDirection;
|
|
29
|
+
slideFromX?: number;
|
|
30
|
+
slideFromY?: number;
|
|
31
|
+
slideToX?: number;
|
|
32
|
+
slideToY?: number;
|
|
26
33
|
}
|
|
27
34
|
export interface UseThemeAnimationReturn {
|
|
28
35
|
ref: RefObject<HTMLButtonElement | null>;
|
package/dist/cjs/types/index.js
CHANGED
|
@@ -5,4 +5,5 @@ var ThemeAnimationType;
|
|
|
5
5
|
(function (ThemeAnimationType) {
|
|
6
6
|
ThemeAnimationType["CIRCLE"] = "circle";
|
|
7
7
|
ThemeAnimationType["BLUR_CIRCLE"] = "blur-circle";
|
|
8
|
+
ThemeAnimationType["SLIDE"] = "slide";
|
|
8
9
|
})(ThemeAnimationType || (exports.ThemeAnimationType = ThemeAnimationType = {}));
|
|
@@ -6,6 +6,8 @@ export declare const resolveTheme: (theme: string) => "light" | "dark";
|
|
|
6
6
|
export declare const supportsViewTransitions: () => boolean;
|
|
7
7
|
export declare const prefersReducedMotion: () => boolean;
|
|
8
8
|
export interface AnimationConfig {
|
|
9
|
+
a?: number;
|
|
10
|
+
b?: number;
|
|
9
11
|
x: number;
|
|
10
12
|
y: number;
|
|
11
13
|
duration: number;
|
|
@@ -15,4 +17,5 @@ export interface AnimationConfig {
|
|
|
15
17
|
styleId: string;
|
|
16
18
|
}
|
|
17
19
|
export declare const createCircleAnimation: (config: AnimationConfig) => void;
|
|
20
|
+
export declare const createSlideAnimation: (config: AnimationConfig) => void;
|
|
18
21
|
export declare const createBlurCircleAnimation: (config: AnimationConfig) => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createBlurCircleAnimation = exports.createCircleAnimation = exports.prefersReducedMotion = exports.supportsViewTransitions = exports.resolveTheme = exports.getSystemTheme = exports.createBlurCircleMask = exports.injectBaseStyles = void 0;
|
|
3
|
+
exports.createBlurCircleAnimation = exports.createSlideAnimation = exports.createCircleAnimation = exports.prefersReducedMotion = exports.supportsViewTransitions = exports.resolveTheme = exports.getSystemTheme = exports.createBlurCircleMask = exports.injectBaseStyles = void 0;
|
|
4
4
|
const isBrowser = typeof window !== 'undefined';
|
|
5
5
|
const injectBaseStyles = () => {
|
|
6
6
|
if (isBrowser) {
|
|
@@ -81,6 +81,39 @@ const createCircleAnimation = (config) => {
|
|
|
81
81
|
});
|
|
82
82
|
};
|
|
83
83
|
exports.createCircleAnimation = createCircleAnimation;
|
|
84
|
+
const createSlideAnimation = (config) => {
|
|
85
|
+
const { a = -100, b = 0, x = 0, y = 0, duration, styleId } = config;
|
|
86
|
+
const styleElement = document.createElement('style');
|
|
87
|
+
styleElement.id = styleId;
|
|
88
|
+
styleElement.textContent = `
|
|
89
|
+
::view-transition-old(root) {
|
|
90
|
+
animation-delay: ${duration}ms;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
::view-transition-new(root) {
|
|
94
|
+
animation: move-in ${duration}ms;
|
|
95
|
+
animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@keyframes move-in {
|
|
99
|
+
from {
|
|
100
|
+
translate: ${a}% ${b}%;
|
|
101
|
+
}
|
|
102
|
+
to {
|
|
103
|
+
translate: ${x}% ${y}%;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
`;
|
|
107
|
+
document.head.appendChild(styleElement);
|
|
108
|
+
// Clean up after animation
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
const styleElement = document.getElementById(styleId);
|
|
111
|
+
if (styleElement) {
|
|
112
|
+
styleElement.remove();
|
|
113
|
+
}
|
|
114
|
+
}, duration);
|
|
115
|
+
};
|
|
116
|
+
exports.createSlideAnimation = createSlideAnimation;
|
|
84
117
|
const createBlurCircleAnimation = (config) => {
|
|
85
118
|
const { x, y, duration, easing, blurAmount, styleId } = config;
|
|
86
119
|
const existingStyle = document.getElementById(styleId);
|
|
@@ -8,9 +8,9 @@ const SunIcon = () => (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 2
|
|
|
8
8
|
const MoonIcon = () => (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" }) }));
|
|
9
9
|
const MonitorIcon = () => (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { width: "20", height: "14", x: "2", y: "3", rx: "2" }), _jsx("line", { x1: "8", x2: "16", y1: "21", y2: "21" }), _jsx("line", { x1: "12", x2: "12", y1: "17", y2: "21" })] }));
|
|
10
10
|
const ThemeOption = ({ icon, value, isActive, isHovered, onClick, onMouseEnter, onMouseLeave, buttonRef, }) => {
|
|
11
|
-
return (_jsxs("button", { ref: isActive ? buttonRef : undefined, className: clsx('relative flex h-9 w-12 cursor-pointer items-center justify-center
|
|
11
|
+
return (_jsxs("button", { ref: isActive ? buttonRef : undefined, className: clsx('relative flex h-9 w-12 cursor-pointer items-center justify-center', 'text-muted-foreground hover:text-foreground', isActive && 'text-foreground font-medium'), role: "radio", "aria-checked": isActive, "aria-label": `Switch to ${value} theme`, onClick: event => onClick(value, event), onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, style: {
|
|
12
12
|
borderRadius: 'var(--radius)',
|
|
13
|
-
}, children: [isHovered && (_jsx(motion.div, { layoutId: "theme-hover
|
|
13
|
+
}, children: [isHovered && (_jsx(motion.div, { layoutId: "theme-hover", className: "absolute inset-0 bg-muted", style: {
|
|
14
14
|
borderRadius: 'var(--radius)',
|
|
15
15
|
}, transition: {
|
|
16
16
|
type: 'spring',
|
|
@@ -18,18 +18,8 @@ const ThemeOption = ({ icon, value, isActive, isHovered, onClick, onMouseEnter,
|
|
|
18
18
|
stiffness: 100,
|
|
19
19
|
damping: 10,
|
|
20
20
|
duration: 0.3,
|
|
21
|
-
} })), _jsx(
|
|
22
|
-
scale: isActive ? 1.1 : 1,
|
|
23
|
-
}, transition: {
|
|
24
|
-
type: 'spring',
|
|
25
|
-
bounce: 0.2,
|
|
26
|
-
duration: 0.4,
|
|
27
|
-
}, children: _jsx("div", { className: "[&_svg]:size-4", children: icon }) }), isActive && (_jsx(motion.div, { layoutId: "theme-active-indicator", className: "absolute bottom-0 left-1/2 h-0.5 w-6 -translate-x-1/2 rounded-full", style: {
|
|
21
|
+
} })), _jsx("div", { className: "relative z-10 flex items-center justify-center", children: _jsx("div", { className: "[&_svg]:size-4", children: icon }) }), isActive && (_jsx("div", { className: "absolute bottom-0 left-1/2 h-0.5 w-6 -translate-x-1/2 rounded-full", style: {
|
|
28
22
|
backgroundColor: 'hsl(var(--primary))',
|
|
29
|
-
}, transition: {
|
|
30
|
-
type: 'spring',
|
|
31
|
-
bounce: 0.3,
|
|
32
|
-
duration: 0.6,
|
|
33
23
|
} }))] }));
|
|
34
24
|
};
|
|
35
25
|
const THEME_OPTIONS = [
|
|
@@ -47,15 +37,11 @@ const THEME_OPTIONS = [
|
|
|
47
37
|
},
|
|
48
38
|
];
|
|
49
39
|
export const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentTheme, onThemeChange, animationType, duration, className, }) => {
|
|
50
|
-
// Try to use SpacemanTheme context if available (controlled mode)
|
|
51
40
|
let contextTheme = null;
|
|
52
41
|
try {
|
|
53
42
|
contextTheme = useSpacemanTheme();
|
|
54
43
|
}
|
|
55
|
-
catch {
|
|
56
|
-
// Context not available, use standalone mode
|
|
57
|
-
}
|
|
58
|
-
// Use context if available, otherwise fall back to standalone hook
|
|
44
|
+
catch { }
|
|
59
45
|
const standaloneHook = useThemeAnimation({
|
|
60
46
|
animationType,
|
|
61
47
|
duration,
|
|
@@ -68,29 +54,24 @@ export const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentThe
|
|
|
68
54
|
? { ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme }
|
|
69
55
|
: standaloneHook;
|
|
70
56
|
const [isMounted, setIsMounted] = useState(false);
|
|
57
|
+
const [hoveredTheme, setHoveredTheme] = useState(null);
|
|
71
58
|
useEffect(() => {
|
|
72
59
|
setIsMounted(true);
|
|
73
60
|
}, []);
|
|
74
61
|
const handleThemeChange = async (newTheme, event) => {
|
|
75
62
|
if (isControlled && contextTheme.switchThemeFromElement && event) {
|
|
76
|
-
// Use controlled mode with animation from clicked element
|
|
77
63
|
await contextTheme.switchThemeFromElement(newTheme, event.currentTarget);
|
|
78
|
-
// Also call the callback if provided
|
|
79
64
|
if (onThemeChange) {
|
|
80
65
|
onThemeChange(newTheme);
|
|
81
66
|
}
|
|
82
67
|
}
|
|
83
68
|
else {
|
|
84
|
-
// Use standalone mode
|
|
85
69
|
await switchTheme(newTheme);
|
|
86
70
|
}
|
|
87
71
|
};
|
|
88
|
-
if (!isMounted) {
|
|
89
|
-
return _jsx("div", { className: "flex h-9 w-fit" });
|
|
90
|
-
}
|
|
91
72
|
const filteredOptions = THEME_OPTIONS.filter(option => themes.includes(option.value));
|
|
92
|
-
return (_jsx(
|
|
73
|
+
return (_jsx("div", { className: clsx('inline-flex h-9 items-center overflow-hidden bg-background shadow-sm border border-border', isMounted ? 'opacity-100' : 'opacity-0', className), style: {
|
|
93
74
|
borderRadius: 'var(--radius)',
|
|
94
75
|
backgroundColor: 'hsl(var(--background))',
|
|
95
|
-
}, role: "radiogroup", children: filteredOptions.map(option => (_jsx(ThemeOption, { icon: option.icon, value: option.value, isActive: theme === option.value, isHovered:
|
|
76
|
+
}, role: "radiogroup", onMouseLeave: () => setHoveredTheme(null), children: filteredOptions.map(option => (_jsx(ThemeOption, { icon: option.icon, value: option.value, isActive: theme === option.value, isHovered: hoveredTheme === option.value, onClick: handleThemeChange, onMouseEnter: () => setHoveredTheme(option.value), onMouseLeave: () => { }, buttonRef: theme === option.value ? ref : undefined }, option.value))) }));
|
|
96
77
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type Theme = 'light' | 'dark' | 'system';
|
|
3
|
+
export interface ViteThemeContextType {
|
|
4
|
+
theme: Theme;
|
|
5
|
+
resolvedTheme: 'light' | 'dark';
|
|
6
|
+
setTheme: (theme: Theme) => void;
|
|
7
|
+
systemTheme: 'light' | 'dark';
|
|
8
|
+
}
|
|
9
|
+
interface ViteThemeProviderProps {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
attribute?: 'class' | 'data-theme';
|
|
12
|
+
defaultTheme?: Theme;
|
|
13
|
+
enableSystem?: boolean;
|
|
14
|
+
disableTransitionOnChange?: boolean;
|
|
15
|
+
storageKey?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const ViteThemeProvider: React.FC<ViteThemeProviderProps>;
|
|
18
|
+
export declare const useViteTheme: () => ViteThemeContextType;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
3
|
+
const ViteThemeContext = createContext(undefined);
|
|
4
|
+
export const ViteThemeProvider = ({ children, attribute = 'class', defaultTheme = 'system', enableSystem = true, disableTransitionOnChange = false, storageKey = 'vite-theme', }) => {
|
|
5
|
+
const [mounted, setMounted] = useState(false);
|
|
6
|
+
const [systemTheme, setSystemTheme] = useState('light');
|
|
7
|
+
const [theme, setThemeState] = useState(() => {
|
|
8
|
+
if (typeof window === 'undefined')
|
|
9
|
+
return defaultTheme;
|
|
10
|
+
try {
|
|
11
|
+
const savedTheme = localStorage.getItem(storageKey);
|
|
12
|
+
return savedTheme && ['light', 'dark', 'system'].includes(savedTheme)
|
|
13
|
+
? savedTheme
|
|
14
|
+
: defaultTheme;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return defaultTheme;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!enableSystem)
|
|
22
|
+
return;
|
|
23
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
24
|
+
const updateSystemTheme = () => {
|
|
25
|
+
setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
|
|
26
|
+
};
|
|
27
|
+
updateSystemTheme();
|
|
28
|
+
mediaQuery.addEventListener('change', updateSystemTheme);
|
|
29
|
+
return () => mediaQuery.removeEventListener('change', updateSystemTheme);
|
|
30
|
+
}, [enableSystem]);
|
|
31
|
+
const resolvedTheme = theme === 'system' ? systemTheme : theme;
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!mounted)
|
|
34
|
+
return;
|
|
35
|
+
const root = document.documentElement;
|
|
36
|
+
if (disableTransitionOnChange) {
|
|
37
|
+
const css = document.createElement('style');
|
|
38
|
+
css.textContent = `*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`;
|
|
39
|
+
document.head.appendChild(css);
|
|
40
|
+
window.getComputedStyle(document.body);
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
document.head.removeChild(css);
|
|
43
|
+
}, 1);
|
|
44
|
+
}
|
|
45
|
+
if (attribute === 'class') {
|
|
46
|
+
root.classList.remove('light', 'dark');
|
|
47
|
+
root.classList.add(resolvedTheme);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
root.setAttribute(attribute, resolvedTheme);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
localStorage.setItem(storageKey, theme);
|
|
54
|
+
}
|
|
55
|
+
catch { }
|
|
56
|
+
}, [theme, resolvedTheme, mounted, attribute, disableTransitionOnChange, storageKey]);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setMounted(true);
|
|
59
|
+
}, []);
|
|
60
|
+
const setTheme = useCallback((newTheme) => {
|
|
61
|
+
setThemeState(newTheme);
|
|
62
|
+
}, []);
|
|
63
|
+
if (!mounted) {
|
|
64
|
+
return _jsx(_Fragment, { children: children });
|
|
65
|
+
}
|
|
66
|
+
const value = {
|
|
67
|
+
theme,
|
|
68
|
+
resolvedTheme,
|
|
69
|
+
setTheme,
|
|
70
|
+
systemTheme,
|
|
71
|
+
};
|
|
72
|
+
return _jsx(ViteThemeContext.Provider, { value: value, children: children });
|
|
73
|
+
};
|
|
74
|
+
export const useViteTheme = () => {
|
|
75
|
+
const context = useContext(ViteThemeContext);
|
|
76
|
+
if (context === undefined) {
|
|
77
|
+
throw new Error('useViteTheme must be used within a ViteThemeProvider');
|
|
78
|
+
}
|
|
79
|
+
return context;
|
|
80
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
2
|
import { flushSync } from 'react-dom';
|
|
3
3
|
import { ThemeAnimationType, } from '../types';
|
|
4
|
-
import { injectBaseStyles, resolveTheme, getSystemTheme, supportsViewTransitions, prefersReducedMotion, createCircleAnimation, createBlurCircleAnimation, } from '../utils/animations';
|
|
4
|
+
import { injectBaseStyles, resolveTheme, getSystemTheme, supportsViewTransitions, prefersReducedMotion, createCircleAnimation, createBlurCircleAnimation, createSlideAnimation, } from '../utils/animations';
|
|
5
5
|
const isBrowser = typeof window !== 'undefined';
|
|
6
6
|
export const useThemeAnimation = (props = {}) => {
|
|
7
|
-
const { duration: propsDuration = 750, easing = 'ease-in-out', animationType = ThemeAnimationType.CIRCLE, blurAmount = 2, styleId = 'spaceman-theme-style', themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', globalClassName = 'dark', colorThemePrefix = 'theme-', storageKey = 'theme', colorStorageKey = 'color-theme', theme: externalTheme, colorTheme: externalColorTheme, onThemeChange, onColorThemeChange,
|
|
7
|
+
const { duration: propsDuration = 750, easing = 'ease-in-out', animationType = ThemeAnimationType.CIRCLE, blurAmount = 2, styleId = 'spaceman-theme-style', themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', globalClassName = 'dark', colorThemePrefix = 'theme-', storageKey = 'theme', colorStorageKey = 'color-theme', theme: externalTheme, colorTheme: externalColorTheme, onThemeChange, onColorThemeChange,
|
|
8
|
+
// Slide animation options
|
|
9
|
+
slideDirection = 'left', slideFromX, slideFromY, slideToX = 0, slideToY = 0, } = props;
|
|
8
10
|
const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
|
|
9
11
|
const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
|
|
10
12
|
const [mounted, setMounted] = useState(false);
|
|
@@ -97,21 +99,69 @@ export const useThemeAnimation = (props = {}) => {
|
|
|
97
99
|
setTheme(newTheme);
|
|
98
100
|
return;
|
|
99
101
|
}
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
// Helper function to convert direction to from coordinates
|
|
103
|
+
const getSlideFromCoords = (direction) => {
|
|
104
|
+
switch (direction) {
|
|
105
|
+
case 'left':
|
|
106
|
+
return { a: -100, b: 0 };
|
|
107
|
+
case 'right':
|
|
108
|
+
return { a: 100, b: 0 };
|
|
109
|
+
case 'top':
|
|
110
|
+
return { a: 0, b: -100 };
|
|
111
|
+
case 'bottom':
|
|
112
|
+
return { a: 0, b: 100 };
|
|
113
|
+
case 'top-left':
|
|
114
|
+
return { a: -100, b: -100 };
|
|
115
|
+
case 'top-right':
|
|
116
|
+
return { a: 100, b: -100 };
|
|
117
|
+
case 'bottom-left':
|
|
118
|
+
return { a: -100, b: 100 };
|
|
119
|
+
case 'bottom-right':
|
|
120
|
+
return { a: 100, b: 100 };
|
|
121
|
+
default:
|
|
122
|
+
return { a: -100, b: 0 };
|
|
123
|
+
}
|
|
111
124
|
};
|
|
125
|
+
let animationConfig;
|
|
126
|
+
if (animationType === ThemeAnimationType.SLIDE) {
|
|
127
|
+
// Use custom coordinates if provided, otherwise use direction
|
|
128
|
+
const fromCoords = slideFromX !== undefined && slideFromY !== undefined
|
|
129
|
+
? { a: slideFromX, b: slideFromY }
|
|
130
|
+
: getSlideFromCoords(slideDirection);
|
|
131
|
+
animationConfig = {
|
|
132
|
+
a: fromCoords.a,
|
|
133
|
+
b: fromCoords.b,
|
|
134
|
+
x: slideToX,
|
|
135
|
+
y: slideToY,
|
|
136
|
+
duration,
|
|
137
|
+
easing,
|
|
138
|
+
animationType,
|
|
139
|
+
blurAmount,
|
|
140
|
+
styleId,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
// For circle and blur circle animations, use button center
|
|
145
|
+
const { top, left, width, height } = ref.current.getBoundingClientRect();
|
|
146
|
+
const x = left + width / 2;
|
|
147
|
+
const y = top + height / 2;
|
|
148
|
+
animationConfig = {
|
|
149
|
+
x,
|
|
150
|
+
y,
|
|
151
|
+
duration,
|
|
152
|
+
easing,
|
|
153
|
+
animationType,
|
|
154
|
+
blurAmount,
|
|
155
|
+
styleId,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
112
158
|
if (animationType === ThemeAnimationType.BLUR_CIRCLE) {
|
|
113
159
|
createBlurCircleAnimation(animationConfig);
|
|
114
160
|
}
|
|
161
|
+
if (animationType === ThemeAnimationType.SLIDE) {
|
|
162
|
+
createSlideAnimation(animationConfig);
|
|
163
|
+
}
|
|
164
|
+
// Start the view transition
|
|
115
165
|
await document.startViewTransition(() => {
|
|
116
166
|
flushSync(() => {
|
|
117
167
|
setTheme(newTheme);
|
|
@@ -120,7 +170,19 @@ export const useThemeAnimation = (props = {}) => {
|
|
|
120
170
|
if (animationType === ThemeAnimationType.CIRCLE) {
|
|
121
171
|
createCircleAnimation(animationConfig);
|
|
122
172
|
}
|
|
123
|
-
}, [
|
|
173
|
+
}, [
|
|
174
|
+
setTheme,
|
|
175
|
+
duration,
|
|
176
|
+
easing,
|
|
177
|
+
animationType,
|
|
178
|
+
blurAmount,
|
|
179
|
+
styleId,
|
|
180
|
+
slideDirection,
|
|
181
|
+
slideFromX,
|
|
182
|
+
slideFromY,
|
|
183
|
+
slideToX,
|
|
184
|
+
slideToY,
|
|
185
|
+
]);
|
|
124
186
|
const toggleTheme = useCallback(async () => {
|
|
125
187
|
const newTheme = resolvedTheme === 'dark' ? 'light' : 'dark';
|
|
126
188
|
await switchTheme(newTheme);
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { useThemeAnimation } from './hooks/use-theme-animation';
|
|
|
2
2
|
export { ThemeSwitcher } from './components/ThemeSwitcher';
|
|
3
3
|
export { ThemeSelector } from './components/ThemeSelector';
|
|
4
4
|
export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
|
|
5
|
+
export { ViteThemeProvider, useViteTheme } from './components/ViteThemeProvider';
|
|
5
6
|
export type { UseThemeAnimationProps, UseThemeAnimationReturn, ThemeSwitcherProps, ThemeSelectorProps, Theme, ColorTheme, } from './types';
|
|
7
|
+
export type { ViteThemeContextType } from './components/ViteThemeProvider';
|
|
6
8
|
export { ThemeAnimationType } from './types';
|
|
7
9
|
export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
|
package/dist/esm/index.js
CHANGED
|
@@ -2,5 +2,6 @@ export { useThemeAnimation } from './hooks/use-theme-animation';
|
|
|
2
2
|
export { ThemeSwitcher } from './components/ThemeSwitcher';
|
|
3
3
|
export { ThemeSelector } from './components/ThemeSelector';
|
|
4
4
|
export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
|
|
5
|
+
export { ViteThemeProvider, useViteTheme } from './components/ViteThemeProvider';
|
|
5
6
|
export { ThemeAnimationType } from './types';
|
|
6
7
|
export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { RefObject } from 'react';
|
|
2
2
|
export declare enum ThemeAnimationType {
|
|
3
3
|
CIRCLE = "circle",
|
|
4
|
-
BLUR_CIRCLE = "blur-circle"
|
|
4
|
+
BLUR_CIRCLE = "blur-circle",
|
|
5
|
+
SLIDE = "slide"
|
|
5
6
|
}
|
|
7
|
+
export type SlideDirection = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
6
8
|
export type Theme = 'light' | 'dark' | 'system';
|
|
7
9
|
export type ColorTheme = string;
|
|
8
10
|
export interface UseThemeAnimationProps {
|
|
@@ -23,6 +25,11 @@ export interface UseThemeAnimationProps {
|
|
|
23
25
|
colorTheme?: ColorTheme;
|
|
24
26
|
onThemeChange?: (theme: Theme) => void;
|
|
25
27
|
onColorThemeChange?: (colorTheme: ColorTheme) => void;
|
|
28
|
+
slideDirection?: SlideDirection;
|
|
29
|
+
slideFromX?: number;
|
|
30
|
+
slideFromY?: number;
|
|
31
|
+
slideToX?: number;
|
|
32
|
+
slideToY?: number;
|
|
26
33
|
}
|
|
27
34
|
export interface UseThemeAnimationReturn {
|
|
28
35
|
ref: RefObject<HTMLButtonElement | null>;
|
package/dist/esm/types/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export declare const resolveTheme: (theme: string) => "light" | "dark";
|
|
|
6
6
|
export declare const supportsViewTransitions: () => boolean;
|
|
7
7
|
export declare const prefersReducedMotion: () => boolean;
|
|
8
8
|
export interface AnimationConfig {
|
|
9
|
+
a?: number;
|
|
10
|
+
b?: number;
|
|
9
11
|
x: number;
|
|
10
12
|
y: number;
|
|
11
13
|
duration: number;
|
|
@@ -15,4 +17,5 @@ export interface AnimationConfig {
|
|
|
15
17
|
styleId: string;
|
|
16
18
|
}
|
|
17
19
|
export declare const createCircleAnimation: (config: AnimationConfig) => void;
|
|
20
|
+
export declare const createSlideAnimation: (config: AnimationConfig) => void;
|
|
18
21
|
export declare const createBlurCircleAnimation: (config: AnimationConfig) => void;
|
|
@@ -71,6 +71,38 @@ export const createCircleAnimation = (config) => {
|
|
|
71
71
|
pseudoElement: '::view-transition-new(root)',
|
|
72
72
|
});
|
|
73
73
|
};
|
|
74
|
+
export const createSlideAnimation = (config) => {
|
|
75
|
+
const { a = -100, b = 0, x = 0, y = 0, duration, styleId } = config;
|
|
76
|
+
const styleElement = document.createElement('style');
|
|
77
|
+
styleElement.id = styleId;
|
|
78
|
+
styleElement.textContent = `
|
|
79
|
+
::view-transition-old(root) {
|
|
80
|
+
animation-delay: ${duration}ms;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
::view-transition-new(root) {
|
|
84
|
+
animation: move-in ${duration}ms;
|
|
85
|
+
animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@keyframes move-in {
|
|
89
|
+
from {
|
|
90
|
+
translate: ${a}% ${b}%;
|
|
91
|
+
}
|
|
92
|
+
to {
|
|
93
|
+
translate: ${x}% ${y}%;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
document.head.appendChild(styleElement);
|
|
98
|
+
// Clean up after animation
|
|
99
|
+
setTimeout(() => {
|
|
100
|
+
const styleElement = document.getElementById(styleId);
|
|
101
|
+
if (styleElement) {
|
|
102
|
+
styleElement.remove();
|
|
103
|
+
}
|
|
104
|
+
}, duration);
|
|
105
|
+
};
|
|
74
106
|
export const createBlurCircleAnimation = (config) => {
|
|
75
107
|
const { x, y, duration, easing, blurAmount, styleId } = config;
|
|
76
108
|
const existingStyle = document.getElementById(styleId);
|