@space-man/react-theme-animation 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,10 +4,63 @@ https://github.com/user-attachments/assets/2819cf70-c474-478e-b821-6b26457c8d4a
4
4
 
5
5
  React theme switching with smooth view transition animations, multi-theme support, and synchronized state management.
6
6
 
7
- <p align="center">
8
- <img width="50%" src="https://github.com/user-attachments/assets/98b7f5cc-705c-4d32-a2f6-47f79b298707" />
9
- <img width="50%" src="https://github.com/user-attachments/assets/128cc7b0-5708-4b4c-884f-a8310d4e494f" />
10
- </p>
7
+ ## Spaceman React Theme Animation version 2
8
+
9
+ Version 2 introduces new features for enhanced color theme management and improved Vite integration.
10
+
11
+ - New functions to directly toggle between different color themes, without using the ThemeSelector component
12
+ - Vite theme provider now supports theme toggling and persistence
13
+ - Functions to manage and check active themes
14
+ - Functions to toggle light and dark themes directly
15
+
16
+ ```tsx
17
+ import { useThemeAnimation } from '@space-man/react-theme-animation'
18
+
19
+ function ColorThemeDemo() {
20
+ const { createColorThemeToggle, isColorThemeActive, toggleDarkTheme, toggleLightTheme } =
21
+ useThemeAnimation({
22
+ colorThemes: ['supabase'],
23
+ })
24
+
25
+ return (
26
+ <>
27
+ <button onClick={createColorThemeToggle('supabase')} className="theme-toggle-btn">
28
+ Supabase Theme {isColorThemeActive('supabase') ? '✅' : '❌'}
29
+ </button>
30
+ <button ref={ref} onClick={toggleDarkTheme} className="theme-toggle-btn">
31
+ Dark Theme
32
+ </button>
33
+ <button ref={ref} onClick={toggleLightTheme} className="theme-toggle-btn">
34
+ Light Theme
35
+ </button>
36
+ </>
37
+ )
38
+ }
39
+
40
+ // Vite Theme Provider Example
41
+ function App() {
42
+ return (
43
+ <ViteThemeProvider
44
+ themes={['light', 'dark', 'system']}
45
+ colorThemes={['default', 'supabase']}
46
+ defaultTheme="system"
47
+ defaultColorTheme="default"
48
+ >
49
+ <ThemeToggle />
50
+ </ViteThemeProvider>
51
+ )
52
+ }
53
+
54
+ function ThemeToggle() {
55
+ const { createColorThemeToggle, isColorThemeActive } = useViteTheme()
56
+
57
+ return (
58
+ <button onClick={createColorThemeToggle('supabase')} className="theme-toggle-btn">
59
+ Supabase Theme {isColorThemeActive('supabase') ? '✅' : '❌'}
60
+ </button>
61
+ )
62
+ }
63
+ ```
11
64
 
12
65
  ## Live Demo
13
66
 
@@ -15,18 +68,18 @@ React theme switching with smooth view transition animations, multi-theme suppor
15
68
 
16
69
  ## Features
17
70
 
18
- - 🎨 **Smooth Animations**: Beautiful view transition animations for theme switching with customizable origins
19
- - 🌓 **Multi-Theme Support**: Support for light, dark, and system themes
20
- - 🎯 **Color Themes**: Additional color theme variants (brand colors, etc.)
21
- - 🪝 **Powerful Hook**: `useThemeAnimation` hook with full control
22
- - 🧩 **Ready Components**: `ThemeSwitcher` and `ThemeSelector` components
23
- - 🎛️ **Provider Pattern**: Centralized theme state management with `SpacemanThemeProvider`
24
- - 🔄 **State Synchronization**: Prevents state drift between multiple components
25
- - 📱 **Responsive**: Works on all screen sizes including high-resolution displays
26
- - ⚡ **Performance**: Optimized animations with reduced motion support
27
- - 🔧 **TypeScript**: Full TypeScript support with comprehensive types
28
- - 🎛️ **Customizable**: Extensive configuration options
29
- - 🔙 **Backward Compatible**: Works with existing implementations
71
+ - **Smooth Animations**: Beautiful view transition animations for theme switching with customizable origins
72
+ - **Multi-Theme Support**: Support for light, dark, and system themes
73
+ - **Color Themes**: Additional color theme variants (brand colors, etc.)
74
+ - **Powerful Hook**: `useThemeAnimation` hook with full control
75
+ - **Ready Components**: `ThemeSwitcher` and `ThemeSelector` components
76
+ - **Provider Pattern**: Centralized theme state management with `SpacemanThemeProvider`
77
+ - **State Synchronization**: Prevents state drift between multiple components
78
+ - **Responsive**: Works on all screen sizes including high-resolution displays
79
+ - **Performance**: Optimized animations with reduced motion support
80
+ - **TypeScript**: Full TypeScript support with comprehensive types
81
+ - **Customizable**: Extensive configuration options
82
+ - **Backward Compatible**: Works with existing implementations
30
83
 
31
84
  ## Installation
32
85
 
@@ -42,11 +95,11 @@ This library supports multiple usage patterns:
42
95
  2. **Provider Pattern**: Centralized state management (recommended)
43
96
  3. **ViteThemeProvider**: Specialized provider for Vite React SPAs
44
97
 
45
- 📚 **[See the complete Getting Started Guide](./getting-started.md)** for detailed examples, advanced configurations, and best practices.
98
+ **[See the complete Getting Started Guide](./getting-started.md)** for detailed examples, advanced configurations, and best practices.
46
99
 
47
- 🚀 **[View the Next.js Code Example](./example/)** to see this package in action with a complete implementation of both hook and provider pattern.
100
+ **[View the Next.js Code Example](./example/)** to see this package in action with a complete implementation of both hook and provider pattern.
48
101
 
49
- ⚡ **[View the Vite React SPA Demo](https://github.com/tonyedgal/spaceman-rta-framework-guides/tree/main/example-vite)** for a complete Vite implementation using ViteThemeProvider.
102
+ **[View the Vite React SPA Demo](https://github.com/tonyedgal/spaceman-rta-framework-guides/tree/main/example-vite)** for a complete Vite implementation using ViteThemeProvider.
50
103
 
51
104
  ---
52
105
 
@@ -4,15 +4,21 @@ import { Theme, ColorTheme, ThemeAnimationType } from '../types';
4
4
  * Context type for the Spaceman Theme Provider
5
5
  */
6
6
  interface SpacemanThemeContextType {
7
+ ref: React.RefObject<HTMLButtonElement | null>;
7
8
  theme: Theme;
8
9
  colorTheme: ColorTheme;
9
10
  resolvedTheme: 'light' | 'dark';
10
11
  setTheme: (theme: Theme) => void;
11
12
  setColorTheme: (colorTheme: ColorTheme) => void;
12
- toggleTheme: () => Promise<void>;
13
13
  switchTheme: (theme: Theme) => Promise<void>;
14
+ switchColorTheme: (colorTheme: string) => void;
15
+ toggleTheme: () => Promise<void>;
16
+ toggleLightTheme: () => Promise<void>;
17
+ toggleDarkTheme: () => Promise<void>;
18
+ toggleColorTheme: () => void;
19
+ createColorThemeToggle: (targetColorTheme: string) => () => void;
20
+ isColorThemeActive: (targetColorTheme: string) => boolean;
14
21
  switchThemeFromElement: (theme: Theme, element: HTMLButtonElement) => Promise<void>;
15
- ref: React.RefObject<HTMLButtonElement | null>;
16
22
  }
17
23
  /**
18
24
  * Props for the Spaceman Theme Provider
@@ -59,28 +59,40 @@ const SpacemanThemeProvider = ({ children, themes = ['light', 'dark', 'system'],
59
59
  };
60
60
  if (!mounted) {
61
61
  const loadingContextValue = {
62
+ ref: { current: null },
62
63
  theme: defaultTheme,
63
64
  colorTheme: defaultColorTheme,
64
65
  resolvedTheme: defaultTheme === 'dark' ? 'dark' : 'light',
65
66
  setTheme: () => { },
66
67
  setColorTheme: () => { },
67
- toggleTheme: async () => { },
68
68
  switchTheme: async () => { },
69
69
  switchThemeFromElement: async () => { },
70
- ref: { current: null },
70
+ switchColorTheme: () => { },
71
+ toggleTheme: async () => { },
72
+ toggleLightTheme: async () => { },
73
+ toggleDarkTheme: async () => { },
74
+ toggleColorTheme: () => { },
75
+ createColorThemeToggle: () => () => { },
76
+ isColorThemeActive: () => false,
71
77
  };
72
78
  return ((0, jsx_runtime_1.jsx)(SpacemanThemeContext.Provider, { value: loadingContextValue, children: children }));
73
79
  }
74
80
  const contextValue = {
81
+ ref: themeState.ref,
75
82
  theme: themeState.theme,
76
83
  colorTheme: themeState.colorTheme,
77
84
  resolvedTheme: themeState.resolvedTheme,
78
85
  setTheme: themeState.setTheme,
79
86
  setColorTheme: themeState.setColorTheme,
80
- toggleTheme: themeState.toggleTheme,
81
87
  switchTheme: themeState.switchTheme,
82
88
  switchThemeFromElement,
83
- ref: themeState.ref,
89
+ switchColorTheme: themeState.switchColorTheme,
90
+ toggleTheme: themeState.toggleTheme,
91
+ toggleLightTheme: themeState.toggleLightTheme,
92
+ toggleDarkTheme: themeState.toggleDarkTheme,
93
+ toggleColorTheme: themeState.toggleColorTheme,
94
+ createColorThemeToggle: themeState.createColorThemeToggle,
95
+ isColorThemeActive: themeState.isColorThemeActive,
84
96
  };
85
97
  return ((0, jsx_runtime_1.jsx)(SpacemanThemeContext.Provider, { value: contextValue, children: children }));
86
98
  };
@@ -1,18 +1,37 @@
1
- import React from 'react';
2
- type Theme = 'light' | 'dark' | 'system';
3
- export interface ViteThemeContextType {
1
+ import React, { ReactNode } from 'react';
2
+ import { Theme, ColorTheme, ThemeAnimationType } from '../types';
3
+ interface ViteThemeContextType {
4
+ ref: React.RefObject<HTMLButtonElement | null>;
4
5
  theme: Theme;
6
+ colorTheme: ColorTheme;
5
7
  resolvedTheme: 'light' | 'dark';
6
- setTheme: (theme: Theme) => void;
7
8
  systemTheme: 'light' | 'dark';
9
+ setTheme: (theme: Theme) => void;
10
+ setColorTheme: (colorTheme: ColorTheme) => void;
11
+ switchTheme: (theme: Theme) => Promise<void>;
12
+ switchColorTheme: (colorTheme: string) => void;
13
+ toggleTheme: () => Promise<void>;
14
+ toggleLightTheme: () => Promise<void>;
15
+ toggleDarkTheme: () => Promise<void>;
16
+ toggleColorTheme: () => void;
17
+ createColorThemeToggle: (targetColorTheme: string) => () => void;
18
+ isColorThemeActive: (targetColorTheme: string) => boolean;
19
+ switchThemeFromElement: (theme: Theme, element: HTMLButtonElement) => Promise<void>;
8
20
  }
9
21
  interface ViteThemeProviderProps {
10
- children: React.ReactNode;
11
- attribute?: 'class' | 'data-theme';
22
+ children: ReactNode;
23
+ themes?: Theme[];
24
+ colorThemes?: ColorTheme[];
12
25
  defaultTheme?: Theme;
13
- enableSystem?: boolean;
26
+ defaultColorTheme?: ColorTheme;
27
+ animationType?: ThemeAnimationType;
28
+ duration?: number;
29
+ attribute?: 'class' | 'data-theme';
14
30
  disableTransitionOnChange?: boolean;
15
31
  storageKey?: string;
32
+ colorStorageKey?: string;
33
+ globalClassName?: string;
34
+ colorThemePrefix?: string;
16
35
  }
17
36
  export declare const ViteThemeProvider: React.FC<ViteThemeProviderProps>;
18
37
  export declare const useViteTheme: () => ViteThemeContextType;
@@ -3,76 +3,108 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useViteTheme = exports.ViteThemeProvider = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
+ const use_theme_animation_1 = require("../hooks/use-theme-animation");
7
+ const types_1 = require("../types");
6
8
  const ViteThemeContext = (0, react_1.createContext)(undefined);
7
- const ViteThemeProvider = ({ children, attribute = 'class', defaultTheme = 'system', enableSystem = true, disableTransitionOnChange = false, storageKey = 'vite-theme', }) => {
8
- const [mounted, setMounted] = (0, react_1.useState)(false);
9
- const [systemTheme, setSystemTheme] = (0, react_1.useState)('light');
10
- const [theme, setThemeState] = (0, react_1.useState)(() => {
11
- if (typeof window === 'undefined')
12
- return defaultTheme;
13
- try {
14
- const savedTheme = localStorage.getItem(storageKey);
15
- return savedTheme && ['light', 'dark', 'system'].includes(savedTheme)
16
- ? savedTheme
17
- : defaultTheme;
18
- }
19
- catch {
20
- return defaultTheme;
21
- }
9
+ const ViteThemeProvider = ({ children, themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', animationType = types_1.ThemeAnimationType.CIRCLE, duration = 750, attribute = 'class', disableTransitionOnChange = false, storageKey = 'vite-theme', colorStorageKey = 'vite-color-theme', globalClassName = 'dark', colorThemePrefix = 'theme-', }) => {
10
+ const themeState = (0, use_theme_animation_1.useThemeAnimation)({
11
+ themes,
12
+ colorThemes,
13
+ defaultTheme,
14
+ defaultColorTheme,
15
+ animationType,
16
+ duration,
17
+ storageKey,
18
+ colorStorageKey,
19
+ globalClassName: attribute === 'class' ? globalClassName : undefined,
20
+ colorThemePrefix,
22
21
  });
23
- (0, react_1.useEffect)(() => {
24
- if (!enableSystem)
25
- return;
26
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
27
- const updateSystemTheme = () => {
28
- setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
29
- };
30
- updateSystemTheme();
31
- mediaQuery.addEventListener('change', updateSystemTheme);
32
- return () => mediaQuery.removeEventListener('change', updateSystemTheme);
33
- }, [enableSystem]);
34
- const resolvedTheme = theme === 'system' ? systemTheme : theme;
35
- (0, react_1.useEffect)(() => {
36
- if (!mounted)
37
- return;
38
- const root = document.documentElement;
39
- if (disableTransitionOnChange) {
40
- const css = document.createElement('style');
41
- css.textContent = `*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`;
42
- document.head.appendChild(css);
43
- window.getComputedStyle(document.body);
22
+ const applyTransitionDisable = (0, react_1.useCallback)(() => {
23
+ if (!disableTransitionOnChange)
24
+ return () => { };
25
+ const css = document.createElement('style');
26
+ css.textContent = `*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`;
27
+ document.head.appendChild(css);
28
+ window.getComputedStyle(document.body);
29
+ return () => {
44
30
  setTimeout(() => {
45
31
  document.head.removeChild(css);
46
32
  }, 1);
47
- }
48
- if (attribute === 'class') {
49
- root.classList.remove('light', 'dark');
50
- root.classList.add(resolvedTheme);
33
+ };
34
+ }, [disableTransitionOnChange]);
35
+ const wrappedSetTheme = (0, react_1.useCallback)((theme) => {
36
+ const cleanup = applyTransitionDisable();
37
+ themeState.setTheme(theme);
38
+ cleanup();
39
+ }, [themeState.setTheme, applyTransitionDisable]);
40
+ const wrappedSwitchTheme = (0, react_1.useCallback)(async (theme) => {
41
+ const cleanup = applyTransitionDisable();
42
+ await themeState.switchTheme(theme);
43
+ cleanup();
44
+ }, [themeState.switchTheme, applyTransitionDisable]);
45
+ const wrappedToggleTheme = (0, react_1.useCallback)(async () => {
46
+ const cleanup = applyTransitionDisable();
47
+ await themeState.toggleTheme();
48
+ cleanup();
49
+ }, [themeState.toggleTheme, applyTransitionDisable]);
50
+ const wrappedToggleLightTheme = (0, react_1.useCallback)(async () => {
51
+ const cleanup = applyTransitionDisable();
52
+ await themeState.toggleLightTheme();
53
+ cleanup();
54
+ }, [themeState.toggleLightTheme, applyTransitionDisable]);
55
+ const wrappedToggleDarkTheme = (0, react_1.useCallback)(async () => {
56
+ const cleanup = applyTransitionDisable();
57
+ await themeState.toggleDarkTheme();
58
+ cleanup();
59
+ }, [themeState.toggleDarkTheme, applyTransitionDisable]);
60
+ const switchThemeFromElement = async (theme, element) => {
61
+ const cleanup = applyTransitionDisable();
62
+ if (themeState.ref.current) {
63
+ const originalRef = themeState.ref.current;
64
+ Object.defineProperty(themeState.ref, 'current', {
65
+ value: element,
66
+ writable: true,
67
+ configurable: true,
68
+ });
69
+ await themeState.switchTheme(theme);
70
+ Object.defineProperty(themeState.ref, 'current', {
71
+ value: originalRef,
72
+ writable: true,
73
+ configurable: true,
74
+ });
51
75
  }
52
76
  else {
53
- root.setAttribute(attribute, resolvedTheme);
54
- }
55
- try {
56
- localStorage.setItem(storageKey, theme);
77
+ Object.defineProperty(themeState.ref, 'current', {
78
+ value: element,
79
+ writable: true,
80
+ configurable: true,
81
+ });
82
+ await themeState.switchTheme(theme);
57
83
  }
58
- catch { }
59
- }, [theme, resolvedTheme, mounted, attribute, disableTransitionOnChange, storageKey]);
60
- (0, react_1.useEffect)(() => {
61
- setMounted(true);
62
- }, []);
63
- const setTheme = (0, react_1.useCallback)((newTheme) => {
64
- setThemeState(newTheme);
65
- }, []);
66
- if (!mounted) {
67
- return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children });
68
- }
69
- const value = {
70
- theme,
71
- resolvedTheme,
72
- setTheme,
84
+ cleanup();
85
+ };
86
+ const systemTheme = typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
87
+ ? 'dark'
88
+ : 'light';
89
+ const contextValue = {
90
+ ref: themeState.ref,
91
+ theme: themeState.theme,
92
+ colorTheme: themeState.colorTheme,
93
+ resolvedTheme: themeState.resolvedTheme,
73
94
  systemTheme,
95
+ setTheme: wrappedSetTheme,
96
+ setColorTheme: themeState.setColorTheme,
97
+ switchTheme: wrappedSwitchTheme,
98
+ switchThemeFromElement,
99
+ switchColorTheme: themeState.switchColorTheme,
100
+ toggleTheme: wrappedToggleTheme,
101
+ toggleLightTheme: wrappedToggleLightTheme,
102
+ toggleDarkTheme: wrappedToggleDarkTheme,
103
+ toggleColorTheme: themeState.toggleColorTheme,
104
+ createColorThemeToggle: themeState.createColorThemeToggle,
105
+ isColorThemeActive: themeState.isColorThemeActive,
74
106
  };
75
- return (0, jsx_runtime_1.jsx)(ViteThemeContext.Provider, { value: value, children: children });
107
+ return (0, jsx_runtime_1.jsx)(ViteThemeContext.Provider, { value: contextValue, children: children });
76
108
  };
77
109
  exports.ViteThemeProvider = ViteThemeProvider;
78
110
  const useViteTheme = () => {
@@ -13,34 +13,23 @@ const useThemeAnimation = (props = {}) => {
13
13
  const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
14
14
  const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
15
15
  const [mounted, setMounted] = (0, react_1.useState)(false);
16
+ // Inject base styles once on mount
16
17
  (0, react_1.useEffect)(() => {
17
18
  (0, animations_1.injectBaseStyles)();
19
+ setMounted(true);
18
20
  }, []);
19
21
  const [internalTheme, setInternalTheme] = (0, react_1.useState)(() => {
20
22
  if (!isBrowser)
21
23
  return defaultTheme;
22
- return defaultTheme;
24
+ const saved = localStorage.getItem(storageKey);
25
+ return saved && themes.indexOf(saved) !== -1 ? saved : defaultTheme;
23
26
  });
24
27
  const [internalColorTheme, setInternalColorTheme] = (0, react_1.useState)(() => {
25
28
  if (!isBrowser)
26
29
  return defaultColorTheme;
27
- return defaultColorTheme;
30
+ const saved = localStorage.getItem(colorStorageKey);
31
+ return saved && colorThemes.indexOf(saved) !== -1 ? saved : defaultColorTheme;
28
32
  });
29
- (0, react_1.useEffect)(() => {
30
- setMounted(true);
31
- if (isBrowser && !externalTheme) {
32
- const saved = localStorage.getItem(storageKey);
33
- if (saved && themes.indexOf(saved) !== -1) {
34
- setInternalTheme(saved);
35
- }
36
- }
37
- if (isBrowser && !externalColorTheme) {
38
- const saved = localStorage.getItem(colorStorageKey);
39
- if (saved && colorThemes.indexOf(saved) !== -1) {
40
- setInternalColorTheme(saved);
41
- }
42
- }
43
- }, []);
44
33
  const currentTheme = externalTheme ?? internalTheme;
45
34
  const currentColorTheme = externalColorTheme ?? internalColorTheme;
46
35
  const [, setSystemTheme] = (0, react_1.useState)(() => (0, animations_1.getSystemTheme)());
@@ -186,26 +175,45 @@ const useThemeAnimation = (props = {}) => {
186
175
  slideToX,
187
176
  slideToY,
188
177
  ]);
178
+ const switchColorTheme = (0, react_1.useCallback)((newColorTheme) => {
179
+ if (colorThemes.indexOf(newColorTheme) === -1) {
180
+ console.warn(`Color theme "${newColorTheme}" not found in available themes`);
181
+ return;
182
+ }
183
+ setColorTheme(newColorTheme);
184
+ }, [colorThemes, setColorTheme]);
189
185
  const toggleTheme = (0, react_1.useCallback)(async () => {
190
186
  const newTheme = resolvedTheme === 'dark' ? 'light' : 'dark';
191
187
  await switchTheme(newTheme);
192
188
  }, [resolvedTheme, switchTheme]);
193
- (0, react_1.useEffect)(() => {
194
- if (!isBrowser || !mounted)
189
+ const toggleLightTheme = (0, react_1.useCallback)(async () => {
190
+ if (resolvedTheme === 'light')
195
191
  return;
196
- if (resolvedTheme === 'dark') {
197
- document.documentElement.classList.add(globalClassName);
198
- }
199
- else {
200
- document.documentElement.classList.remove(globalClassName);
201
- }
202
- colorThemes.forEach(theme => {
203
- document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
204
- });
205
- if (currentColorTheme !== 'default') {
206
- document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
207
- }
208
- }, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes, mounted]);
192
+ await switchTheme('light');
193
+ }, [resolvedTheme, switchTheme]);
194
+ const toggleDarkTheme = (0, react_1.useCallback)(async () => {
195
+ if (resolvedTheme === 'dark')
196
+ return;
197
+ await switchTheme('dark');
198
+ }, [resolvedTheme, switchTheme]);
199
+ const toggleColorTheme = (0, react_1.useCallback)(() => {
200
+ const currentIndex = colorThemes.indexOf(currentColorTheme);
201
+ const nextIndex = (currentIndex + 1) % colorThemes.length;
202
+ const nextColorTheme = colorThemes[nextIndex];
203
+ setColorTheme(nextColorTheme);
204
+ }, [currentColorTheme, colorThemes, setColorTheme]);
205
+ const createColorThemeToggle = (0, react_1.useCallback)((targetColorTheme) => {
206
+ return () => {
207
+ if (colorThemes.indexOf(targetColorTheme) === -1) {
208
+ console.warn(`Color theme "${targetColorTheme}" not found in available themes`);
209
+ return;
210
+ }
211
+ setColorTheme(targetColorTheme);
212
+ };
213
+ }, [colorThemes, setColorTheme]);
214
+ const isColorThemeActive = (0, react_1.useCallback)((targetColorTheme) => {
215
+ return currentColorTheme === targetColorTheme;
216
+ }, [currentColorTheme]);
209
217
  return {
210
218
  ref,
211
219
  theme: currentTheme,
@@ -213,8 +221,14 @@ const useThemeAnimation = (props = {}) => {
213
221
  resolvedTheme,
214
222
  setTheme,
215
223
  setColorTheme,
216
- toggleTheme,
217
224
  switchTheme,
225
+ switchColorTheme,
226
+ toggleTheme,
227
+ toggleLightTheme,
228
+ toggleDarkTheme,
229
+ toggleColorTheme,
230
+ createColorThemeToggle,
231
+ isColorThemeActive,
218
232
  };
219
233
  };
220
234
  exports.useThemeAnimation = useThemeAnimation;
@@ -3,7 +3,6 @@ export { ThemeSwitcher } from './components/ThemeSwitcher';
3
3
  export { ThemeSelector } from './components/ThemeSelector';
4
4
  export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
5
5
  export { ViteThemeProvider, useViteTheme } from './components/ViteThemeProvider';
6
- export type { UseThemeAnimationProps, UseThemeAnimationReturn, ThemeSwitcherProps, ThemeSelectorProps, Theme, ColorTheme, } from './types';
7
- export type { ViteThemeContextType } from './components/ViteThemeProvider';
6
+ export type { UseThemeAnimationProps, UseThemeAnimationReturn, ThemeSwitcherProps, ThemeSelectorProps, Theme, ColorTheme, SlideDirection, } from './types';
8
7
  export { ThemeAnimationType } from './types';
9
8
  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
41
  switchTheme: (theme: Theme) => Promise<void>;
42
+ switchColorTheme: (colorTheme: string) => void;
43
+ toggleTheme: () => Promise<void>;
44
+ toggleLightTheme: () => Promise<void>;
45
+ toggleDarkTheme: () => 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[];
@@ -4,15 +4,21 @@ import { Theme, ColorTheme, ThemeAnimationType } from '../types';
4
4
  * Context type for the Spaceman Theme Provider
5
5
  */
6
6
  interface SpacemanThemeContextType {
7
+ ref: React.RefObject<HTMLButtonElement | null>;
7
8
  theme: Theme;
8
9
  colorTheme: ColorTheme;
9
10
  resolvedTheme: 'light' | 'dark';
10
11
  setTheme: (theme: Theme) => void;
11
12
  setColorTheme: (colorTheme: ColorTheme) => void;
12
- toggleTheme: () => Promise<void>;
13
13
  switchTheme: (theme: Theme) => Promise<void>;
14
+ switchColorTheme: (colorTheme: string) => void;
15
+ toggleTheme: () => Promise<void>;
16
+ toggleLightTheme: () => Promise<void>;
17
+ toggleDarkTheme: () => Promise<void>;
18
+ toggleColorTheme: () => void;
19
+ createColorThemeToggle: (targetColorTheme: string) => () => void;
20
+ isColorThemeActive: (targetColorTheme: string) => boolean;
14
21
  switchThemeFromElement: (theme: Theme, element: HTMLButtonElement) => Promise<void>;
15
- ref: React.RefObject<HTMLButtonElement | null>;
16
22
  }
17
23
  /**
18
24
  * Props for the Spaceman Theme Provider
@@ -56,28 +56,40 @@ export const SpacemanThemeProvider = ({ children, themes = ['light', 'dark', 'sy
56
56
  };
57
57
  if (!mounted) {
58
58
  const loadingContextValue = {
59
+ ref: { current: null },
59
60
  theme: defaultTheme,
60
61
  colorTheme: defaultColorTheme,
61
62
  resolvedTheme: defaultTheme === 'dark' ? 'dark' : 'light',
62
63
  setTheme: () => { },
63
64
  setColorTheme: () => { },
64
- toggleTheme: async () => { },
65
65
  switchTheme: async () => { },
66
66
  switchThemeFromElement: async () => { },
67
- ref: { current: null },
67
+ switchColorTheme: () => { },
68
+ toggleTheme: async () => { },
69
+ toggleLightTheme: async () => { },
70
+ toggleDarkTheme: async () => { },
71
+ toggleColorTheme: () => { },
72
+ createColorThemeToggle: () => () => { },
73
+ isColorThemeActive: () => false,
68
74
  };
69
75
  return (_jsx(SpacemanThemeContext.Provider, { value: loadingContextValue, children: children }));
70
76
  }
71
77
  const contextValue = {
78
+ ref: themeState.ref,
72
79
  theme: themeState.theme,
73
80
  colorTheme: themeState.colorTheme,
74
81
  resolvedTheme: themeState.resolvedTheme,
75
82
  setTheme: themeState.setTheme,
76
83
  setColorTheme: themeState.setColorTheme,
77
- toggleTheme: themeState.toggleTheme,
78
84
  switchTheme: themeState.switchTheme,
79
85
  switchThemeFromElement,
80
- ref: themeState.ref,
86
+ switchColorTheme: themeState.switchColorTheme,
87
+ toggleTheme: themeState.toggleTheme,
88
+ toggleLightTheme: themeState.toggleLightTheme,
89
+ toggleDarkTheme: themeState.toggleDarkTheme,
90
+ toggleColorTheme: themeState.toggleColorTheme,
91
+ createColorThemeToggle: themeState.createColorThemeToggle,
92
+ isColorThemeActive: themeState.isColorThemeActive,
81
93
  };
82
94
  return (_jsx(SpacemanThemeContext.Provider, { value: contextValue, children: children }));
83
95
  };
@@ -1,18 +1,37 @@
1
- import React from 'react';
2
- type Theme = 'light' | 'dark' | 'system';
3
- export interface ViteThemeContextType {
1
+ import React, { ReactNode } from 'react';
2
+ import { Theme, ColorTheme, ThemeAnimationType } from '../types';
3
+ interface ViteThemeContextType {
4
+ ref: React.RefObject<HTMLButtonElement | null>;
4
5
  theme: Theme;
6
+ colorTheme: ColorTheme;
5
7
  resolvedTheme: 'light' | 'dark';
6
- setTheme: (theme: Theme) => void;
7
8
  systemTheme: 'light' | 'dark';
9
+ setTheme: (theme: Theme) => void;
10
+ setColorTheme: (colorTheme: ColorTheme) => void;
11
+ switchTheme: (theme: Theme) => Promise<void>;
12
+ switchColorTheme: (colorTheme: string) => void;
13
+ toggleTheme: () => Promise<void>;
14
+ toggleLightTheme: () => Promise<void>;
15
+ toggleDarkTheme: () => Promise<void>;
16
+ toggleColorTheme: () => void;
17
+ createColorThemeToggle: (targetColorTheme: string) => () => void;
18
+ isColorThemeActive: (targetColorTheme: string) => boolean;
19
+ switchThemeFromElement: (theme: Theme, element: HTMLButtonElement) => Promise<void>;
8
20
  }
9
21
  interface ViteThemeProviderProps {
10
- children: React.ReactNode;
11
- attribute?: 'class' | 'data-theme';
22
+ children: ReactNode;
23
+ themes?: Theme[];
24
+ colorThemes?: ColorTheme[];
12
25
  defaultTheme?: Theme;
13
- enableSystem?: boolean;
26
+ defaultColorTheme?: ColorTheme;
27
+ animationType?: ThemeAnimationType;
28
+ duration?: number;
29
+ attribute?: 'class' | 'data-theme';
14
30
  disableTransitionOnChange?: boolean;
15
31
  storageKey?: string;
32
+ colorStorageKey?: string;
33
+ globalClassName?: string;
34
+ colorThemePrefix?: string;
16
35
  }
17
36
  export declare const ViteThemeProvider: React.FC<ViteThemeProviderProps>;
18
37
  export declare const useViteTheme: () => ViteThemeContextType;
@@ -1,75 +1,107 @@
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
+ 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-', }) => {
7
+ const themeState = useThemeAnimation({
8
+ themes,
9
+ colorThemes,
10
+ defaultTheme,
11
+ defaultColorTheme,
12
+ animationType,
13
+ duration,
14
+ storageKey,
15
+ colorStorageKey,
16
+ globalClassName: attribute === 'class' ? globalClassName : undefined,
17
+ colorThemePrefix,
19
18
  });
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);
19
+ const applyTransitionDisable = useCallback(() => {
20
+ if (!disableTransitionOnChange)
21
+ return () => { };
22
+ const css = document.createElement('style');
23
+ css.textContent = `*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`;
24
+ document.head.appendChild(css);
25
+ window.getComputedStyle(document.body);
26
+ return () => {
41
27
  setTimeout(() => {
42
28
  document.head.removeChild(css);
43
29
  }, 1);
44
- }
45
- if (attribute === 'class') {
46
- root.classList.remove('light', 'dark');
47
- root.classList.add(resolvedTheme);
30
+ };
31
+ }, [disableTransitionOnChange]);
32
+ const wrappedSetTheme = useCallback((theme) => {
33
+ const cleanup = applyTransitionDisable();
34
+ themeState.setTheme(theme);
35
+ cleanup();
36
+ }, [themeState.setTheme, applyTransitionDisable]);
37
+ const wrappedSwitchTheme = useCallback(async (theme) => {
38
+ const cleanup = applyTransitionDisable();
39
+ await themeState.switchTheme(theme);
40
+ cleanup();
41
+ }, [themeState.switchTheme, applyTransitionDisable]);
42
+ const wrappedToggleTheme = useCallback(async () => {
43
+ const cleanup = applyTransitionDisable();
44
+ await themeState.toggleTheme();
45
+ cleanup();
46
+ }, [themeState.toggleTheme, applyTransitionDisable]);
47
+ const wrappedToggleLightTheme = useCallback(async () => {
48
+ const cleanup = applyTransitionDisable();
49
+ await themeState.toggleLightTheme();
50
+ cleanup();
51
+ }, [themeState.toggleLightTheme, applyTransitionDisable]);
52
+ const wrappedToggleDarkTheme = useCallback(async () => {
53
+ const cleanup = applyTransitionDisable();
54
+ await themeState.toggleDarkTheme();
55
+ cleanup();
56
+ }, [themeState.toggleDarkTheme, applyTransitionDisable]);
57
+ const switchThemeFromElement = async (theme, element) => {
58
+ const cleanup = applyTransitionDisable();
59
+ if (themeState.ref.current) {
60
+ const originalRef = themeState.ref.current;
61
+ Object.defineProperty(themeState.ref, 'current', {
62
+ value: element,
63
+ writable: true,
64
+ configurable: true,
65
+ });
66
+ await themeState.switchTheme(theme);
67
+ Object.defineProperty(themeState.ref, 'current', {
68
+ value: originalRef,
69
+ writable: true,
70
+ configurable: true,
71
+ });
48
72
  }
49
73
  else {
50
- root.setAttribute(attribute, resolvedTheme);
51
- }
52
- try {
53
- localStorage.setItem(storageKey, theme);
74
+ Object.defineProperty(themeState.ref, 'current', {
75
+ value: element,
76
+ writable: true,
77
+ configurable: true,
78
+ });
79
+ await themeState.switchTheme(theme);
54
80
  }
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,
81
+ cleanup();
82
+ };
83
+ const systemTheme = typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
84
+ ? 'dark'
85
+ : 'light';
86
+ const contextValue = {
87
+ ref: themeState.ref,
88
+ theme: themeState.theme,
89
+ colorTheme: themeState.colorTheme,
90
+ resolvedTheme: themeState.resolvedTheme,
70
91
  systemTheme,
92
+ setTheme: wrappedSetTheme,
93
+ setColorTheme: themeState.setColorTheme,
94
+ switchTheme: wrappedSwitchTheme,
95
+ switchThemeFromElement,
96
+ switchColorTheme: themeState.switchColorTheme,
97
+ toggleTheme: wrappedToggleTheme,
98
+ toggleLightTheme: wrappedToggleLightTheme,
99
+ toggleDarkTheme: wrappedToggleDarkTheme,
100
+ toggleColorTheme: themeState.toggleColorTheme,
101
+ createColorThemeToggle: themeState.createColorThemeToggle,
102
+ isColorThemeActive: themeState.isColorThemeActive,
71
103
  };
72
- return _jsx(ViteThemeContext.Provider, { value: value, children: children });
104
+ return _jsx(ViteThemeContext.Provider, { value: contextValue, children: children });
73
105
  };
74
106
  export const useViteTheme = () => {
75
107
  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());
@@ -183,26 +172,45 @@ export const useThemeAnimation = (props = {}) => {
183
172
  slideToX,
184
173
  slideToY,
185
174
  ]);
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]);
186
182
  const toggleTheme = useCallback(async () => {
187
183
  const newTheme = resolvedTheme === 'dark' ? 'light' : 'dark';
188
184
  await switchTheme(newTheme);
189
185
  }, [resolvedTheme, switchTheme]);
190
- useEffect(() => {
191
- if (!isBrowser || !mounted)
186
+ const toggleLightTheme = useCallback(async () => {
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');
190
+ }, [resolvedTheme, switchTheme]);
191
+ const toggleDarkTheme = useCallback(async () => {
192
+ if (resolvedTheme === 'dark')
193
+ return;
194
+ await switchTheme('dark');
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
  };
@@ -3,7 +3,6 @@ export { ThemeSwitcher } from './components/ThemeSwitcher';
3
3
  export { ThemeSelector } from './components/ThemeSelector';
4
4
  export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
5
5
  export { ViteThemeProvider, useViteTheme } from './components/ViteThemeProvider';
6
- export type { UseThemeAnimationProps, UseThemeAnimationReturn, ThemeSwitcherProps, ThemeSelectorProps, Theme, ColorTheme, } from './types';
7
- export type { ViteThemeContextType } from './components/ViteThemeProvider';
6
+ export type { UseThemeAnimationProps, UseThemeAnimationReturn, ThemeSwitcherProps, ThemeSelectorProps, Theme, ColorTheme, SlideDirection, } from './types';
8
7
  export { ThemeAnimationType } from './types';
9
8
  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
41
  switchTheme: (theme: Theme) => Promise<void>;
42
+ switchColorTheme: (colorTheme: string) => void;
43
+ toggleTheme: () => Promise<void>;
44
+ toggleLightTheme: () => Promise<void>;
45
+ toggleDarkTheme: () => 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.0",
3
+ "version": "2.0.0",
4
4
  "description": "React theme switching with smooth view transition animations and multi-theme support",
5
5
  "keywords": [
6
6
  "react",