@space-man/react-theme-animation 1.0.1 → 1.0.3

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
@@ -43,16 +43,16 @@ Perfect for custom theme toggle buttons and complete control over animated theme
43
43
  ### Basic Theme Toggle
44
44
 
45
45
  ```tsx
46
- import { useThemeAnimation } from '@space-man/react-theme-animation';
46
+ import { useThemeAnimation } from '@space-man/react-theme-animation'
47
47
 
48
48
  function ThemeToggle() {
49
- const { theme, toggleTheme, ref } = useThemeAnimation();
49
+ const { theme, toggleTheme, ref } = useThemeAnimation()
50
50
 
51
51
  return (
52
52
  <button ref={ref} onClick={toggleTheme} className="theme-toggle-btn">
53
53
  {theme === 'light' ? '🌙' : '🌞'} {theme}
54
54
  </button>
55
- );
55
+ )
56
56
  }
57
57
  ```
58
58
 
@@ -69,7 +69,7 @@ import {
69
69
  SpacemanThemeProvider,
70
70
  ThemeSwitcher,
71
71
  ThemeSelector,
72
- } from '@space-man/react-theme-animation';
72
+ } from '@space-man/react-theme-animation'
73
73
 
74
74
  function App() {
75
75
  return (
@@ -88,7 +88,7 @@ function App() {
88
88
  </main>
89
89
  </div>
90
90
  </SpacemanThemeProvider>
91
- );
91
+ )
92
92
  }
93
93
  ```
94
94
 
@@ -135,7 +135,7 @@ Hook to access theme state from SpacemanThemeProvider context.
135
135
 
136
136
  ```tsx
137
137
  const { theme, colorTheme, switchTheme, setColorTheme, switchThemeFromElement, ref } =
138
- useSpacemanTheme();
138
+ useSpacemanTheme()
139
139
  ```
140
140
 
141
141
  #### Returns
@@ -161,7 +161,7 @@ const { theme, colorTheme, switchTheme, setColorTheme, toggleTheme, ref } = useT
161
161
  duration: 800,
162
162
  onThemeChange: theme => console.log('Theme changed:', theme),
163
163
  onColorThemeChange: colorTheme => console.log('Color changed:', colorTheme),
164
- });
164
+ })
165
165
  ```
166
166
 
167
167
  #### Options
@@ -258,59 +258,59 @@ enum ThemeAnimationType {
258
258
  ### Types
259
259
 
260
260
  ```tsx
261
- type Theme = 'light' | 'dark' | 'system';
262
- type ColorTheme = string; // e.g., 'default', 'blue', 'green', etc.
261
+ type Theme = 'light' | 'dark' | 'system'
262
+ type ColorTheme = string // e.g., 'default', 'blue', 'green', etc.
263
263
 
264
264
  interface SpacemanThemeProviderProps {
265
- children: React.ReactNode;
266
- defaultTheme?: Theme;
267
- defaultColorTheme?: ColorTheme;
268
- themes?: Theme[];
269
- colorThemes?: ColorTheme[];
270
- animationType?: ThemeAnimationType;
271
- duration?: number;
272
- blurAmount?: number;
273
- onThemeChange?: (theme: Theme) => void;
274
- onColorThemeChange?: (colorTheme: ColorTheme) => void;
265
+ children: React.ReactNode
266
+ defaultTheme?: Theme
267
+ defaultColorTheme?: ColorTheme
268
+ themes?: Theme[]
269
+ colorThemes?: ColorTheme[]
270
+ animationType?: ThemeAnimationType
271
+ duration?: number
272
+ blurAmount?: number
273
+ onThemeChange?: (theme: Theme) => void
274
+ onColorThemeChange?: (colorTheme: ColorTheme) => void
275
275
  }
276
276
 
277
277
  interface UseThemeAnimationProps {
278
- themes?: Theme[];
279
- colorThemes?: ColorTheme[];
280
- theme?: Theme;
281
- colorTheme?: ColorTheme;
282
- animationType?: ThemeAnimationType;
283
- duration?: number;
284
- blurAmount?: number;
285
- onThemeChange?: (theme: Theme) => void;
286
- onColorThemeChange?: (colorTheme: ColorTheme) => void;
278
+ themes?: Theme[]
279
+ colorThemes?: ColorTheme[]
280
+ theme?: Theme
281
+ colorTheme?: ColorTheme
282
+ animationType?: ThemeAnimationType
283
+ duration?: number
284
+ blurAmount?: number
285
+ onThemeChange?: (theme: Theme) => void
286
+ onColorThemeChange?: (colorTheme: ColorTheme) => void
287
287
  }
288
288
 
289
289
  interface UseThemeAnimationReturn {
290
- theme: Theme;
291
- colorTheme: ColorTheme;
292
- switchTheme: (theme: Theme) => Promise<void>;
293
- setColorTheme: (colorTheme: ColorTheme) => void;
294
- toggleTheme: () => Promise<void>;
295
- ref: RefObject<HTMLElement>;
290
+ theme: Theme
291
+ colorTheme: ColorTheme
292
+ switchTheme: (theme: Theme) => Promise<void>
293
+ setColorTheme: (colorTheme: ColorTheme) => void
294
+ toggleTheme: () => Promise<void>
295
+ ref: RefObject<HTMLElement>
296
296
  }
297
297
 
298
298
  interface ThemeSwitcherProps {
299
- themes?: Theme[];
300
- currentTheme?: Theme;
301
- onThemeChange?: (theme: Theme) => void;
302
- animationType?: ThemeAnimationType;
303
- duration?: number;
304
- className?: string;
299
+ themes?: Theme[]
300
+ currentTheme?: Theme
301
+ onThemeChange?: (theme: Theme) => void
302
+ animationType?: ThemeAnimationType
303
+ duration?: number
304
+ className?: string
305
305
  }
306
306
 
307
307
  interface ThemeSelectorProps {
308
- themes?: Theme[];
309
- colorThemes?: ColorTheme[];
310
- currentColorTheme?: ColorTheme;
311
- onColorThemeChange?: (colorTheme: ColorTheme) => void;
312
- animationType?: ThemeAnimationType;
313
- duration?: number;
308
+ themes?: Theme[]
309
+ colorThemes?: ColorTheme[]
310
+ currentColorTheme?: ColorTheme
311
+ onColorThemeChange?: (colorTheme: ColorTheme) => void
312
+ animationType?: ThemeAnimationType
313
+ duration?: number
314
314
  }
315
315
  ```
316
316
 
@@ -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,
@@ -24,9 +24,9 @@ const ThemeSelector = ({ themes = ['light', 'dark', 'system'], colorThemes = ['d
24
24
  onColorThemeChange,
25
25
  });
26
26
  const isControlled = contextTheme !== null;
27
- const { colorTheme, setColorTheme } = isControlled ?
28
- { colorTheme: contextTheme.colorTheme, setColorTheme: contextTheme.setColorTheme } :
29
- standaloneHook;
27
+ const { colorTheme, setColorTheme } = isControlled
28
+ ? { colorTheme: contextTheme.colorTheme, setColorTheme: contextTheme.setColorTheme }
29
+ : standaloneHook;
30
30
  const handleColorThemeChange = (newColorTheme) => {
31
31
  setColorTheme(newColorTheme);
32
32
  // Also call the callback if provided
@@ -11,7 +11,7 @@ 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 transition-all duration-200 ease-in-out', '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: {
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 transition-all duration-200 ease-in-out', '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
16
  }, children: [isHovered && ((0, jsx_runtime_1.jsx)(react_2.motion.div, { layoutId: "theme-hover-bg", className: "absolute inset-0 bg-muted backdrop-blur-sm", style: {
17
17
  borderRadius: 'var(--radius)',
@@ -67,9 +67,9 @@ const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentTheme, onT
67
67
  onThemeChange,
68
68
  });
69
69
  const isControlled = contextTheme !== null;
70
- const { ref, theme, switchTheme } = isControlled ?
71
- { ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme } :
72
- standaloneHook;
70
+ const { ref, theme, switchTheme } = isControlled
71
+ ? { ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme }
72
+ : standaloneHook;
73
73
  const [isMounted, setIsMounted] = (0, react_1.useState)(false);
74
74
  (0, react_1.useEffect)(() => {
75
75
  setIsMounted(true);
@@ -1,10 +1,10 @@
1
- import * as React from "react";
2
- import * as SelectPrimitive from "@radix-ui/react-select";
1
+ import * as React from 'react';
2
+ import * as SelectPrimitive from '@radix-ui/react-select';
3
3
  declare function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
4
4
  declare function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>): import("react/jsx-runtime").JSX.Element;
5
5
  declare function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>): import("react/jsx-runtime").JSX.Element;
6
6
  declare function SelectTrigger({ className, size, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
7
- size?: "sm" | "default";
7
+ size?: 'sm' | 'default';
8
8
  }): import("react/jsx-runtime").JSX.Element;
9
9
  declare function SelectContent({ className, children, position, ...props }: React.ComponentProps<typeof SelectPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
10
10
  declare function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>): import("react/jsx-runtime").JSX.Element;
@@ -58,26 +58,26 @@ function SelectValue({ ...props }) {
58
58
  const ChevronDownIcon = () => ((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: "m6 9 6 6 6-6" }) }));
59
59
  const ChevronUpIcon = () => ((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: "m18 15-6-6-6 6" }) }));
60
60
  const CheckIcon = () => ((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: "M20 6 9 17l-5-5" }) }));
61
- function SelectTrigger({ className, size = "default", children, ...props }) {
61
+ function SelectTrigger({ className, size = 'default', children, ...props }) {
62
62
  return ((0, jsx_runtime_1.jsxs)(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: (0, clsx_1.clsx)("border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [children, (0, jsx_runtime_1.jsx)(SelectPrimitive.Icon, { asChild: true, children: (0, jsx_runtime_1.jsx)(ChevronDownIcon, {}) })] }));
63
63
  }
64
- function SelectContent({ className, children, position = "popper", ...props }) {
65
- return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Portal, { children: (0, jsx_runtime_1.jsx)(SelectPrimitive.Content, { "data-slot": "select-content", className: (0, clsx_1.clsx)("bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md", position === "popper" &&
66
- "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className), position: position, ...props, children: (0, jsx_runtime_1.jsx)(SelectPrimitive.Viewport, { className: (0, clsx_1.clsx)("p-1", position === "popper" &&
67
- "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"), children: children }) }) }));
64
+ function SelectContent({ className, children, position = 'popper', ...props }) {
65
+ return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Portal, { children: (0, jsx_runtime_1.jsx)(SelectPrimitive.Content, { "data-slot": "select-content", className: (0, clsx_1.clsx)('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md', position === 'popper' &&
66
+ 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', className), position: position, ...props, children: (0, jsx_runtime_1.jsx)(SelectPrimitive.Viewport, { className: (0, clsx_1.clsx)('p-1', position === 'popper' &&
67
+ 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'), children: children }) }) }));
68
68
  }
69
69
  function SelectLabel({ className, ...props }) {
70
- return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Label, { "data-slot": "select-label", className: (0, clsx_1.clsx)("py-1.5 pl-8 pr-2 text-sm font-semibold", className), ...props }));
70
+ return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Label, { "data-slot": "select-label", className: (0, clsx_1.clsx)('py-1.5 pl-8 pr-2 text-sm font-semibold', className), ...props }));
71
71
  }
72
72
  function SelectItem({ className, children, ...props }) {
73
73
  return ((0, jsx_runtime_1.jsxs)(SelectPrimitive.Item, { "data-slot": "select-item", className: (0, clsx_1.clsx)("focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className), ...props, children: [(0, jsx_runtime_1.jsx)("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: (0, jsx_runtime_1.jsx)(SelectPrimitive.ItemIndicator, { children: (0, jsx_runtime_1.jsx)(CheckIcon, {}) }) }), (0, jsx_runtime_1.jsx)(SelectPrimitive.ItemText, { children: children })] }));
74
74
  }
75
75
  function SelectSeparator({ className, ...props }) {
76
- return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Separator, { "data-slot": "select-separator", className: (0, clsx_1.clsx)("-mx-1 my-1 h-px bg-muted", className), ...props }));
76
+ return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Separator, { "data-slot": "select-separator", className: (0, clsx_1.clsx)('-mx-1 my-1 h-px bg-muted', className), ...props }));
77
77
  }
78
78
  function SelectScrollUpButton({ className, ...props }) {
79
- return ((0, jsx_runtime_1.jsx)(SelectPrimitive.ScrollUpButton, { "data-slot": "select-scroll-up-button", className: (0, clsx_1.clsx)("flex cursor-default items-center justify-center py-1", className), ...props, children: (0, jsx_runtime_1.jsx)(ChevronUpIcon, {}) }));
79
+ return ((0, jsx_runtime_1.jsx)(SelectPrimitive.ScrollUpButton, { "data-slot": "select-scroll-up-button", className: (0, clsx_1.clsx)('flex cursor-default items-center justify-center py-1', className), ...props, children: (0, jsx_runtime_1.jsx)(ChevronUpIcon, {}) }));
80
80
  }
81
81
  function SelectScrollDownButton({ className, ...props }) {
82
- return ((0, jsx_runtime_1.jsx)(SelectPrimitive.ScrollDownButton, { "data-slot": "select-scroll-down-button", className: (0, clsx_1.clsx)("flex cursor-default items-center justify-center py-1", className), ...props, children: (0, jsx_runtime_1.jsx)(ChevronDownIcon, {}) }));
82
+ return ((0, jsx_runtime_1.jsx)(SelectPrimitive.ScrollDownButton, { "data-slot": "select-scroll-down-button", className: (0, clsx_1.clsx)('flex cursor-default items-center justify-center py-1', className), ...props, children: (0, jsx_runtime_1.jsx)(ChevronDownIcon, {}) }));
83
83
  }
@@ -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,
@@ -21,9 +21,9 @@ export const ThemeSelector = ({ themes = ['light', 'dark', 'system'], colorTheme
21
21
  onColorThemeChange,
22
22
  });
23
23
  const isControlled = contextTheme !== null;
24
- const { colorTheme, setColorTheme } = isControlled ?
25
- { colorTheme: contextTheme.colorTheme, setColorTheme: contextTheme.setColorTheme } :
26
- standaloneHook;
24
+ const { colorTheme, setColorTheme } = isControlled
25
+ ? { colorTheme: contextTheme.colorTheme, setColorTheme: contextTheme.setColorTheme }
26
+ : standaloneHook;
27
27
  const handleColorThemeChange = (newColorTheme) => {
28
28
  setColorTheme(newColorTheme);
29
29
  // Also call the callback if provided
@@ -8,7 +8,7 @@ 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 transition-all duration-200 ease-in-out', '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: {
11
+ return (_jsxs("button", { ref: isActive ? buttonRef : undefined, className: clsx('relative flex h-9 w-12 cursor-pointer items-center justify-center transition-all duration-200 ease-in-out', '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
13
  }, children: [isHovered && (_jsx(motion.div, { layoutId: "theme-hover-bg", className: "absolute inset-0 bg-muted backdrop-blur-sm", style: {
14
14
  borderRadius: 'var(--radius)',
@@ -64,9 +64,9 @@ export const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentThe
64
64
  onThemeChange,
65
65
  });
66
66
  const isControlled = contextTheme !== null;
67
- const { ref, theme, switchTheme } = isControlled ?
68
- { ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme } :
69
- standaloneHook;
67
+ const { ref, theme, switchTheme } = isControlled
68
+ ? { ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme }
69
+ : standaloneHook;
70
70
  const [isMounted, setIsMounted] = useState(false);
71
71
  useEffect(() => {
72
72
  setIsMounted(true);
@@ -1,10 +1,10 @@
1
- import * as React from "react";
2
- import * as SelectPrimitive from "@radix-ui/react-select";
1
+ import * as React from 'react';
2
+ import * as SelectPrimitive from '@radix-ui/react-select';
3
3
  declare function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
4
4
  declare function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>): import("react/jsx-runtime").JSX.Element;
5
5
  declare function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>): import("react/jsx-runtime").JSX.Element;
6
6
  declare function SelectTrigger({ className, size, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
7
- size?: "sm" | "default";
7
+ size?: 'sm' | 'default';
8
8
  }): import("react/jsx-runtime").JSX.Element;
9
9
  declare function SelectContent({ className, children, position, ...props }: React.ComponentProps<typeof SelectPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
10
10
  declare function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import * as SelectPrimitive from "@radix-ui/react-select";
3
- import { clsx } from "clsx";
2
+ import * as SelectPrimitive from '@radix-ui/react-select';
3
+ import { clsx } from 'clsx';
4
4
  function Select({ ...props }) {
5
5
  return _jsx(SelectPrimitive.Root, { "data-slot": "select", ...props });
6
6
  }
@@ -13,27 +13,27 @@ function SelectValue({ ...props }) {
13
13
  const ChevronDownIcon = () => (_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: "m6 9 6 6 6-6" }) }));
14
14
  const ChevronUpIcon = () => (_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: "m18 15-6-6-6 6" }) }));
15
15
  const CheckIcon = () => (_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: "M20 6 9 17l-5-5" }) }));
16
- function SelectTrigger({ className, size = "default", children, ...props }) {
16
+ function SelectTrigger({ className, size = 'default', children, ...props }) {
17
17
  return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: clsx("border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDownIcon, {}) })] }));
18
18
  }
19
- function SelectContent({ className, children, position = "popper", ...props }) {
20
- return (_jsx(SelectPrimitive.Portal, { children: _jsx(SelectPrimitive.Content, { "data-slot": "select-content", className: clsx("bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md", position === "popper" &&
21
- "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className), position: position, ...props, children: _jsx(SelectPrimitive.Viewport, { className: clsx("p-1", position === "popper" &&
22
- "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"), children: children }) }) }));
19
+ function SelectContent({ className, children, position = 'popper', ...props }) {
20
+ return (_jsx(SelectPrimitive.Portal, { children: _jsx(SelectPrimitive.Content, { "data-slot": "select-content", className: clsx('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md', position === 'popper' &&
21
+ 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', className), position: position, ...props, children: _jsx(SelectPrimitive.Viewport, { className: clsx('p-1', position === 'popper' &&
22
+ 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'), children: children }) }) }));
23
23
  }
24
24
  function SelectLabel({ className, ...props }) {
25
- return (_jsx(SelectPrimitive.Label, { "data-slot": "select-label", className: clsx("py-1.5 pl-8 pr-2 text-sm font-semibold", className), ...props }));
25
+ return (_jsx(SelectPrimitive.Label, { "data-slot": "select-label", className: clsx('py-1.5 pl-8 pr-2 text-sm font-semibold', className), ...props }));
26
26
  }
27
27
  function SelectItem({ className, children, ...props }) {
28
28
  return (_jsxs(SelectPrimitive.Item, { "data-slot": "select-item", className: clsx("focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(CheckIcon, {}) }) }), _jsx(SelectPrimitive.ItemText, { children: children })] }));
29
29
  }
30
30
  function SelectSeparator({ className, ...props }) {
31
- return (_jsx(SelectPrimitive.Separator, { "data-slot": "select-separator", className: clsx("-mx-1 my-1 h-px bg-muted", className), ...props }));
31
+ return (_jsx(SelectPrimitive.Separator, { "data-slot": "select-separator", className: clsx('-mx-1 my-1 h-px bg-muted', className), ...props }));
32
32
  }
33
33
  function SelectScrollUpButton({ className, ...props }) {
34
- return (_jsx(SelectPrimitive.ScrollUpButton, { "data-slot": "select-scroll-up-button", className: clsx("flex cursor-default items-center justify-center py-1", className), ...props, children: _jsx(ChevronUpIcon, {}) }));
34
+ return (_jsx(SelectPrimitive.ScrollUpButton, { "data-slot": "select-scroll-up-button", className: clsx('flex cursor-default items-center justify-center py-1', className), ...props, children: _jsx(ChevronUpIcon, {}) }));
35
35
  }
36
36
  function SelectScrollDownButton({ className, ...props }) {
37
- return (_jsx(SelectPrimitive.ScrollDownButton, { "data-slot": "select-scroll-down-button", className: clsx("flex cursor-default items-center justify-center py-1", className), ...props, children: _jsx(ChevronDownIcon, {}) }));
37
+ return (_jsx(SelectPrimitive.ScrollDownButton, { "data-slot": "select-scroll-down-button", className: clsx('flex cursor-default items-center justify-center py-1', className), ...props, children: _jsx(ChevronDownIcon, {}) }));
38
38
  }
39
39
  export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, };
@@ -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,21 +1,28 @@
1
1
  {
2
2
  "name": "@space-man/react-theme-animation",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "React theme switching with smooth view transition animations and multi-theme support",
5
5
  "keywords": [
6
6
  "react",
7
- "reactjs",
8
- "hooks",
9
7
  "theme",
8
+ "dark mode",
9
+ "light mode",
10
+ "theme switching",
11
+ "view transitions",
12
+ "hooks",
10
13
  "animation",
11
14
  "switch",
12
15
  "typescript",
13
- "nextjs",
14
16
  "dark",
15
17
  "light",
16
18
  "view-transition",
19
+ "view-transition-api",
20
+ "tailwind",
17
21
  "tailwindcss",
18
- "shadcn"
22
+ "shadcn",
23
+ "reactjs",
24
+ "nextjs",
25
+ "vite"
19
26
  ],
20
27
  "publishConfig": {
21
28
  "registry": "https://registry.npmjs.org/",
@@ -50,6 +57,8 @@
50
57
  "lint": "eslint \"**/*.{js,jsx,ts,tsx}\"",
51
58
  "lint:fix": "eslint --fix . --ext .{js,jsx,ts,tsx}",
52
59
  "test": "echo \"Error: no test specified\" && exit 1",
60
+ "prettier:check": "prettier --check . --config ./.prettierrc",
61
+ "prettier:fix": "prettier --write . --config ./.prettierrc",
53
62
  "changeset": "changeset add --type patch",
54
63
  "changeset:patch": "changeset add --type patch",
55
64
  "changeset:feature": "changeset add --type minor",