@space-man/react-theme-animation 1.0.0 → 1.0.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 +8 -247
- package/dist/cjs/components/SpacemanThemeProvider.js +21 -13
- package/dist/cjs/hooks/use-theme-animation.js +26 -17
- package/dist/esm/components/SpacemanThemeProvider.js +22 -14
- package/dist/esm/hooks/use-theme-animation.js +26 -17
- package/package.json +10 -2
- package/dist/components/SpacemanThemeProvider.d.ts +0 -51
- package/dist/components/SpacemanThemeProvider.js +0 -88
- package/dist/components/ThemeSelector.d.ts +0 -3
- package/dist/components/ThemeSelector.js +0 -35
- package/dist/components/ThemeSwitcher.d.ts +0 -3
- package/dist/components/ThemeSwitcher.js +0 -96
- package/dist/components/ui/select.d.ts +0 -15
- package/dist/components/ui/select.js +0 -39
- package/dist/hooks/use-theme-animation.d.ts +0 -2
- package/dist/hooks/use-theme-animation.js +0 -145
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -6
- package/dist/types/index.d.ts +0 -65
- package/dist/types/index.js +0 -5
- package/dist/utils/animations.d.ts +0 -18
- package/dist/utils/animations.js +0 -145
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @space-man/react-theme-animation
|
|
2
2
|
|
|
3
3
|
React theme switching with smooth view transition animations, multi-theme support, and synchronized state management.
|
|
4
4
|
|
|
@@ -20,7 +20,7 @@ React theme switching with smooth view transition animations, multi-theme suppor
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npm install @
|
|
23
|
+
npm install @space-man/react-theme-animation
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## Usage Patterns
|
|
@@ -30,6 +30,10 @@ This library supports two main usage patterns:
|
|
|
30
30
|
1. **Hook-Only Usage**: Direct hook usage for custom implementations
|
|
31
31
|
2. **Provider Pattern**: Centralized state management (recommended)
|
|
32
32
|
|
|
33
|
+
📚 **[See the complete Getting Started Guide](./getting-started.md)** for detailed examples, advanced configurations, and best practices.
|
|
34
|
+
|
|
35
|
+
🚀 **[View the Code Example](./example/)** to see this package in action with a complete implementation of both hook and provider pattern.
|
|
36
|
+
|
|
33
37
|
---
|
|
34
38
|
|
|
35
39
|
## 1. Hook-Only Usage For Light and Dark mode animation
|
|
@@ -39,7 +43,7 @@ Perfect for custom theme toggle buttons and complete control over animated theme
|
|
|
39
43
|
### Basic Theme Toggle
|
|
40
44
|
|
|
41
45
|
```tsx
|
|
42
|
-
import { useThemeAnimation } from '@
|
|
46
|
+
import { useThemeAnimation } from '@space-man/react-theme-animation';
|
|
43
47
|
|
|
44
48
|
function ThemeToggle() {
|
|
45
49
|
const { theme, toggleTheme, ref } = useThemeAnimation();
|
|
@@ -65,7 +69,7 @@ import {
|
|
|
65
69
|
SpacemanThemeProvider,
|
|
66
70
|
ThemeSwitcher,
|
|
67
71
|
ThemeSelector,
|
|
68
|
-
} from '@
|
|
72
|
+
} from '@space-man/react-theme-animation';
|
|
69
73
|
|
|
70
74
|
function App() {
|
|
71
75
|
return (
|
|
@@ -88,249 +92,6 @@ function App() {
|
|
|
88
92
|
}
|
|
89
93
|
```
|
|
90
94
|
|
|
91
|
-
### Advanced Provider Configuration
|
|
92
|
-
|
|
93
|
-
```tsx
|
|
94
|
-
import {
|
|
95
|
-
SpacemanThemeProvider,
|
|
96
|
-
ThemeSwitcher,
|
|
97
|
-
ThemeSelector,
|
|
98
|
-
ThemeAnimationType,
|
|
99
|
-
} from '@spaceman/react-theme-animation';
|
|
100
|
-
|
|
101
|
-
function App() {
|
|
102
|
-
return (
|
|
103
|
-
<SpacemanThemeProvider
|
|
104
|
-
defaultTheme="system"
|
|
105
|
-
defaultColorTheme="blue"
|
|
106
|
-
themes={['light', 'dark', 'system']}
|
|
107
|
-
colorThemes={['default', 'blue', 'green', 'purple', 'red']}
|
|
108
|
-
animationType={ThemeAnimationType.BLUR_CIRCLE}
|
|
109
|
-
blurAmount={10}
|
|
110
|
-
duration={1000}
|
|
111
|
-
onThemeChange={theme => {
|
|
112
|
-
// Analytics, localStorage, etc.
|
|
113
|
-
console.log('Global theme changed:', theme);
|
|
114
|
-
}}
|
|
115
|
-
onColorThemeChange={colorTheme => {
|
|
116
|
-
console.log('Global color theme changed:', colorTheme);
|
|
117
|
-
}}
|
|
118
|
-
>
|
|
119
|
-
<AppLayout />
|
|
120
|
-
</SpacemanThemeProvider>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function AppLayout() {
|
|
125
|
-
return (
|
|
126
|
-
<div className="app-layout">
|
|
127
|
-
<Header />
|
|
128
|
-
<Sidebar />
|
|
129
|
-
<MainContent />
|
|
130
|
-
</div>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function Header() {
|
|
135
|
-
return (
|
|
136
|
-
<header className="header">
|
|
137
|
-
<h1>My App</h1>
|
|
138
|
-
<ThemeSwitcher className="ml-auto" />
|
|
139
|
-
</header>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function Sidebar() {
|
|
144
|
-
return (
|
|
145
|
-
<aside className="sidebar">
|
|
146
|
-
<h2>Settings</h2>
|
|
147
|
-
<ThemeSelector colorThemes={['default', 'blue', 'green', 'purple', 'red']} />
|
|
148
|
-
</aside>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### Using the Provider Hook
|
|
154
|
-
|
|
155
|
-
Access theme state anywhere in your component tree. This hook also enables you to set a reference point for the animation to start from.
|
|
156
|
-
|
|
157
|
-
```tsx
|
|
158
|
-
import { useSpacemanTheme } from '@spaceman/react-theme-animation';
|
|
159
|
-
|
|
160
|
-
function ThemeStatus() {
|
|
161
|
-
const { theme, colorTheme, switchTheme, setColorTheme, switchThemeFromElement } =
|
|
162
|
-
useSpacemanTheme();
|
|
163
|
-
|
|
164
|
-
const handleCustomThemeSwitch = event => {
|
|
165
|
-
// Animate from the clicked element
|
|
166
|
-
switchThemeFromElement('dark', event.currentTarget);
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
return (
|
|
170
|
-
<div className="theme-status">
|
|
171
|
-
<p>Current theme: {theme}</p>
|
|
172
|
-
<p>Current color: {colorTheme}</p>
|
|
173
|
-
|
|
174
|
-
<button onClick={handleCustomThemeSwitch}>Switch to Dark (with animation)</button>
|
|
175
|
-
</div>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
## Complete Example: Advanced Theme System
|
|
183
|
-
|
|
184
|
-
Here's a complete example showcasing all features:
|
|
185
|
-
|
|
186
|
-
```tsx
|
|
187
|
-
import React from 'react';
|
|
188
|
-
import {
|
|
189
|
-
SpacemanThemeProvider,
|
|
190
|
-
ThemeSwitcher,
|
|
191
|
-
ThemeSelector,
|
|
192
|
-
useSpacemanTheme,
|
|
193
|
-
ThemeAnimationType,
|
|
194
|
-
} from '@spaceman/react-theme-animation';
|
|
195
|
-
|
|
196
|
-
// Main App with Provider
|
|
197
|
-
function App() {
|
|
198
|
-
return (
|
|
199
|
-
<SpacemanThemeProvider
|
|
200
|
-
defaultTheme="system"
|
|
201
|
-
defaultColorTheme="blue"
|
|
202
|
-
themes={['light', 'dark', 'system']}
|
|
203
|
-
colorThemes={['default', 'blue', 'green', 'purple', 'red', 'orange']}
|
|
204
|
-
animationType={ThemeAnimationType.CIRCLE}
|
|
205
|
-
duration={800}
|
|
206
|
-
onThemeChange={theme => {
|
|
207
|
-
// Save to localStorage, analytics, etc.
|
|
208
|
-
localStorage.setItem('preferred-theme', theme);
|
|
209
|
-
}}
|
|
210
|
-
onColorThemeChange={colorTheme => {
|
|
211
|
-
localStorage.setItem('preferred-color-theme', colorTheme);
|
|
212
|
-
}}
|
|
213
|
-
>
|
|
214
|
-
<AppContent />
|
|
215
|
-
</SpacemanThemeProvider>
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// App Layout
|
|
220
|
-
function AppContent() {
|
|
221
|
-
return (
|
|
222
|
-
<div className="min-h-screen bg-background text-foreground">
|
|
223
|
-
<Header />
|
|
224
|
-
<div className="container mx-auto px-4 py-8">
|
|
225
|
-
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
|
226
|
-
<Sidebar />
|
|
227
|
-
<MainContent />
|
|
228
|
-
</div>
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Header with Theme Switcher
|
|
235
|
-
function Header() {
|
|
236
|
-
return (
|
|
237
|
-
<header className="border-b bg-card">
|
|
238
|
-
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
|
239
|
-
<h1 className="text-2xl font-bold">Theme Demo</h1>
|
|
240
|
-
<div className="flex items-center gap-4">
|
|
241
|
-
<ThemeSwitcher className="rounded-lg" />
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
</header>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Sidebar with Theme Selector
|
|
249
|
-
function Sidebar() {
|
|
250
|
-
return (
|
|
251
|
-
<aside className="space-y-6">
|
|
252
|
-
<div className="bg-card p-6 rounded-lg border">
|
|
253
|
-
<h2 className="text-lg font-semibold mb-4">Theme Settings</h2>
|
|
254
|
-
<div className="space-y-4">
|
|
255
|
-
<div>
|
|
256
|
-
<label className="text-sm font-medium mb-2 block">Color Theme</label>
|
|
257
|
-
<ThemeSelector colorThemes={['default', 'blue', 'green', 'purple', 'red', 'orange']} />
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
|
|
262
|
-
<ThemeInfo />
|
|
263
|
-
</aside>
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Component using the theme hook
|
|
268
|
-
function ThemeInfo() {
|
|
269
|
-
const { theme, colorTheme, switchThemeFromElement } = useSpacemanTheme();
|
|
270
|
-
|
|
271
|
-
const quickSwitchTheme = (newTheme, event) => {
|
|
272
|
-
switchThemeFromElement(newTheme, event.currentTarget);
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
return (
|
|
276
|
-
<div className="bg-card p-6 rounded-lg border">
|
|
277
|
-
<h3 className="text-lg font-semibold mb-4">Current State</h3>
|
|
278
|
-
<div className="space-y-2 text-sm">
|
|
279
|
-
<p>
|
|
280
|
-
<strong>Theme:</strong> {theme}
|
|
281
|
-
</p>
|
|
282
|
-
<p>
|
|
283
|
-
<strong>Color:</strong> {colorTheme}
|
|
284
|
-
</p>
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
|
-
<div className="mt-4 space-y-2">
|
|
288
|
-
<p className="text-sm font-medium">Quick Switch:</p>
|
|
289
|
-
<div className="flex gap-2">
|
|
290
|
-
{['light', 'dark'].map(t => (
|
|
291
|
-
<button
|
|
292
|
-
key={t}
|
|
293
|
-
onClick={e => quickSwitchTheme(t, e)}
|
|
294
|
-
className="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors"
|
|
295
|
-
>
|
|
296
|
-
{t}
|
|
297
|
-
</button>
|
|
298
|
-
))}
|
|
299
|
-
</div>
|
|
300
|
-
</div>
|
|
301
|
-
</div>
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Main content area
|
|
306
|
-
function MainContent() {
|
|
307
|
-
return (
|
|
308
|
-
<main className="lg:col-span-3 space-y-6">
|
|
309
|
-
<div className="bg-card p-6 rounded-lg border">
|
|
310
|
-
<h2 className="text-xl font-semibold mb-4">Welcome to Theme Demo</h2>
|
|
311
|
-
<p className="text-muted-foreground">
|
|
312
|
-
This demo showcases the spaceman-react-theme-animation library with synchronized theme
|
|
313
|
-
state, smooth animations, and multiple color themes.
|
|
314
|
-
</p>
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
318
|
-
{[1, 2, 3, 4].map(i => (
|
|
319
|
-
<div key={i} className="bg-card p-6 rounded-lg border">
|
|
320
|
-
<h3 className="font-semibold mb-2">Card {i}</h3>
|
|
321
|
-
<p className="text-sm text-muted-foreground">
|
|
322
|
-
Sample content that adapts to the current theme and color scheme.
|
|
323
|
-
</p>
|
|
324
|
-
</div>
|
|
325
|
-
))}
|
|
326
|
-
</div>
|
|
327
|
-
</main>
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export default App;
|
|
332
|
-
```
|
|
333
|
-
|
|
334
95
|
---
|
|
335
96
|
|
|
336
97
|
## API Reference
|
|
@@ -21,7 +21,7 @@ const SpacemanThemeContext = (0, react_1.createContext)(undefined);
|
|
|
21
21
|
* @param duration - Animation duration in milliseconds (default: 750)
|
|
22
22
|
*/
|
|
23
23
|
const SpacemanThemeProvider = ({ children, themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', animationType = types_1.ThemeAnimationType.CIRCLE, duration = 750, }) => {
|
|
24
|
-
|
|
24
|
+
const [mounted, setMounted] = (0, react_1.useState)(false);
|
|
25
25
|
const themeState = (0, use_theme_animation_1.useThemeAnimation)({
|
|
26
26
|
themes,
|
|
27
27
|
colorThemes,
|
|
@@ -30,39 +30,47 @@ const SpacemanThemeProvider = ({ children, themes = ['light', 'dark', 'system'],
|
|
|
30
30
|
animationType,
|
|
31
31
|
duration,
|
|
32
32
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* @param theme - Theme to switch to
|
|
37
|
-
* @param element - HTML button element to use as animation origin
|
|
38
|
-
*/
|
|
33
|
+
(0, react_1.useEffect)(() => {
|
|
34
|
+
setMounted(true);
|
|
35
|
+
}, []);
|
|
39
36
|
const switchThemeFromElement = async (theme, element) => {
|
|
40
|
-
// Temporarily set the ref to the clicked element
|
|
41
37
|
if (themeState.ref.current) {
|
|
42
38
|
const originalRef = themeState.ref.current;
|
|
43
|
-
// Use Object.defineProperty to temporarily override the ref
|
|
44
39
|
Object.defineProperty(themeState.ref, 'current', {
|
|
45
40
|
value: element,
|
|
46
41
|
writable: true,
|
|
47
|
-
configurable: true
|
|
42
|
+
configurable: true,
|
|
48
43
|
});
|
|
49
44
|
await themeState.switchTheme(theme);
|
|
50
|
-
// Restore the original ref after animation
|
|
51
45
|
Object.defineProperty(themeState.ref, 'current', {
|
|
52
46
|
value: originalRef,
|
|
53
47
|
writable: true,
|
|
54
|
-
configurable: true
|
|
48
|
+
configurable: true,
|
|
55
49
|
});
|
|
56
50
|
}
|
|
57
51
|
else {
|
|
58
52
|
Object.defineProperty(themeState.ref, 'current', {
|
|
59
53
|
value: element,
|
|
60
54
|
writable: true,
|
|
61
|
-
configurable: true
|
|
55
|
+
configurable: true,
|
|
62
56
|
});
|
|
63
57
|
await themeState.switchTheme(theme);
|
|
64
58
|
}
|
|
65
59
|
};
|
|
60
|
+
if (!mounted) {
|
|
61
|
+
const loadingContextValue = {
|
|
62
|
+
theme: defaultTheme,
|
|
63
|
+
colorTheme: defaultColorTheme,
|
|
64
|
+
resolvedTheme: defaultTheme === 'dark' ? 'dark' : 'light',
|
|
65
|
+
setTheme: () => { },
|
|
66
|
+
setColorTheme: () => { },
|
|
67
|
+
toggleTheme: async () => { },
|
|
68
|
+
switchTheme: async () => { },
|
|
69
|
+
switchThemeFromElement: async () => { },
|
|
70
|
+
ref: { current: null },
|
|
71
|
+
};
|
|
72
|
+
return ((0, jsx_runtime_1.jsx)(SpacemanThemeContext.Provider, { value: loadingContextValue, children: children }));
|
|
73
|
+
}
|
|
66
74
|
const contextValue = {
|
|
67
75
|
theme: themeState.theme,
|
|
68
76
|
colorTheme: themeState.colorTheme,
|
|
@@ -10,21 +10,35 @@ const useThemeAnimation = (props = {}) => {
|
|
|
10
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, } = props;
|
|
11
11
|
const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
|
|
12
12
|
const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
|
|
13
|
+
const [mounted, setMounted] = (0, react_1.useState)(false);
|
|
13
14
|
(0, react_1.useEffect)(() => {
|
|
14
15
|
(0, animations_1.injectBaseStyles)();
|
|
15
16
|
}, []);
|
|
16
17
|
const [internalTheme, setInternalTheme] = (0, react_1.useState)(() => {
|
|
17
18
|
if (!isBrowser)
|
|
18
19
|
return defaultTheme;
|
|
19
|
-
|
|
20
|
-
return saved && themes.indexOf(saved) !== -1 ? saved : defaultTheme;
|
|
20
|
+
return defaultTheme;
|
|
21
21
|
});
|
|
22
22
|
const [internalColorTheme, setInternalColorTheme] = (0, react_1.useState)(() => {
|
|
23
23
|
if (!isBrowser)
|
|
24
24
|
return defaultColorTheme;
|
|
25
|
-
|
|
26
|
-
return saved && colorThemes.indexOf(saved) !== -1 ? saved : defaultColorTheme;
|
|
25
|
+
return defaultColorTheme;
|
|
27
26
|
});
|
|
27
|
+
(0, react_1.useEffect)(() => {
|
|
28
|
+
setMounted(true);
|
|
29
|
+
if (isBrowser && !externalTheme) {
|
|
30
|
+
const saved = localStorage.getItem(storageKey);
|
|
31
|
+
if (saved && themes.indexOf(saved) !== -1) {
|
|
32
|
+
setInternalTheme(saved);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (isBrowser && !externalColorTheme) {
|
|
36
|
+
const saved = localStorage.getItem(colorStorageKey);
|
|
37
|
+
if (saved && colorThemes.indexOf(saved) !== -1) {
|
|
38
|
+
setInternalColorTheme(saved);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
28
42
|
const currentTheme = externalTheme ?? internalTheme;
|
|
29
43
|
const currentColorTheme = externalColorTheme ?? internalColorTheme;
|
|
30
44
|
const [, setSystemTheme] = (0, react_1.useState)(() => (0, animations_1.getSystemTheme)());
|
|
@@ -39,23 +53,19 @@ const useThemeAnimation = (props = {}) => {
|
|
|
39
53
|
}, []);
|
|
40
54
|
// Apply theme classes to DOM
|
|
41
55
|
(0, react_1.useEffect)(() => {
|
|
42
|
-
if (!isBrowser)
|
|
56
|
+
if (!isBrowser || !mounted)
|
|
43
57
|
return;
|
|
44
|
-
// Apply theme class (light/dark) - use resolvedTheme instead of manual resolution
|
|
45
58
|
if (resolvedTheme === 'dark') {
|
|
46
59
|
document.documentElement.classList.add(globalClassName);
|
|
47
60
|
}
|
|
48
61
|
else {
|
|
49
62
|
document.documentElement.classList.remove(globalClassName);
|
|
50
63
|
}
|
|
51
|
-
// Apply color theme classes
|
|
52
|
-
// Remove all existing color theme classes
|
|
53
64
|
colorThemes.forEach(theme => {
|
|
54
65
|
document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
|
|
55
66
|
});
|
|
56
|
-
// Add current color theme class
|
|
57
67
|
document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
58
|
-
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
68
|
+
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes, mounted]);
|
|
59
69
|
const ref = (0, react_1.useRef)(null);
|
|
60
70
|
const setTheme = (0, react_1.useCallback)((newTheme) => {
|
|
61
71
|
// Always save to localStorage
|
|
@@ -119,22 +129,21 @@ const useThemeAnimation = (props = {}) => {
|
|
|
119
129
|
await switchTheme(newTheme);
|
|
120
130
|
}, [resolvedTheme, switchTheme]);
|
|
121
131
|
(0, react_1.useEffect)(() => {
|
|
122
|
-
if (!isBrowser)
|
|
132
|
+
if (!isBrowser || !mounted)
|
|
123
133
|
return;
|
|
124
|
-
const root = document.documentElement;
|
|
125
134
|
if (resolvedTheme === 'dark') {
|
|
126
|
-
|
|
135
|
+
document.documentElement.classList.add(globalClassName);
|
|
127
136
|
}
|
|
128
137
|
else {
|
|
129
|
-
|
|
138
|
+
document.documentElement.classList.remove(globalClassName);
|
|
130
139
|
}
|
|
131
140
|
colorThemes.forEach(theme => {
|
|
132
|
-
|
|
141
|
+
document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
|
|
133
142
|
});
|
|
134
143
|
if (currentColorTheme !== 'default') {
|
|
135
|
-
|
|
144
|
+
document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
136
145
|
}
|
|
137
|
-
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
146
|
+
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes, mounted]);
|
|
138
147
|
return {
|
|
139
148
|
ref,
|
|
140
149
|
theme: currentTheme,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext } from 'react';
|
|
2
|
+
import { createContext, useContext, useState, useEffect } from 'react';
|
|
3
3
|
import { useThemeAnimation } from '../hooks/use-theme-animation';
|
|
4
4
|
import { ThemeAnimationType } from '../types';
|
|
5
5
|
const SpacemanThemeContext = createContext(undefined);
|
|
@@ -18,7 +18,7 @@ const SpacemanThemeContext = createContext(undefined);
|
|
|
18
18
|
* @param duration - Animation duration in milliseconds (default: 750)
|
|
19
19
|
*/
|
|
20
20
|
export const SpacemanThemeProvider = ({ children, themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', animationType = ThemeAnimationType.CIRCLE, duration = 750, }) => {
|
|
21
|
-
|
|
21
|
+
const [mounted, setMounted] = useState(false);
|
|
22
22
|
const themeState = useThemeAnimation({
|
|
23
23
|
themes,
|
|
24
24
|
colorThemes,
|
|
@@ -27,39 +27,47 @@ export const SpacemanThemeProvider = ({ children, themes = ['light', 'dark', 'sy
|
|
|
27
27
|
animationType,
|
|
28
28
|
duration,
|
|
29
29
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* @param theme - Theme to switch to
|
|
34
|
-
* @param element - HTML button element to use as animation origin
|
|
35
|
-
*/
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setMounted(true);
|
|
32
|
+
}, []);
|
|
36
33
|
const switchThemeFromElement = async (theme, element) => {
|
|
37
|
-
// Temporarily set the ref to the clicked element
|
|
38
34
|
if (themeState.ref.current) {
|
|
39
35
|
const originalRef = themeState.ref.current;
|
|
40
|
-
// Use Object.defineProperty to temporarily override the ref
|
|
41
36
|
Object.defineProperty(themeState.ref, 'current', {
|
|
42
37
|
value: element,
|
|
43
38
|
writable: true,
|
|
44
|
-
configurable: true
|
|
39
|
+
configurable: true,
|
|
45
40
|
});
|
|
46
41
|
await themeState.switchTheme(theme);
|
|
47
|
-
// Restore the original ref after animation
|
|
48
42
|
Object.defineProperty(themeState.ref, 'current', {
|
|
49
43
|
value: originalRef,
|
|
50
44
|
writable: true,
|
|
51
|
-
configurable: true
|
|
45
|
+
configurable: true,
|
|
52
46
|
});
|
|
53
47
|
}
|
|
54
48
|
else {
|
|
55
49
|
Object.defineProperty(themeState.ref, 'current', {
|
|
56
50
|
value: element,
|
|
57
51
|
writable: true,
|
|
58
|
-
configurable: true
|
|
52
|
+
configurable: true,
|
|
59
53
|
});
|
|
60
54
|
await themeState.switchTheme(theme);
|
|
61
55
|
}
|
|
62
56
|
};
|
|
57
|
+
if (!mounted) {
|
|
58
|
+
const loadingContextValue = {
|
|
59
|
+
theme: defaultTheme,
|
|
60
|
+
colorTheme: defaultColorTheme,
|
|
61
|
+
resolvedTheme: defaultTheme === 'dark' ? 'dark' : 'light',
|
|
62
|
+
setTheme: () => { },
|
|
63
|
+
setColorTheme: () => { },
|
|
64
|
+
toggleTheme: async () => { },
|
|
65
|
+
switchTheme: async () => { },
|
|
66
|
+
switchThemeFromElement: async () => { },
|
|
67
|
+
ref: { current: null },
|
|
68
|
+
};
|
|
69
|
+
return (_jsx(SpacemanThemeContext.Provider, { value: loadingContextValue, children: children }));
|
|
70
|
+
}
|
|
63
71
|
const contextValue = {
|
|
64
72
|
theme: themeState.theme,
|
|
65
73
|
colorTheme: themeState.colorTheme,
|
|
@@ -7,21 +7,35 @@ export const useThemeAnimation = (props = {}) => {
|
|
|
7
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, } = props;
|
|
8
8
|
const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
|
|
9
9
|
const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
|
|
10
|
+
const [mounted, setMounted] = useState(false);
|
|
10
11
|
useEffect(() => {
|
|
11
12
|
injectBaseStyles();
|
|
12
13
|
}, []);
|
|
13
14
|
const [internalTheme, setInternalTheme] = useState(() => {
|
|
14
15
|
if (!isBrowser)
|
|
15
16
|
return defaultTheme;
|
|
16
|
-
|
|
17
|
-
return saved && themes.indexOf(saved) !== -1 ? saved : defaultTheme;
|
|
17
|
+
return defaultTheme;
|
|
18
18
|
});
|
|
19
19
|
const [internalColorTheme, setInternalColorTheme] = useState(() => {
|
|
20
20
|
if (!isBrowser)
|
|
21
21
|
return defaultColorTheme;
|
|
22
|
-
|
|
23
|
-
return saved && colorThemes.indexOf(saved) !== -1 ? saved : defaultColorTheme;
|
|
22
|
+
return defaultColorTheme;
|
|
24
23
|
});
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setMounted(true);
|
|
26
|
+
if (isBrowser && !externalTheme) {
|
|
27
|
+
const saved = localStorage.getItem(storageKey);
|
|
28
|
+
if (saved && themes.indexOf(saved) !== -1) {
|
|
29
|
+
setInternalTheme(saved);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (isBrowser && !externalColorTheme) {
|
|
33
|
+
const saved = localStorage.getItem(colorStorageKey);
|
|
34
|
+
if (saved && colorThemes.indexOf(saved) !== -1) {
|
|
35
|
+
setInternalColorTheme(saved);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
25
39
|
const currentTheme = externalTheme ?? internalTheme;
|
|
26
40
|
const currentColorTheme = externalColorTheme ?? internalColorTheme;
|
|
27
41
|
const [, setSystemTheme] = useState(() => getSystemTheme());
|
|
@@ -36,23 +50,19 @@ export const useThemeAnimation = (props = {}) => {
|
|
|
36
50
|
}, []);
|
|
37
51
|
// Apply theme classes to DOM
|
|
38
52
|
useEffect(() => {
|
|
39
|
-
if (!isBrowser)
|
|
53
|
+
if (!isBrowser || !mounted)
|
|
40
54
|
return;
|
|
41
|
-
// Apply theme class (light/dark) - use resolvedTheme instead of manual resolution
|
|
42
55
|
if (resolvedTheme === 'dark') {
|
|
43
56
|
document.documentElement.classList.add(globalClassName);
|
|
44
57
|
}
|
|
45
58
|
else {
|
|
46
59
|
document.documentElement.classList.remove(globalClassName);
|
|
47
60
|
}
|
|
48
|
-
// Apply color theme classes
|
|
49
|
-
// Remove all existing color theme classes
|
|
50
61
|
colorThemes.forEach(theme => {
|
|
51
62
|
document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
|
|
52
63
|
});
|
|
53
|
-
// Add current color theme class
|
|
54
64
|
document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
55
|
-
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
65
|
+
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes, mounted]);
|
|
56
66
|
const ref = useRef(null);
|
|
57
67
|
const setTheme = useCallback((newTheme) => {
|
|
58
68
|
// Always save to localStorage
|
|
@@ -116,22 +126,21 @@ export const useThemeAnimation = (props = {}) => {
|
|
|
116
126
|
await switchTheme(newTheme);
|
|
117
127
|
}, [resolvedTheme, switchTheme]);
|
|
118
128
|
useEffect(() => {
|
|
119
|
-
if (!isBrowser)
|
|
129
|
+
if (!isBrowser || !mounted)
|
|
120
130
|
return;
|
|
121
|
-
const root = document.documentElement;
|
|
122
131
|
if (resolvedTheme === 'dark') {
|
|
123
|
-
|
|
132
|
+
document.documentElement.classList.add(globalClassName);
|
|
124
133
|
}
|
|
125
134
|
else {
|
|
126
|
-
|
|
135
|
+
document.documentElement.classList.remove(globalClassName);
|
|
127
136
|
}
|
|
128
137
|
colorThemes.forEach(theme => {
|
|
129
|
-
|
|
138
|
+
document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
|
|
130
139
|
});
|
|
131
140
|
if (currentColorTheme !== 'default') {
|
|
132
|
-
|
|
141
|
+
document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
133
142
|
}
|
|
134
|
-
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
143
|
+
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes, mounted]);
|
|
135
144
|
return {
|
|
136
145
|
ref,
|
|
137
146
|
theme: currentTheme,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@space-man/react-theme-animation",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "React theme switching with smooth view transition animations and multi-theme support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -49,7 +49,14 @@
|
|
|
49
49
|
"format": "prettier --write . --config ./.prettierrc",
|
|
50
50
|
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
|
51
51
|
"lint:fix": "eslint --fix . --ext .{js,jsx,ts,tsx}",
|
|
52
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
52
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
53
|
+
"changeset": "changeset add --type patch",
|
|
54
|
+
"changeset:patch": "changeset add --type patch",
|
|
55
|
+
"changeset:feature": "changeset add --type minor",
|
|
56
|
+
"changeset:breaking": "changeset add --type major",
|
|
57
|
+
"changeset:custom": "changeset",
|
|
58
|
+
"version-packages": "changeset version",
|
|
59
|
+
"release": "npm run build && changeset publish"
|
|
53
60
|
},
|
|
54
61
|
"peerDependencies": {
|
|
55
62
|
"react": ">=17",
|
|
@@ -58,6 +65,7 @@
|
|
|
58
65
|
"@radix-ui/react-select": ">=2"
|
|
59
66
|
},
|
|
60
67
|
"devDependencies": {
|
|
68
|
+
"@changesets/cli": "^2.27.1",
|
|
61
69
|
"@types/react": "^18.3.12",
|
|
62
70
|
"@types/react-dom": "^18.3.1",
|
|
63
71
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|