@space-man/react-theme-animation 1.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/LICENSE +21 -0
- package/README.md +623 -0
- package/dist/cjs/components/SpacemanThemeProvider.d.ts +51 -0
- package/dist/cjs/components/SpacemanThemeProvider.js +93 -0
- package/dist/cjs/components/ThemeSelector.d.ts +3 -0
- package/dist/cjs/components/ThemeSelector.js +39 -0
- package/dist/cjs/components/ThemeSwitcher.d.ts +3 -0
- package/dist/cjs/components/ThemeSwitcher.js +100 -0
- package/dist/cjs/components/ui/select.d.ts +15 -0
- package/dist/cjs/components/ui/select.js +83 -0
- package/dist/cjs/hooks/use-theme-animation.d.ts +2 -0
- package/dist/cjs/hooks/use-theme-animation.js +149 -0
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/types/index.d.ts +65 -0
- package/dist/cjs/types/index.js +8 -0
- package/dist/cjs/utils/animations.d.ts +18 -0
- package/dist/cjs/utils/animations.js +156 -0
- package/dist/components/SpacemanThemeProvider.d.ts +51 -0
- package/dist/components/SpacemanThemeProvider.js +88 -0
- package/dist/components/ThemeSelector.d.ts +3 -0
- package/dist/components/ThemeSelector.js +35 -0
- package/dist/components/ThemeSwitcher.d.ts +3 -0
- package/dist/components/ThemeSwitcher.js +96 -0
- package/dist/components/ui/select.d.ts +15 -0
- package/dist/components/ui/select.js +39 -0
- package/dist/esm/components/SpacemanThemeProvider.d.ts +51 -0
- package/dist/esm/components/SpacemanThemeProvider.js +88 -0
- package/dist/esm/components/ThemeSelector.d.ts +3 -0
- package/dist/esm/components/ThemeSelector.js +35 -0
- package/dist/esm/components/ThemeSwitcher.d.ts +3 -0
- package/dist/esm/components/ThemeSwitcher.js +96 -0
- package/dist/esm/components/ui/select.d.ts +15 -0
- package/dist/esm/components/ui/select.js +39 -0
- package/dist/esm/hooks/use-theme-animation.d.ts +2 -0
- package/dist/esm/hooks/use-theme-animation.js +145 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/types/index.d.ts +65 -0
- package/dist/esm/types/index.js +5 -0
- package/dist/esm/utils/animations.d.ts +18 -0
- package/dist/esm/utils/animations.js +145 -0
- package/dist/hooks/use-theme-animation.d.ts +2 -0
- package/dist/hooks/use-theme-animation.js +145 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.js +5 -0
- package/dist/utils/animations.d.ts +18 -0
- package/dist/utils/animations.js +145 -0
- package/package.json +74 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
+
import { flushSync } from 'react-dom';
|
|
3
|
+
import { ThemeAnimationType, } from '../types';
|
|
4
|
+
import { injectBaseStyles, resolveTheme, getSystemTheme, supportsViewTransitions, prefersReducedMotion, createCircleAnimation, createBlurCircleAnimation, } from '../utils/animations';
|
|
5
|
+
const isBrowser = typeof window !== 'undefined';
|
|
6
|
+
export const useThemeAnimation = (props = {}) => {
|
|
7
|
+
const { duration: propsDuration = 750, easing = 'ease-in-out', animationType = ThemeAnimationType.CIRCLE, blurAmount = 2, styleId = 'spaceman-theme-style', themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', globalClassName = 'dark', colorThemePrefix = 'theme-', storageKey = 'theme', colorStorageKey = 'color-theme', theme: externalTheme, colorTheme: externalColorTheme, onThemeChange, onColorThemeChange, } = props;
|
|
8
|
+
const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
|
|
9
|
+
const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
injectBaseStyles();
|
|
12
|
+
}, []);
|
|
13
|
+
const [internalTheme, setInternalTheme] = useState(() => {
|
|
14
|
+
if (!isBrowser)
|
|
15
|
+
return defaultTheme;
|
|
16
|
+
const saved = localStorage.getItem(storageKey);
|
|
17
|
+
return saved && themes.indexOf(saved) !== -1 ? saved : defaultTheme;
|
|
18
|
+
});
|
|
19
|
+
const [internalColorTheme, setInternalColorTheme] = useState(() => {
|
|
20
|
+
if (!isBrowser)
|
|
21
|
+
return defaultColorTheme;
|
|
22
|
+
const saved = localStorage.getItem(colorStorageKey);
|
|
23
|
+
return saved && colorThemes.indexOf(saved) !== -1 ? saved : defaultColorTheme;
|
|
24
|
+
});
|
|
25
|
+
const currentTheme = externalTheme ?? internalTheme;
|
|
26
|
+
const currentColorTheme = externalColorTheme ?? internalColorTheme;
|
|
27
|
+
const [, setSystemTheme] = useState(() => getSystemTheme());
|
|
28
|
+
const resolvedTheme = resolveTheme(currentTheme);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!isBrowser)
|
|
31
|
+
return;
|
|
32
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
33
|
+
const handleChange = () => setSystemTheme(getSystemTheme());
|
|
34
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
35
|
+
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
36
|
+
}, []);
|
|
37
|
+
// Apply theme classes to DOM
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isBrowser)
|
|
40
|
+
return;
|
|
41
|
+
// Apply theme class (light/dark) - use resolvedTheme instead of manual resolution
|
|
42
|
+
if (resolvedTheme === 'dark') {
|
|
43
|
+
document.documentElement.classList.add(globalClassName);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
document.documentElement.classList.remove(globalClassName);
|
|
47
|
+
}
|
|
48
|
+
// Apply color theme classes
|
|
49
|
+
// Remove all existing color theme classes
|
|
50
|
+
colorThemes.forEach(theme => {
|
|
51
|
+
document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
|
|
52
|
+
});
|
|
53
|
+
// Add current color theme class
|
|
54
|
+
document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
55
|
+
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
56
|
+
const ref = useRef(null);
|
|
57
|
+
const setTheme = useCallback((newTheme) => {
|
|
58
|
+
// Always save to localStorage
|
|
59
|
+
if (isBrowser) {
|
|
60
|
+
localStorage.setItem(storageKey, newTheme);
|
|
61
|
+
}
|
|
62
|
+
// Update internal state if no external control
|
|
63
|
+
if (externalTheme === undefined) {
|
|
64
|
+
setInternalTheme(newTheme);
|
|
65
|
+
}
|
|
66
|
+
// Call external callback if provided
|
|
67
|
+
if (onThemeChange) {
|
|
68
|
+
onThemeChange(newTheme);
|
|
69
|
+
}
|
|
70
|
+
}, [onThemeChange, externalTheme, storageKey]);
|
|
71
|
+
const setColorTheme = useCallback((newColorTheme) => {
|
|
72
|
+
// Always save to localStorage
|
|
73
|
+
if (isBrowser) {
|
|
74
|
+
localStorage.setItem(colorStorageKey, newColorTheme);
|
|
75
|
+
}
|
|
76
|
+
// Update internal state if no external control
|
|
77
|
+
if (externalColorTheme === undefined) {
|
|
78
|
+
setInternalColorTheme(newColorTheme);
|
|
79
|
+
}
|
|
80
|
+
// Call external callback if provided
|
|
81
|
+
if (onColorThemeChange) {
|
|
82
|
+
onColorThemeChange(newColorTheme);
|
|
83
|
+
}
|
|
84
|
+
}, [onColorThemeChange, externalColorTheme, colorStorageKey]);
|
|
85
|
+
const switchTheme = useCallback(async (newTheme) => {
|
|
86
|
+
if (!ref.current || !supportsViewTransitions() || prefersReducedMotion()) {
|
|
87
|
+
setTheme(newTheme);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const { top, left, width, height } = ref.current.getBoundingClientRect();
|
|
91
|
+
const x = left + width / 2;
|
|
92
|
+
const y = top + height / 2;
|
|
93
|
+
const animationConfig = {
|
|
94
|
+
x,
|
|
95
|
+
y,
|
|
96
|
+
duration,
|
|
97
|
+
easing,
|
|
98
|
+
animationType,
|
|
99
|
+
blurAmount,
|
|
100
|
+
styleId,
|
|
101
|
+
};
|
|
102
|
+
if (animationType === ThemeAnimationType.BLUR_CIRCLE) {
|
|
103
|
+
createBlurCircleAnimation(animationConfig);
|
|
104
|
+
}
|
|
105
|
+
await document.startViewTransition(() => {
|
|
106
|
+
flushSync(() => {
|
|
107
|
+
setTheme(newTheme);
|
|
108
|
+
});
|
|
109
|
+
}).ready;
|
|
110
|
+
if (animationType === ThemeAnimationType.CIRCLE) {
|
|
111
|
+
createCircleAnimation(animationConfig);
|
|
112
|
+
}
|
|
113
|
+
}, [setTheme, duration, easing, animationType, blurAmount, styleId]);
|
|
114
|
+
const toggleTheme = useCallback(async () => {
|
|
115
|
+
const newTheme = resolvedTheme === 'dark' ? 'light' : 'dark';
|
|
116
|
+
await switchTheme(newTheme);
|
|
117
|
+
}, [resolvedTheme, switchTheme]);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!isBrowser)
|
|
120
|
+
return;
|
|
121
|
+
const root = document.documentElement;
|
|
122
|
+
if (resolvedTheme === 'dark') {
|
|
123
|
+
root.classList.add(globalClassName);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
root.classList.remove(globalClassName);
|
|
127
|
+
}
|
|
128
|
+
colorThemes.forEach(theme => {
|
|
129
|
+
root.classList.remove(`${colorThemePrefix}${theme}`);
|
|
130
|
+
});
|
|
131
|
+
if (currentColorTheme !== 'default') {
|
|
132
|
+
root.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
133
|
+
}
|
|
134
|
+
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
135
|
+
return {
|
|
136
|
+
ref,
|
|
137
|
+
theme: currentTheme,
|
|
138
|
+
colorTheme: currentColorTheme,
|
|
139
|
+
resolvedTheme,
|
|
140
|
+
setTheme,
|
|
141
|
+
setColorTheme,
|
|
142
|
+
toggleTheme,
|
|
143
|
+
switchTheme,
|
|
144
|
+
};
|
|
145
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { useThemeAnimation } from './hooks/use-theme-animation';
|
|
2
|
+
export { ThemeSwitcher } from './components/ThemeSwitcher';
|
|
3
|
+
export { ThemeSelector } from './components/ThemeSelector';
|
|
4
|
+
export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
|
|
5
|
+
export type { UseThemeAnimationProps, UseThemeAnimationReturn, ThemeSwitcherProps, ThemeSelectorProps, Theme, ColorTheme, } from './types';
|
|
6
|
+
export { ThemeAnimationType } from './types';
|
|
7
|
+
export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { useThemeAnimation } from './hooks/use-theme-animation';
|
|
2
|
+
export { ThemeSwitcher } from './components/ThemeSwitcher';
|
|
3
|
+
export { ThemeSelector } from './components/ThemeSelector';
|
|
4
|
+
export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
|
|
5
|
+
export { ThemeAnimationType } from './types';
|
|
6
|
+
export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
export declare enum ThemeAnimationType {
|
|
3
|
+
CIRCLE = "circle",
|
|
4
|
+
BLUR_CIRCLE = "blur-circle"
|
|
5
|
+
}
|
|
6
|
+
export type Theme = 'light' | 'dark' | 'system';
|
|
7
|
+
export type ColorTheme = string;
|
|
8
|
+
export interface UseThemeAnimationProps {
|
|
9
|
+
duration?: number;
|
|
10
|
+
easing?: string;
|
|
11
|
+
animationType?: ThemeAnimationType;
|
|
12
|
+
blurAmount?: number;
|
|
13
|
+
styleId?: string;
|
|
14
|
+
themes?: Theme[];
|
|
15
|
+
colorThemes?: ColorTheme[];
|
|
16
|
+
defaultTheme?: Theme;
|
|
17
|
+
defaultColorTheme?: ColorTheme;
|
|
18
|
+
globalClassName?: string;
|
|
19
|
+
colorThemePrefix?: string;
|
|
20
|
+
storageKey?: string;
|
|
21
|
+
colorStorageKey?: string;
|
|
22
|
+
theme?: Theme;
|
|
23
|
+
colorTheme?: ColorTheme;
|
|
24
|
+
onThemeChange?: (theme: Theme) => void;
|
|
25
|
+
onColorThemeChange?: (colorTheme: ColorTheme) => void;
|
|
26
|
+
}
|
|
27
|
+
export interface UseThemeAnimationReturn {
|
|
28
|
+
ref: RefObject<HTMLButtonElement | null>;
|
|
29
|
+
theme: Theme;
|
|
30
|
+
colorTheme: ColorTheme;
|
|
31
|
+
resolvedTheme: 'light' | 'dark';
|
|
32
|
+
setTheme: (theme: Theme) => void;
|
|
33
|
+
setColorTheme: (colorTheme: ColorTheme) => void;
|
|
34
|
+
toggleTheme: () => Promise<void>;
|
|
35
|
+
switchTheme: (theme: Theme) => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export interface ThemeSwitcherProps {
|
|
38
|
+
themes?: Theme[];
|
|
39
|
+
currentTheme?: Theme;
|
|
40
|
+
onThemeChange?: (theme: Theme) => void;
|
|
41
|
+
animationType?: ThemeAnimationType;
|
|
42
|
+
duration?: number;
|
|
43
|
+
className?: string;
|
|
44
|
+
size?: 'sm' | 'md' | 'lg';
|
|
45
|
+
variant?: 'default' | 'outline' | 'ghost';
|
|
46
|
+
icons?: {
|
|
47
|
+
light?: React.ReactNode;
|
|
48
|
+
dark?: React.ReactNode;
|
|
49
|
+
system?: React.ReactNode;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface ThemeSelectorProps {
|
|
53
|
+
themes?: Theme[];
|
|
54
|
+
colorThemes?: ColorTheme[];
|
|
55
|
+
currentTheme?: Theme;
|
|
56
|
+
currentColorTheme?: ColorTheme;
|
|
57
|
+
onThemeChange?: (theme: Theme) => void;
|
|
58
|
+
onColorThemeChange?: (colorTheme: ColorTheme) => void;
|
|
59
|
+
animationType?: ThemeAnimationType;
|
|
60
|
+
duration?: number;
|
|
61
|
+
className?: string;
|
|
62
|
+
placeholder?: string;
|
|
63
|
+
themeLabel?: string;
|
|
64
|
+
colorThemeLabel?: string;
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ThemeAnimationType } from '../types';
|
|
2
|
+
export declare const injectBaseStyles: () => void;
|
|
3
|
+
export declare const createBlurCircleMask: (blur: number) => string;
|
|
4
|
+
export declare const getSystemTheme: () => "light" | "dark";
|
|
5
|
+
export declare const resolveTheme: (theme: string) => "light" | "dark";
|
|
6
|
+
export declare const supportsViewTransitions: () => boolean;
|
|
7
|
+
export declare const prefersReducedMotion: () => boolean;
|
|
8
|
+
export interface AnimationConfig {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
duration: number;
|
|
12
|
+
easing: string;
|
|
13
|
+
animationType: ThemeAnimationType;
|
|
14
|
+
blurAmount: number;
|
|
15
|
+
styleId: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const createCircleAnimation: (config: AnimationConfig) => void;
|
|
18
|
+
export declare const createBlurCircleAnimation: (config: AnimationConfig) => void;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const isBrowser = typeof window !== 'undefined';
|
|
2
|
+
export const injectBaseStyles = () => {
|
|
3
|
+
if (isBrowser) {
|
|
4
|
+
const styleId = 'spaceman-theme-base-style';
|
|
5
|
+
if (!document.getElementById(styleId)) {
|
|
6
|
+
const style = document.createElement('style');
|
|
7
|
+
style.id = styleId;
|
|
8
|
+
const isHighResolution = window.innerWidth >= 3000 || window.innerHeight >= 2000;
|
|
9
|
+
style.textContent = `
|
|
10
|
+
::view-transition-old(root),
|
|
11
|
+
::view-transition-new(root) {
|
|
12
|
+
animation: none;
|
|
13
|
+
mix-blend-mode: normal;
|
|
14
|
+
${isHighResolution ? 'transform: translateZ(0);' : ''}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
${isHighResolution
|
|
18
|
+
? `
|
|
19
|
+
::view-transition-group(root),
|
|
20
|
+
::view-transition-image-pair(root),
|
|
21
|
+
::view-transition-old(root),
|
|
22
|
+
::view-transition-new(root) {
|
|
23
|
+
backface-visibility: hidden;
|
|
24
|
+
perspective: 1000px;
|
|
25
|
+
transform: translate3d(0, 0, 0);
|
|
26
|
+
}
|
|
27
|
+
`
|
|
28
|
+
: ''}
|
|
29
|
+
`;
|
|
30
|
+
document.head.appendChild(style);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
export const createBlurCircleMask = (blur) => {
|
|
35
|
+
const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
|
|
36
|
+
const blurFilter = `<filter id="blur"><feGaussianBlur stdDeviation="${blur}" /></filter>`;
|
|
37
|
+
const circleRadius = isHighResolution ? 20 : 25;
|
|
38
|
+
return `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 100 100"><defs>${blurFilter}</defs><circle cx="0" cy="0" r="${circleRadius}" fill="white" filter="url(%23blur)"/></svg>')`;
|
|
39
|
+
};
|
|
40
|
+
export const getSystemTheme = () => {
|
|
41
|
+
if (!isBrowser)
|
|
42
|
+
return 'light';
|
|
43
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
44
|
+
};
|
|
45
|
+
export const resolveTheme = (theme) => {
|
|
46
|
+
if (theme === 'system') {
|
|
47
|
+
return getSystemTheme();
|
|
48
|
+
}
|
|
49
|
+
return theme === 'dark' ? 'dark' : 'light';
|
|
50
|
+
};
|
|
51
|
+
export const supportsViewTransitions = () => {
|
|
52
|
+
return isBrowser && 'startViewTransition' in document;
|
|
53
|
+
};
|
|
54
|
+
export const prefersReducedMotion = () => {
|
|
55
|
+
return isBrowser && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
56
|
+
};
|
|
57
|
+
export const createCircleAnimation = (config) => {
|
|
58
|
+
const { x, y, duration, easing } = config;
|
|
59
|
+
// Calculate the distance to each corner of the viewport
|
|
60
|
+
const topLeft = Math.hypot(x, y);
|
|
61
|
+
const topRight = Math.hypot(window.innerWidth - x, y);
|
|
62
|
+
const bottomLeft = Math.hypot(x, window.innerHeight - y);
|
|
63
|
+
const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y);
|
|
64
|
+
// Find the maximum distance to ensure animation covers the entire viewport
|
|
65
|
+
const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight);
|
|
66
|
+
document.documentElement.animate({
|
|
67
|
+
clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${maxRadius}px at ${x}px ${y}px)`],
|
|
68
|
+
}, {
|
|
69
|
+
duration,
|
|
70
|
+
easing,
|
|
71
|
+
pseudoElement: '::view-transition-new(root)',
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
export const createBlurCircleAnimation = (config) => {
|
|
75
|
+
const { x, y, duration, easing, blurAmount, styleId } = config;
|
|
76
|
+
const existingStyle = document.getElementById(styleId);
|
|
77
|
+
if (existingStyle) {
|
|
78
|
+
existingStyle.remove();
|
|
79
|
+
}
|
|
80
|
+
const viewportSize = Math.max(window.innerWidth, window.innerHeight) + 200;
|
|
81
|
+
const isHighResolution = window.innerWidth >= 3000 || window.innerHeight >= 2000;
|
|
82
|
+
const scaleFactor = isHighResolution ? 2.5 : 4;
|
|
83
|
+
const optimalMaskSize = isHighResolution
|
|
84
|
+
? Math.min(viewportSize * scaleFactor, 5000)
|
|
85
|
+
: viewportSize * scaleFactor;
|
|
86
|
+
const topLeft = Math.hypot(x, y);
|
|
87
|
+
const topRight = Math.hypot(window.innerWidth - x, y);
|
|
88
|
+
const bottomLeft = Math.hypot(x, window.innerHeight - y);
|
|
89
|
+
const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y);
|
|
90
|
+
const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight);
|
|
91
|
+
const styleElement = document.createElement('style');
|
|
92
|
+
styleElement.id = styleId;
|
|
93
|
+
const blurFactor = isHighResolution ? 1.5 : 1.2;
|
|
94
|
+
const finalMaskSize = Math.max(optimalMaskSize, maxRadius * 2.5);
|
|
95
|
+
styleElement.textContent = `
|
|
96
|
+
::view-transition-group(root) {
|
|
97
|
+
animation-duration: ${duration}ms;
|
|
98
|
+
animation-timing-function: ${isHighResolution
|
|
99
|
+
? 'cubic-bezier(0.2, 0, 0.2, 1)'
|
|
100
|
+
: 'linear(' +
|
|
101
|
+
'0 0%, 0.2342 12.49%, 0.4374 24.99%,' +
|
|
102
|
+
'0.6093 37.49%, 0.6835 43.74%,' +
|
|
103
|
+
'0.7499 49.99%, 0.8086 56.25%,' +
|
|
104
|
+
'0.8593 62.5%, 0.9023 68.75%, 0.9375 75%,' +
|
|
105
|
+
'0.9648 81.25%, 0.9844 87.5%,' +
|
|
106
|
+
'0.9961 93.75%, 1 100%' +
|
|
107
|
+
')'};
|
|
108
|
+
will-change: transform;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
::view-transition-new(root) {
|
|
112
|
+
mask: ${createBlurCircleMask(blurAmount * blurFactor)} 0 0 / 100% 100% no-repeat;
|
|
113
|
+
mask-position: ${x}px ${y}px;
|
|
114
|
+
animation: spaceman-mask-scale ${duration}ms ${easing};
|
|
115
|
+
transform-origin: ${x}px ${y}px;
|
|
116
|
+
will-change: mask-size, mask-position;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
::view-transition-old(root),
|
|
120
|
+
.dark::view-transition-old(root) {
|
|
121
|
+
animation: spaceman-mask-scale ${duration}ms ${easing};
|
|
122
|
+
transform-origin: ${x}px ${y}px;
|
|
123
|
+
z-index: -1;
|
|
124
|
+
will-change: mask-size, mask-position;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@keyframes spaceman-mask-scale {
|
|
128
|
+
0% {
|
|
129
|
+
mask-size: 0px;
|
|
130
|
+
mask-position: ${x}px ${y}px;
|
|
131
|
+
}
|
|
132
|
+
100% {
|
|
133
|
+
mask-size: ${finalMaskSize}px;
|
|
134
|
+
mask-position: ${x - finalMaskSize / 2}px ${y - finalMaskSize / 2}px;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
document.head.appendChild(styleElement);
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
const styleElement = document.getElementById(styleId);
|
|
141
|
+
if (styleElement) {
|
|
142
|
+
styleElement.remove();
|
|
143
|
+
}
|
|
144
|
+
}, duration);
|
|
145
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@space-man/react-theme-animation",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React theme switching with smooth view transition animations and multi-theme support",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"reactjs",
|
|
8
|
+
"hooks",
|
|
9
|
+
"theme",
|
|
10
|
+
"animation",
|
|
11
|
+
"switch",
|
|
12
|
+
"typescript",
|
|
13
|
+
"nextjs",
|
|
14
|
+
"dark",
|
|
15
|
+
"light",
|
|
16
|
+
"view-transition",
|
|
17
|
+
"tailwindcss",
|
|
18
|
+
"shadcn"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"registry": "https://registry.npmjs.org/",
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/tonyedgal/spaceman-react-theme-animation.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/tonyedgal/spaceman-react-theme-animation#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/tonyedgal/spaceman-react-theme-animation/issues"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"author": "tonyedgal",
|
|
34
|
+
"main": "dist/cjs/index.js",
|
|
35
|
+
"module": "dist/esm/index.js",
|
|
36
|
+
"types": "dist/esm/index.d.ts",
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"prepare": "npm run build",
|
|
44
|
+
"prepublishOnly": "npm run build",
|
|
45
|
+
"build": "npm run build:esm && npm run build:cjs",
|
|
46
|
+
"build:esm": "tsc --module esnext --outDir dist/esm",
|
|
47
|
+
"build:cjs": "tsc --module commonjs --outDir dist/cjs",
|
|
48
|
+
"dev": "tsc --watch",
|
|
49
|
+
"format": "prettier --write . --config ./.prettierrc",
|
|
50
|
+
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
|
51
|
+
"lint:fix": "eslint --fix . --ext .{js,jsx,ts,tsx}",
|
|
52
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": ">=17",
|
|
56
|
+
"react-dom": ">=17",
|
|
57
|
+
"motion": ">=11",
|
|
58
|
+
"@radix-ui/react-select": ">=2"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/react": "^18.3.12",
|
|
62
|
+
"@types/react-dom": "^18.3.1",
|
|
63
|
+
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
|
64
|
+
"@typescript-eslint/parser": "^8.15.0",
|
|
65
|
+
"eslint": "^9.15.0",
|
|
66
|
+
"eslint-plugin-react": "^7.37.2",
|
|
67
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
68
|
+
"prettier": "^3.3.3",
|
|
69
|
+
"typescript": "^5.6.3"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"clsx": "^2.1.1"
|
|
73
|
+
}
|
|
74
|
+
}
|