@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 +72 -19
- package/dist/cjs/components/SpacemanThemeProvider.d.ts +8 -2
- package/dist/cjs/components/SpacemanThemeProvider.js +16 -4
- package/dist/cjs/components/ViteThemeProvider.d.ts +26 -7
- package/dist/cjs/components/ViteThemeProvider.js +92 -60
- package/dist/cjs/hooks/use-theme-animation.js +47 -33
- package/dist/cjs/index.d.ts +1 -2
- package/dist/cjs/types/index.d.ts +7 -1
- package/dist/esm/components/SpacemanThemeProvider.d.ts +8 -2
- package/dist/esm/components/SpacemanThemeProvider.js +16 -4
- package/dist/esm/components/ViteThemeProvider.d.ts +26 -7
- package/dist/esm/components/ViteThemeProvider.js +94 -62
- package/dist/esm/hooks/use-theme-animation.js +47 -33
- package/dist/esm/index.d.ts +1 -2
- package/dist/esm/types/index.d.ts +7 -1
- package/package.json +1 -1
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
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
|
-
|
|
98
|
+
**[See the complete Getting Started Guide](./getting-started.md)** for detailed examples, advanced configurations, and best practices.
|
|
46
99
|
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
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:
|
|
11
|
-
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
themes?: Theme[];
|
|
24
|
+
colorThemes?: ColorTheme[];
|
|
12
25
|
defaultTheme?: Theme;
|
|
13
|
-
|
|
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,
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
24
|
-
if (!
|
|
25
|
-
return;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
Object.defineProperty(themeState.ref, 'current', {
|
|
78
|
+
value: element,
|
|
79
|
+
writable: true,
|
|
80
|
+
configurable: true,
|
|
81
|
+
});
|
|
82
|
+
await themeState.switchTheme(theme);
|
|
57
83
|
}
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
194
|
-
if (
|
|
189
|
+
const toggleLightTheme = (0, react_1.useCallback)(async () => {
|
|
190
|
+
if (resolvedTheme === 'light')
|
|
195
191
|
return;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}, [
|
|
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;
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
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:
|
|
11
|
-
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
themes?: Theme[];
|
|
24
|
+
colorThemes?: ColorTheme[];
|
|
12
25
|
defaultTheme?: Theme;
|
|
13
|
-
|
|
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 {
|
|
2
|
-
import { createContext, useContext,
|
|
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,
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
if (!
|
|
22
|
-
return;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
Object.defineProperty(themeState.ref, 'current', {
|
|
75
|
+
value: element,
|
|
76
|
+
writable: true,
|
|
77
|
+
configurable: true,
|
|
78
|
+
});
|
|
79
|
+
await themeState.switchTheme(theme);
|
|
54
80
|
}
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
if (
|
|
186
|
+
const toggleLightTheme = useCallback(async () => {
|
|
187
|
+
if (resolvedTheme === 'light')
|
|
192
188
|
return;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}, [
|
|
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
|
};
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -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[];
|