@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 CHANGED
@@ -1,4 +1,4 @@
1
- # @spaceman/react-theme-animation
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 @spaceman/react-theme-animation
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 '@spaceman/react-theme-animation';
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 '@spaceman/react-theme-animation';
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
- // This is our master hook instance - the single source of truth
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
- * Switch theme with animation originating from a specific element
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
- const saved = localStorage.getItem(storageKey);
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
- const saved = localStorage.getItem(colorStorageKey);
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
- root.classList.add(globalClassName);
135
+ document.documentElement.classList.add(globalClassName);
127
136
  }
128
137
  else {
129
- root.classList.remove(globalClassName);
138
+ document.documentElement.classList.remove(globalClassName);
130
139
  }
131
140
  colorThemes.forEach(theme => {
132
- root.classList.remove(`${colorThemePrefix}${theme}`);
141
+ document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
133
142
  });
134
143
  if (currentColorTheme !== 'default') {
135
- root.classList.add(`${colorThemePrefix}${currentColorTheme}`);
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
- // This is our master hook instance - the single source of truth
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
- * Switch theme with animation originating from a specific element
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
- const saved = localStorage.getItem(storageKey);
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
- const saved = localStorage.getItem(colorStorageKey);
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
- root.classList.add(globalClassName);
132
+ document.documentElement.classList.add(globalClassName);
124
133
  }
125
134
  else {
126
- root.classList.remove(globalClassName);
135
+ document.documentElement.classList.remove(globalClassName);
127
136
  }
128
137
  colorThemes.forEach(theme => {
129
- root.classList.remove(`${colorThemePrefix}${theme}`);
138
+ document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
130
139
  });
131
140
  if (currentColorTheme !== 'default') {
132
- root.classList.add(`${colorThemePrefix}${currentColorTheme}`);
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.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",