@space-man/react-theme-animation 1.1.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,75 +1,130 @@
1
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useEffect, useState, useCallback } from 'react';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useCallback } from 'react';
3
+ import { useThemeAnimation } from '../hooks/use-theme-animation';
4
+ import { ThemeAnimationType } from '../types';
3
5
  const ViteThemeContext = createContext(undefined);
4
- export const ViteThemeProvider = ({ children, attribute = 'class', defaultTheme = 'system', enableSystem = true, disableTransitionOnChange = false, storageKey = 'vite-theme', }) => {
5
- const [mounted, setMounted] = useState(false);
6
- const [systemTheme, setSystemTheme] = useState('light');
7
- const [theme, setThemeState] = useState(() => {
8
- if (typeof window === 'undefined')
9
- return defaultTheme;
10
- try {
11
- const savedTheme = localStorage.getItem(storageKey);
12
- return savedTheme && ['light', 'dark', 'system'].includes(savedTheme)
13
- ? savedTheme
14
- : defaultTheme;
15
- }
16
- catch {
17
- return defaultTheme;
18
- }
6
+ /**
7
+ * Vite Theme Provider - Optimized for Vite SPA applications
8
+ *
9
+ * This provider is designed for Vite-based single-page applications with optional
10
+ * transition disabling to prevent flash during theme switches.
11
+ *
12
+ * Note: For flash prevention on initial load, add a script to your index.html
13
+ * or inject it via Vite plugin to apply theme before React loads.
14
+ *
15
+ * @param children - React children to wrap with theme context
16
+ * @param themes - Available theme options
17
+ * @param colorThemes - Available color theme options
18
+ * @param defaultTheme - Default theme to use
19
+ * @param defaultColorTheme - Default color theme to use
20
+ * @param animationType - Animation type for theme transitions
21
+ * @param duration - Animation duration in milliseconds
22
+ * @param attribute - HTML attribute to use for theme ('class' or 'data-theme')
23
+ * @param disableTransitionOnChange - Temporarily disable CSS transitions during theme switch
24
+ * @param storageKey - LocalStorage key for theme preference
25
+ * @param colorStorageKey - LocalStorage key for color theme preference
26
+ * @param globalClassName - Class name for dark theme (when using 'class' attribute)
27
+ * @param colorThemePrefix - Prefix for color theme classes
28
+ */
29
+ export const ViteThemeProvider = ({ children, themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', animationType = ThemeAnimationType.CIRCLE, duration = 750, attribute = 'class', disableTransitionOnChange = false, storageKey = 'vite-theme', colorStorageKey = 'vite-color-theme', globalClassName = 'dark', colorThemePrefix = 'theme-', }) => {
30
+ const themeState = useThemeAnimation({
31
+ themes,
32
+ colorThemes,
33
+ defaultTheme,
34
+ defaultColorTheme,
35
+ animationType,
36
+ duration,
37
+ storageKey,
38
+ colorStorageKey,
39
+ globalClassName: attribute === 'class' ? globalClassName : undefined,
40
+ colorThemePrefix,
19
41
  });
20
- useEffect(() => {
21
- if (!enableSystem)
22
- return;
23
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
24
- const updateSystemTheme = () => {
25
- setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
26
- };
27
- updateSystemTheme();
28
- mediaQuery.addEventListener('change', updateSystemTheme);
29
- return () => mediaQuery.removeEventListener('change', updateSystemTheme);
30
- }, [enableSystem]);
31
- const resolvedTheme = theme === 'system' ? systemTheme : theme;
32
- useEffect(() => {
33
- if (!mounted)
34
- return;
35
- const root = document.documentElement;
36
- if (disableTransitionOnChange) {
37
- const css = document.createElement('style');
38
- css.textContent = `*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`;
39
- document.head.appendChild(css);
40
- window.getComputedStyle(document.body);
42
+ const applyTransitionDisable = useCallback(() => {
43
+ if (!disableTransitionOnChange)
44
+ return () => { };
45
+ const css = document.createElement('style');
46
+ css.textContent = `*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`;
47
+ document.head.appendChild(css);
48
+ window.getComputedStyle(document.body);
49
+ return () => {
41
50
  setTimeout(() => {
42
51
  document.head.removeChild(css);
43
52
  }, 1);
44
- }
45
- if (attribute === 'class') {
46
- root.classList.remove('light', 'dark');
47
- root.classList.add(resolvedTheme);
53
+ };
54
+ }, [disableTransitionOnChange]);
55
+ const wrappedSetTheme = useCallback((theme) => {
56
+ const cleanup = applyTransitionDisable();
57
+ themeState.setTheme(theme);
58
+ cleanup();
59
+ }, [themeState.setTheme, applyTransitionDisable]);
60
+ const wrappedSwitchTheme = useCallback(async (theme, animationOff = false) => {
61
+ const cleanup = applyTransitionDisable();
62
+ await themeState.switchTheme(theme, animationOff);
63
+ cleanup();
64
+ }, [themeState.switchTheme, applyTransitionDisable]);
65
+ const wrappedToggleTheme = useCallback(async (animationOff = false) => {
66
+ const cleanup = applyTransitionDisable();
67
+ await themeState.toggleTheme(animationOff);
68
+ cleanup();
69
+ }, [themeState.toggleTheme, applyTransitionDisable]);
70
+ const wrappedToggleLightTheme = useCallback(async (animationOff = false) => {
71
+ const cleanup = applyTransitionDisable();
72
+ await themeState.toggleLightTheme(animationOff);
73
+ cleanup();
74
+ }, [themeState.toggleLightTheme, applyTransitionDisable]);
75
+ const wrappedToggleDarkTheme = useCallback(async (animationOff = false) => {
76
+ const cleanup = applyTransitionDisable();
77
+ await themeState.toggleDarkTheme(animationOff);
78
+ cleanup();
79
+ }, [themeState.toggleDarkTheme, applyTransitionDisable]);
80
+ const switchThemeFromElement = async (theme, element) => {
81
+ const cleanup = applyTransitionDisable();
82
+ if (themeState.ref.current) {
83
+ const originalRef = themeState.ref.current;
84
+ Object.defineProperty(themeState.ref, 'current', {
85
+ value: element,
86
+ writable: true,
87
+ configurable: true,
88
+ });
89
+ await themeState.switchTheme(theme);
90
+ Object.defineProperty(themeState.ref, 'current', {
91
+ value: originalRef,
92
+ writable: true,
93
+ configurable: true,
94
+ });
48
95
  }
49
96
  else {
50
- root.setAttribute(attribute, resolvedTheme);
51
- }
52
- try {
53
- localStorage.setItem(storageKey, theme);
97
+ Object.defineProperty(themeState.ref, 'current', {
98
+ value: element,
99
+ writable: true,
100
+ configurable: true,
101
+ });
102
+ await themeState.switchTheme(theme);
54
103
  }
55
- catch { }
56
- }, [theme, resolvedTheme, mounted, attribute, disableTransitionOnChange, storageKey]);
57
- useEffect(() => {
58
- setMounted(true);
59
- }, []);
60
- const setTheme = useCallback((newTheme) => {
61
- setThemeState(newTheme);
62
- }, []);
63
- if (!mounted) {
64
- return _jsx(_Fragment, { children: children });
65
- }
66
- const value = {
67
- theme,
68
- resolvedTheme,
69
- setTheme,
104
+ cleanup();
105
+ };
106
+ const systemTheme = typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
107
+ ? 'dark'
108
+ : 'light';
109
+ const contextValue = {
110
+ ref: themeState.ref,
111
+ theme: themeState.theme,
112
+ colorTheme: themeState.colorTheme,
113
+ resolvedTheme: themeState.resolvedTheme,
70
114
  systemTheme,
115
+ setTheme: wrappedSetTheme,
116
+ setColorTheme: themeState.setColorTheme,
117
+ switchTheme: wrappedSwitchTheme,
118
+ switchThemeFromElement,
119
+ switchColorTheme: themeState.switchColorTheme,
120
+ toggleTheme: wrappedToggleTheme,
121
+ toggleLightTheme: wrappedToggleLightTheme,
122
+ toggleDarkTheme: wrappedToggleDarkTheme,
123
+ toggleColorTheme: themeState.toggleColorTheme,
124
+ createColorThemeToggle: themeState.createColorThemeToggle,
125
+ isColorThemeActive: themeState.isColorThemeActive,
71
126
  };
72
- return _jsx(ViteThemeContext.Provider, { value: value, children: children });
127
+ return _jsx(ViteThemeContext.Provider, { value: contextValue, children: children });
73
128
  };
74
129
  export const useViteTheme = () => {
75
130
  const context = useContext(ViteThemeContext);
@@ -10,34 +10,23 @@ export const useThemeAnimation = (props = {}) => {
10
10
  const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
11
11
  const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
12
12
  const [mounted, setMounted] = useState(false);
13
+ // Inject base styles once on mount
13
14
  useEffect(() => {
14
15
  injectBaseStyles();
16
+ setMounted(true);
15
17
  }, []);
16
18
  const [internalTheme, setInternalTheme] = useState(() => {
17
19
  if (!isBrowser)
18
20
  return defaultTheme;
19
- return defaultTheme;
21
+ const saved = localStorage.getItem(storageKey);
22
+ return saved && themes.indexOf(saved) !== -1 ? saved : defaultTheme;
20
23
  });
21
24
  const [internalColorTheme, setInternalColorTheme] = useState(() => {
22
25
  if (!isBrowser)
23
26
  return defaultColorTheme;
24
- return defaultColorTheme;
27
+ const saved = localStorage.getItem(colorStorageKey);
28
+ return saved && colorThemes.indexOf(saved) !== -1 ? saved : defaultColorTheme;
25
29
  });
26
- useEffect(() => {
27
- setMounted(true);
28
- if (isBrowser && !externalTheme) {
29
- const saved = localStorage.getItem(storageKey);
30
- if (saved && themes.indexOf(saved) !== -1) {
31
- setInternalTheme(saved);
32
- }
33
- }
34
- if (isBrowser && !externalColorTheme) {
35
- const saved = localStorage.getItem(colorStorageKey);
36
- if (saved && colorThemes.indexOf(saved) !== -1) {
37
- setInternalColorTheme(saved);
38
- }
39
- }
40
- }, []);
41
30
  const currentTheme = externalTheme ?? internalTheme;
42
31
  const currentColorTheme = externalColorTheme ?? internalColorTheme;
43
32
  const [, setSystemTheme] = useState(() => getSystemTheme());
@@ -94,8 +83,8 @@ export const useThemeAnimation = (props = {}) => {
94
83
  onColorThemeChange(newColorTheme);
95
84
  }
96
85
  }, [onColorThemeChange, externalColorTheme, colorStorageKey]);
97
- const switchTheme = useCallback(async (newTheme) => {
98
- if (!ref.current || !supportsViewTransitions() || prefersReducedMotion()) {
86
+ const switchTheme = useCallback(async (newTheme, animationOff = false) => {
87
+ if (!ref.current || !supportsViewTransitions() || prefersReducedMotion() || animationOff) {
99
88
  setTheme(newTheme);
100
89
  return;
101
90
  }
@@ -183,26 +172,45 @@ export const useThemeAnimation = (props = {}) => {
183
172
  slideToX,
184
173
  slideToY,
185
174
  ]);
186
- const toggleTheme = useCallback(async () => {
175
+ const switchColorTheme = useCallback((newColorTheme) => {
176
+ if (colorThemes.indexOf(newColorTheme) === -1) {
177
+ console.warn(`Color theme "${newColorTheme}" not found in available themes`);
178
+ return;
179
+ }
180
+ setColorTheme(newColorTheme);
181
+ }, [colorThemes, setColorTheme]);
182
+ const toggleTheme = useCallback(async (animationOff = false) => {
187
183
  const newTheme = resolvedTheme === 'dark' ? 'light' : 'dark';
188
- await switchTheme(newTheme);
184
+ await switchTheme(newTheme, animationOff);
189
185
  }, [resolvedTheme, switchTheme]);
190
- useEffect(() => {
191
- if (!isBrowser || !mounted)
186
+ const toggleLightTheme = useCallback(async (animationOff = false) => {
187
+ if (resolvedTheme === 'light')
192
188
  return;
193
- if (resolvedTheme === 'dark') {
194
- document.documentElement.classList.add(globalClassName);
195
- }
196
- else {
197
- document.documentElement.classList.remove(globalClassName);
198
- }
199
- colorThemes.forEach(theme => {
200
- document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
201
- });
202
- if (currentColorTheme !== 'default') {
203
- document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
204
- }
205
- }, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes, mounted]);
189
+ await switchTheme('light', animationOff);
190
+ }, [resolvedTheme, switchTheme]);
191
+ const toggleDarkTheme = useCallback(async (animationOff = false) => {
192
+ if (resolvedTheme === 'dark')
193
+ return;
194
+ await switchTheme('dark', animationOff);
195
+ }, [resolvedTheme, switchTheme]);
196
+ const toggleColorTheme = useCallback(() => {
197
+ const currentIndex = colorThemes.indexOf(currentColorTheme);
198
+ const nextIndex = (currentIndex + 1) % colorThemes.length;
199
+ const nextColorTheme = colorThemes[nextIndex];
200
+ setColorTheme(nextColorTheme);
201
+ }, [currentColorTheme, colorThemes, setColorTheme]);
202
+ const createColorThemeToggle = useCallback((targetColorTheme) => {
203
+ return () => {
204
+ if (colorThemes.indexOf(targetColorTheme) === -1) {
205
+ console.warn(`Color theme "${targetColorTheme}" not found in available themes`);
206
+ return;
207
+ }
208
+ setColorTheme(targetColorTheme);
209
+ };
210
+ }, [colorThemes, setColorTheme]);
211
+ const isColorThemeActive = useCallback((targetColorTheme) => {
212
+ return currentColorTheme === targetColorTheme;
213
+ }, [currentColorTheme]);
206
214
  return {
207
215
  ref,
208
216
  theme: currentTheme,
@@ -210,7 +218,13 @@ export const useThemeAnimation = (props = {}) => {
210
218
  resolvedTheme,
211
219
  setTheme,
212
220
  setColorTheme,
213
- toggleTheme,
214
221
  switchTheme,
222
+ switchColorTheme,
223
+ toggleTheme,
224
+ toggleLightTheme,
225
+ toggleDarkTheme,
226
+ toggleColorTheme,
227
+ createColorThemeToggle,
228
+ isColorThemeActive,
215
229
  };
216
230
  };
@@ -2,8 +2,9 @@ export { useThemeAnimation } from './hooks/use-theme-animation';
2
2
  export { ThemeSwitcher } from './components/ThemeSwitcher';
3
3
  export { ThemeSelector } from './components/ThemeSelector';
4
4
  export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
5
+ export { NextThemeProvider, useNextTheme } from './components/NextThemeProvider';
6
+ export { TanStackThemeProvider, useTanStackTheme } from './components/TanStackThemeProvider';
5
7
  export { ViteThemeProvider, useViteTheme } from './components/ViteThemeProvider';
6
8
  export type { UseThemeAnimationProps, UseThemeAnimationReturn, ThemeSwitcherProps, ThemeSelectorProps, Theme, ColorTheme, SlideDirection, } from './types';
7
- export type { ViteThemeContextType } from './components/ViteThemeProvider';
8
9
  export { ThemeAnimationType } from './types';
9
10
  export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
package/dist/esm/index.js CHANGED
@@ -2,6 +2,9 @@ export { useThemeAnimation } from './hooks/use-theme-animation';
2
2
  export { ThemeSwitcher } from './components/ThemeSwitcher';
3
3
  export { ThemeSelector } from './components/ThemeSelector';
4
4
  export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
5
+ // Framework-specific optimized providers
6
+ export { NextThemeProvider, useNextTheme } from './components/NextThemeProvider';
7
+ export { TanStackThemeProvider, useTanStackTheme } from './components/TanStackThemeProvider';
5
8
  export { ViteThemeProvider, useViteTheme } from './components/ViteThemeProvider';
6
9
  export { ThemeAnimationType } from './types';
7
10
  export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
@@ -38,8 +38,14 @@ export interface UseThemeAnimationReturn {
38
38
  resolvedTheme: 'light' | 'dark';
39
39
  setTheme: (theme: Theme) => void;
40
40
  setColorTheme: (colorTheme: ColorTheme) => void;
41
- toggleTheme: () => Promise<void>;
42
- switchTheme: (theme: Theme) => Promise<void>;
41
+ switchTheme: (theme: Theme, animationOff?: boolean) => Promise<void>;
42
+ switchColorTheme: (colorTheme: string) => void;
43
+ toggleTheme: (animationOff?: boolean) => Promise<void>;
44
+ toggleLightTheme: (animationOff?: boolean) => Promise<void>;
45
+ toggleDarkTheme: (animationOff?: boolean) => Promise<void>;
46
+ toggleColorTheme: () => void;
47
+ createColorThemeToggle: (targetColorTheme: string) => () => void;
48
+ isColorThemeActive: (targetColorTheme: string) => boolean;
43
49
  }
44
50
  export interface ThemeSwitcherProps {
45
51
  themes?: Theme[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space-man/react-theme-animation",
3
- "version": "1.1.1",
3
+ "version": "2.1.0",
4
4
  "description": "React theme switching with smooth view transition animations and multi-theme support",
5
5
  "keywords": [
6
6
  "react",