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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,13 @@
1
1
  # @space-man/react-theme-animation
2
2
 
3
- React theme switching with smooth view transition animations, multi-theme support, and synchronized state management.
3
+ https://github.com/user-attachments/assets/2819cf70-c474-478e-b821-6b26457c8d4a
4
4
 
5
- ## Demo
5
+ React theme switching with smooth view transition animations, multi-theme support, and synchronized state management.
6
6
 
7
- <video src="https://tonyedgal.github.io/spaceman-react-theme-animation/Demo.mov" controls width="600" loop autoplay muted></video>
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>
8
11
 
9
12
  ## Live Demo
10
13
 
@@ -278,7 +281,7 @@ Standalone hook for theme management and animations.
278
281
  const { theme, colorTheme, switchTheme, setColorTheme, toggleTheme, ref } = useThemeAnimation({
279
282
  themes: ['light', 'dark', 'system'],
280
283
  colorThemes: ['default', 'blue', 'green', 'purple'],
281
- animationType: ThemeAnimationType.SCALE,
284
+ animationType: ThemeAnimationType.SLIDE,
282
285
  duration: 800,
283
286
  onThemeChange: theme => console.log('Theme changed:', theme),
284
287
  onColorThemeChange: colorTheme => console.log('Color changed:', colorTheme),
@@ -287,17 +290,18 @@ const { theme, colorTheme, switchTheme, setColorTheme, toggleTheme, ref } = useT
287
290
 
288
291
  #### Options
289
292
 
290
- | Property | Type | Default | Description |
291
- | -------------------- | ---------------------------------- | ----------------------------- | ------------------------------------- |
292
- | `themes` | `Theme[]` | `['light', 'dark', 'system']` | Available themes |
293
- | `colorThemes` | `ColorTheme[]` | `['default']` | Available color themes |
294
- | `theme` | `Theme` | `'system'` | Initial theme |
295
- | `colorTheme` | `ColorTheme` | `'default'` | Initial color theme |
296
- | `animationType` | `ThemeAnimationType` | `ThemeAnimationType.CIRCLE` | Animation type |
297
- | `duration` | `number` | `500` | Animation duration in ms |
298
- | `blurAmount` | `number` | `2` | Blur amount for blur-circle animation |
299
- | `onThemeChange` | `(theme: Theme) => void` | - | Theme change callback |
300
- | `onColorThemeChange` | `(colorTheme: ColorTheme) => void` | - | Color theme change callback |
293
+ | Property | Type | Default | Description |
294
+ | -------------------- | ---------------------------------- | ----------------------------- | --------------------------------------------------- |
295
+ | `themes` | `Theme[]` | `['light', 'dark', 'system']` | Available themes |
296
+ | `colorThemes` | `ColorTheme[]` | `['default']` | Available color themes |
297
+ | `theme` | `Theme` | `'system'` | Initial theme |
298
+ | `colorTheme` | `ColorTheme` | `'default'` | Initial color theme |
299
+ | `animationType` | `ThemeAnimationType` | `ThemeAnimationType.CIRCLE` | Animation type. Options: CIRCLE, BLUR_CIRCLE, SLIDE |
300
+ | `duration` | `number` | `500` | Animation duration in ms |
301
+ | `blurAmount` | `number` | `2` | Blur amount for blur-circle animation |
302
+ | `onThemeChange` | `(theme: Theme) => void` | - | Theme change callback |
303
+ | `onColorThemeChange` | `(colorTheme: ColorTheme) => void` | - | Color theme change callback |
304
+ | `slideDirection` | `SlideDirection` | `left` | Animation slide direction |
301
305
 
302
306
  #### Return
303
307
 
@@ -7,7 +7,9 @@ const types_1 = require("../types");
7
7
  const animations_1 = require("../utils/animations");
8
8
  const isBrowser = typeof window !== 'undefined';
9
9
  const useThemeAnimation = (props = {}) => {
10
- const { duration: propsDuration = 750, easing = 'ease-in-out', animationType = types_1.ThemeAnimationType.CIRCLE, blurAmount = 2, styleId = 'spaceman-theme-style', themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', globalClassName = 'dark', colorThemePrefix = 'theme-', storageKey = 'theme', colorStorageKey = 'color-theme', theme: externalTheme, colorTheme: externalColorTheme, onThemeChange, onColorThemeChange, } = props;
10
+ const { duration: propsDuration = 750, easing = 'ease-in-out', animationType = types_1.ThemeAnimationType.CIRCLE, blurAmount = 2, styleId = 'spaceman-theme-style', themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', globalClassName = 'dark', colorThemePrefix = 'theme-', storageKey = 'theme', colorStorageKey = 'color-theme', theme: externalTheme, colorTheme: externalColorTheme, onThemeChange, onColorThemeChange,
11
+ // Slide animation options
12
+ slideDirection = 'left', slideFromX, slideFromY, slideToX = 0, slideToY = 0, } = props;
11
13
  const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
12
14
  const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
13
15
  const [mounted, setMounted] = (0, react_1.useState)(false);
@@ -100,21 +102,69 @@ const useThemeAnimation = (props = {}) => {
100
102
  setTheme(newTheme);
101
103
  return;
102
104
  }
103
- const { top, left, width, height } = ref.current.getBoundingClientRect();
104
- const x = left + width / 2;
105
- const y = top + height / 2;
106
- const animationConfig = {
107
- x,
108
- y,
109
- duration,
110
- easing,
111
- animationType,
112
- blurAmount,
113
- styleId,
105
+ // Helper function to convert direction to from coordinates
106
+ const getSlideFromCoords = (direction) => {
107
+ switch (direction) {
108
+ case 'left':
109
+ return { a: -100, b: 0 };
110
+ case 'right':
111
+ return { a: 100, b: 0 };
112
+ case 'top':
113
+ return { a: 0, b: -100 };
114
+ case 'bottom':
115
+ return { a: 0, b: 100 };
116
+ case 'top-left':
117
+ return { a: -100, b: -100 };
118
+ case 'top-right':
119
+ return { a: 100, b: -100 };
120
+ case 'bottom-left':
121
+ return { a: -100, b: 100 };
122
+ case 'bottom-right':
123
+ return { a: 100, b: 100 };
124
+ default:
125
+ return { a: -100, b: 0 };
126
+ }
114
127
  };
128
+ let animationConfig;
129
+ if (animationType === types_1.ThemeAnimationType.SLIDE) {
130
+ // Use custom coordinates if provided, otherwise use direction
131
+ const fromCoords = slideFromX !== undefined && slideFromY !== undefined
132
+ ? { a: slideFromX, b: slideFromY }
133
+ : getSlideFromCoords(slideDirection);
134
+ animationConfig = {
135
+ a: fromCoords.a,
136
+ b: fromCoords.b,
137
+ x: slideToX,
138
+ y: slideToY,
139
+ duration,
140
+ easing,
141
+ animationType,
142
+ blurAmount,
143
+ styleId,
144
+ };
145
+ }
146
+ else {
147
+ // For circle and blur circle animations, use button center
148
+ const { top, left, width, height } = ref.current.getBoundingClientRect();
149
+ const x = left + width / 2;
150
+ const y = top + height / 2;
151
+ animationConfig = {
152
+ x,
153
+ y,
154
+ duration,
155
+ easing,
156
+ animationType,
157
+ blurAmount,
158
+ styleId,
159
+ };
160
+ }
115
161
  if (animationType === types_1.ThemeAnimationType.BLUR_CIRCLE) {
116
162
  (0, animations_1.createBlurCircleAnimation)(animationConfig);
117
163
  }
164
+ if (animationType === types_1.ThemeAnimationType.SLIDE) {
165
+ (0, animations_1.createSlideAnimation)(animationConfig);
166
+ }
167
+ // Start the view transition
118
168
  await document.startViewTransition(() => {
119
169
  (0, react_dom_1.flushSync)(() => {
120
170
  setTheme(newTheme);
@@ -123,7 +173,19 @@ const useThemeAnimation = (props = {}) => {
123
173
  if (animationType === types_1.ThemeAnimationType.CIRCLE) {
124
174
  (0, animations_1.createCircleAnimation)(animationConfig);
125
175
  }
126
- }, [setTheme, duration, easing, animationType, blurAmount, styleId]);
176
+ }, [
177
+ setTheme,
178
+ duration,
179
+ easing,
180
+ animationType,
181
+ blurAmount,
182
+ styleId,
183
+ slideDirection,
184
+ slideFromX,
185
+ slideFromY,
186
+ slideToX,
187
+ slideToY,
188
+ ]);
127
189
  const toggleTheme = (0, react_1.useCallback)(async () => {
128
190
  const newTheme = resolvedTheme === 'dark' ? 'light' : 'dark';
129
191
  await switchTheme(newTheme);
@@ -1,8 +1,10 @@
1
1
  import { RefObject } from 'react';
2
2
  export declare enum ThemeAnimationType {
3
3
  CIRCLE = "circle",
4
- BLUR_CIRCLE = "blur-circle"
4
+ BLUR_CIRCLE = "blur-circle",
5
+ SLIDE = "slide"
5
6
  }
7
+ export type SlideDirection = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
6
8
  export type Theme = 'light' | 'dark' | 'system';
7
9
  export type ColorTheme = string;
8
10
  export interface UseThemeAnimationProps {
@@ -23,6 +25,11 @@ export interface UseThemeAnimationProps {
23
25
  colorTheme?: ColorTheme;
24
26
  onThemeChange?: (theme: Theme) => void;
25
27
  onColorThemeChange?: (colorTheme: ColorTheme) => void;
28
+ slideDirection?: SlideDirection;
29
+ slideFromX?: number;
30
+ slideFromY?: number;
31
+ slideToX?: number;
32
+ slideToY?: number;
26
33
  }
27
34
  export interface UseThemeAnimationReturn {
28
35
  ref: RefObject<HTMLButtonElement | null>;
@@ -5,4 +5,5 @@ var ThemeAnimationType;
5
5
  (function (ThemeAnimationType) {
6
6
  ThemeAnimationType["CIRCLE"] = "circle";
7
7
  ThemeAnimationType["BLUR_CIRCLE"] = "blur-circle";
8
+ ThemeAnimationType["SLIDE"] = "slide";
8
9
  })(ThemeAnimationType || (exports.ThemeAnimationType = ThemeAnimationType = {}));
@@ -6,6 +6,8 @@ export declare const resolveTheme: (theme: string) => "light" | "dark";
6
6
  export declare const supportsViewTransitions: () => boolean;
7
7
  export declare const prefersReducedMotion: () => boolean;
8
8
  export interface AnimationConfig {
9
+ a?: number;
10
+ b?: number;
9
11
  x: number;
10
12
  y: number;
11
13
  duration: number;
@@ -15,4 +17,5 @@ export interface AnimationConfig {
15
17
  styleId: string;
16
18
  }
17
19
  export declare const createCircleAnimation: (config: AnimationConfig) => void;
20
+ export declare const createSlideAnimation: (config: AnimationConfig) => void;
18
21
  export declare const createBlurCircleAnimation: (config: AnimationConfig) => void;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createBlurCircleAnimation = exports.createCircleAnimation = exports.prefersReducedMotion = exports.supportsViewTransitions = exports.resolveTheme = exports.getSystemTheme = exports.createBlurCircleMask = exports.injectBaseStyles = void 0;
3
+ exports.createBlurCircleAnimation = exports.createSlideAnimation = exports.createCircleAnimation = exports.prefersReducedMotion = exports.supportsViewTransitions = exports.resolveTheme = exports.getSystemTheme = exports.createBlurCircleMask = exports.injectBaseStyles = void 0;
4
4
  const isBrowser = typeof window !== 'undefined';
5
5
  const injectBaseStyles = () => {
6
6
  if (isBrowser) {
@@ -81,6 +81,39 @@ const createCircleAnimation = (config) => {
81
81
  });
82
82
  };
83
83
  exports.createCircleAnimation = createCircleAnimation;
84
+ const createSlideAnimation = (config) => {
85
+ const { a = -100, b = 0, x = 0, y = 0, duration, styleId } = config;
86
+ const styleElement = document.createElement('style');
87
+ styleElement.id = styleId;
88
+ styleElement.textContent = `
89
+ ::view-transition-old(root) {
90
+ animation-delay: ${duration}ms;
91
+ }
92
+
93
+ ::view-transition-new(root) {
94
+ animation: move-in ${duration}ms;
95
+ animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);
96
+ }
97
+
98
+ @keyframes move-in {
99
+ from {
100
+ translate: ${a}% ${b}%;
101
+ }
102
+ to {
103
+ translate: ${x}% ${y}%;
104
+ }
105
+ }
106
+ `;
107
+ document.head.appendChild(styleElement);
108
+ // Clean up after animation
109
+ setTimeout(() => {
110
+ const styleElement = document.getElementById(styleId);
111
+ if (styleElement) {
112
+ styleElement.remove();
113
+ }
114
+ }, duration);
115
+ };
116
+ exports.createSlideAnimation = createSlideAnimation;
84
117
  const createBlurCircleAnimation = (config) => {
85
118
  const { x, y, duration, easing, blurAmount, styleId } = config;
86
119
  const existingStyle = document.getElementById(styleId);
@@ -1,10 +1,12 @@
1
1
  import { useEffect, useRef, useState, useCallback } from 'react';
2
2
  import { flushSync } from 'react-dom';
3
3
  import { ThemeAnimationType, } from '../types';
4
- import { injectBaseStyles, resolveTheme, getSystemTheme, supportsViewTransitions, prefersReducedMotion, createCircleAnimation, createBlurCircleAnimation, } from '../utils/animations';
4
+ import { injectBaseStyles, resolveTheme, getSystemTheme, supportsViewTransitions, prefersReducedMotion, createCircleAnimation, createBlurCircleAnimation, createSlideAnimation, } from '../utils/animations';
5
5
  const isBrowser = typeof window !== 'undefined';
6
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;
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,
8
+ // Slide animation options
9
+ slideDirection = 'left', slideFromX, slideFromY, slideToX = 0, slideToY = 0, } = props;
8
10
  const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
9
11
  const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
10
12
  const [mounted, setMounted] = useState(false);
@@ -97,21 +99,69 @@ export const useThemeAnimation = (props = {}) => {
97
99
  setTheme(newTheme);
98
100
  return;
99
101
  }
100
- const { top, left, width, height } = ref.current.getBoundingClientRect();
101
- const x = left + width / 2;
102
- const y = top + height / 2;
103
- const animationConfig = {
104
- x,
105
- y,
106
- duration,
107
- easing,
108
- animationType,
109
- blurAmount,
110
- styleId,
102
+ // Helper function to convert direction to from coordinates
103
+ const getSlideFromCoords = (direction) => {
104
+ switch (direction) {
105
+ case 'left':
106
+ return { a: -100, b: 0 };
107
+ case 'right':
108
+ return { a: 100, b: 0 };
109
+ case 'top':
110
+ return { a: 0, b: -100 };
111
+ case 'bottom':
112
+ return { a: 0, b: 100 };
113
+ case 'top-left':
114
+ return { a: -100, b: -100 };
115
+ case 'top-right':
116
+ return { a: 100, b: -100 };
117
+ case 'bottom-left':
118
+ return { a: -100, b: 100 };
119
+ case 'bottom-right':
120
+ return { a: 100, b: 100 };
121
+ default:
122
+ return { a: -100, b: 0 };
123
+ }
111
124
  };
125
+ let animationConfig;
126
+ if (animationType === ThemeAnimationType.SLIDE) {
127
+ // Use custom coordinates if provided, otherwise use direction
128
+ const fromCoords = slideFromX !== undefined && slideFromY !== undefined
129
+ ? { a: slideFromX, b: slideFromY }
130
+ : getSlideFromCoords(slideDirection);
131
+ animationConfig = {
132
+ a: fromCoords.a,
133
+ b: fromCoords.b,
134
+ x: slideToX,
135
+ y: slideToY,
136
+ duration,
137
+ easing,
138
+ animationType,
139
+ blurAmount,
140
+ styleId,
141
+ };
142
+ }
143
+ else {
144
+ // For circle and blur circle animations, use button center
145
+ const { top, left, width, height } = ref.current.getBoundingClientRect();
146
+ const x = left + width / 2;
147
+ const y = top + height / 2;
148
+ animationConfig = {
149
+ x,
150
+ y,
151
+ duration,
152
+ easing,
153
+ animationType,
154
+ blurAmount,
155
+ styleId,
156
+ };
157
+ }
112
158
  if (animationType === ThemeAnimationType.BLUR_CIRCLE) {
113
159
  createBlurCircleAnimation(animationConfig);
114
160
  }
161
+ if (animationType === ThemeAnimationType.SLIDE) {
162
+ createSlideAnimation(animationConfig);
163
+ }
164
+ // Start the view transition
115
165
  await document.startViewTransition(() => {
116
166
  flushSync(() => {
117
167
  setTheme(newTheme);
@@ -120,7 +170,19 @@ export const useThemeAnimation = (props = {}) => {
120
170
  if (animationType === ThemeAnimationType.CIRCLE) {
121
171
  createCircleAnimation(animationConfig);
122
172
  }
123
- }, [setTheme, duration, easing, animationType, blurAmount, styleId]);
173
+ }, [
174
+ setTheme,
175
+ duration,
176
+ easing,
177
+ animationType,
178
+ blurAmount,
179
+ styleId,
180
+ slideDirection,
181
+ slideFromX,
182
+ slideFromY,
183
+ slideToX,
184
+ slideToY,
185
+ ]);
124
186
  const toggleTheme = useCallback(async () => {
125
187
  const newTheme = resolvedTheme === 'dark' ? 'light' : 'dark';
126
188
  await switchTheme(newTheme);
@@ -1,8 +1,10 @@
1
1
  import { RefObject } from 'react';
2
2
  export declare enum ThemeAnimationType {
3
3
  CIRCLE = "circle",
4
- BLUR_CIRCLE = "blur-circle"
4
+ BLUR_CIRCLE = "blur-circle",
5
+ SLIDE = "slide"
5
6
  }
7
+ export type SlideDirection = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
6
8
  export type Theme = 'light' | 'dark' | 'system';
7
9
  export type ColorTheme = string;
8
10
  export interface UseThemeAnimationProps {
@@ -23,6 +25,11 @@ export interface UseThemeAnimationProps {
23
25
  colorTheme?: ColorTheme;
24
26
  onThemeChange?: (theme: Theme) => void;
25
27
  onColorThemeChange?: (colorTheme: ColorTheme) => void;
28
+ slideDirection?: SlideDirection;
29
+ slideFromX?: number;
30
+ slideFromY?: number;
31
+ slideToX?: number;
32
+ slideToY?: number;
26
33
  }
27
34
  export interface UseThemeAnimationReturn {
28
35
  ref: RefObject<HTMLButtonElement | null>;
@@ -2,4 +2,5 @@ export var ThemeAnimationType;
2
2
  (function (ThemeAnimationType) {
3
3
  ThemeAnimationType["CIRCLE"] = "circle";
4
4
  ThemeAnimationType["BLUR_CIRCLE"] = "blur-circle";
5
+ ThemeAnimationType["SLIDE"] = "slide";
5
6
  })(ThemeAnimationType || (ThemeAnimationType = {}));
@@ -6,6 +6,8 @@ export declare const resolveTheme: (theme: string) => "light" | "dark";
6
6
  export declare const supportsViewTransitions: () => boolean;
7
7
  export declare const prefersReducedMotion: () => boolean;
8
8
  export interface AnimationConfig {
9
+ a?: number;
10
+ b?: number;
9
11
  x: number;
10
12
  y: number;
11
13
  duration: number;
@@ -15,4 +17,5 @@ export interface AnimationConfig {
15
17
  styleId: string;
16
18
  }
17
19
  export declare const createCircleAnimation: (config: AnimationConfig) => void;
20
+ export declare const createSlideAnimation: (config: AnimationConfig) => void;
18
21
  export declare const createBlurCircleAnimation: (config: AnimationConfig) => void;
@@ -71,6 +71,38 @@ export const createCircleAnimation = (config) => {
71
71
  pseudoElement: '::view-transition-new(root)',
72
72
  });
73
73
  };
74
+ export const createSlideAnimation = (config) => {
75
+ const { a = -100, b = 0, x = 0, y = 0, duration, styleId } = config;
76
+ const styleElement = document.createElement('style');
77
+ styleElement.id = styleId;
78
+ styleElement.textContent = `
79
+ ::view-transition-old(root) {
80
+ animation-delay: ${duration}ms;
81
+ }
82
+
83
+ ::view-transition-new(root) {
84
+ animation: move-in ${duration}ms;
85
+ animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);
86
+ }
87
+
88
+ @keyframes move-in {
89
+ from {
90
+ translate: ${a}% ${b}%;
91
+ }
92
+ to {
93
+ translate: ${x}% ${y}%;
94
+ }
95
+ }
96
+ `;
97
+ document.head.appendChild(styleElement);
98
+ // Clean up after animation
99
+ setTimeout(() => {
100
+ const styleElement = document.getElementById(styleId);
101
+ if (styleElement) {
102
+ styleElement.remove();
103
+ }
104
+ }, duration);
105
+ };
74
106
  export const createBlurCircleAnimation = (config) => {
75
107
  const { x, y, duration, easing, blurAmount, styleId } = config;
76
108
  const existingStyle = document.getElementById(styleId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space-man/react-theme-animation",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "React theme switching with smooth view transition animations and multi-theme support",
5
5
  "keywords": [
6
6
  "react",