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

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
@@ -2,6 +2,14 @@
2
2
 
3
3
  React theme switching with smooth view transition animations, multi-theme support, and synchronized state management.
4
4
 
5
+ ## Demo
6
+
7
+ <video src="https://tonyedgal.github.io/spaceman-react-theme-animation/Demo.mov" controls width="600" loop autoplay muted></video>
8
+
9
+ ## Live Demo
10
+
11
+ [Live Demo Link](https://spaceman-rta-vite.netlify.app/)
12
+
5
13
  ## Features
6
14
 
7
15
  - 🎨 **Smooth Animations**: Beautiful view transition animations for theme switching with customizable origins
@@ -25,14 +33,17 @@ npm install @space-man/react-theme-animation
25
33
 
26
34
  ## Usage Patterns
27
35
 
28
- This library supports two main usage patterns:
36
+ This library supports multiple usage patterns:
29
37
 
30
38
  1. **Hook-Only Usage**: Direct hook usage for custom implementations
31
39
  2. **Provider Pattern**: Centralized state management (recommended)
40
+ 3. **ViteThemeProvider**: Specialized provider for Vite React SPAs
32
41
 
33
42
  📚 **[See the complete Getting Started Guide](./getting-started.md)** for detailed examples, advanced configurations, and best practices.
34
43
 
35
- 🚀 **[View the Code Example](./example/)** to see this package in action with a complete implementation of both hook and provider pattern.
44
+ 🚀 **[View the Next.js Code Example](./example/)** to see this package in action with a complete implementation of both hook and provider pattern.
45
+
46
+ ⚡ **[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.
36
47
 
37
48
  ---
38
49
 
@@ -80,7 +91,7 @@ function App() {
80
91
  </header>
81
92
 
82
93
  <aside>
83
- <ThemeSelector colorThemes={['default', 'blue', 'green', 'purple']} />
94
+ <ThemeSelector colorThemes={['default', 'supabase', 'mono']} />
84
95
  </aside>
85
96
 
86
97
  <main>
@@ -94,11 +105,121 @@ function App() {
94
105
 
95
106
  ---
96
107
 
108
+ ## 3. ViteThemeProvider (Vite React SPAs)
109
+
110
+ Specialized theme provider optimized for Vite React single-page applications. Provides essential theme management without animation features.
111
+
112
+ ### Basic Vite Setup
113
+
114
+ ```tsx
115
+ import { ViteThemeProvider, useViteTheme } from '@space-man/react-theme-animation'
116
+
117
+ function App() {
118
+ return (
119
+ <ViteThemeProvider defaultTheme="system" storageKey="my-app-theme" attribute="class">
120
+ <div className="app">
121
+ <header>
122
+ <ThemeToggle />
123
+ </header>
124
+ <main>
125
+ <ThemeSection />
126
+ </main>
127
+ </div>
128
+ </ViteThemeProvider>
129
+ )
130
+ }
131
+
132
+ function ThemeSection() {
133
+ return (
134
+ <SpacemanThemeProvider>
135
+ <div className="app">
136
+ <header>
137
+ <ThemeSwitcher />
138
+ </header>
139
+
140
+ <aside>
141
+ <ThemeSelector colorThemes={['default', 'supabase', 'mono']} />
142
+ </aside>
143
+
144
+ <main>
145
+ <YourAppContent />
146
+ </main>
147
+ </div>
148
+ </SpacemanThemeProvider>
149
+ )
150
+ }
151
+
152
+ function ThemeToggle() {
153
+ const { theme, setTheme, resolvedTheme } = useViteTheme()
154
+
155
+ return (
156
+ <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
157
+ {resolvedTheme === 'dark' ? '🌞' : '🌙'} {theme}
158
+ </button>
159
+ )
160
+ }
161
+ ```
162
+
163
+ ### ViteThemeProvider Props
164
+
165
+ | Prop | Type | Default | Description |
166
+ | --------------------------- | ------------------------------- | -------------- | ------------------------------------------- |
167
+ | `children` | `ReactNode` | - | React children |
168
+ | `attribute` | `'class' \| 'data-theme'` | `'class'` | How to apply theme to DOM |
169
+ | `defaultTheme` | `'light' \| 'dark' \| 'system'` | `'system'` | Default theme |
170
+ | `enableSystem` | `boolean` | `true` | Enable system theme detection |
171
+ | `disableTransitionOnChange` | `boolean` | `false` | Disable CSS transitions during theme change |
172
+ | `storageKey` | `string` | `'vite-theme'` | localStorage key for persistence |
173
+
174
+ ---
175
+
176
+ ## Defining Application color theme as CSS Variables
177
+
178
+ The library uses CSS custom properties for theming. Define these in your **MAIN** CSS file. _Note_: do not define the other theme variables in a separate CSS file and import it, as the `:root` variables for light mode will override the light mode variables for the other themes. Keep everything in the same file.
179
+
180
+ ```css
181
+ /* Base theme variables */
182
+ :root {
183
+ --background: 0 0% 100%;
184
+ --foreground: 222.2 84% 4.9%;
185
+ }
186
+
187
+ /* Dark theme */
188
+ .dark {
189
+ --background: 222.2 84% 4.9%;
190
+ --foreground: 210 40% 98%;
191
+ }
192
+
193
+ /* Add other theme variants from design system or tweakcn */
194
+
195
+ .theme-supabase {
196
+ --background: oklch(0.9911 0 0);
197
+ --foreground: oklch(0.2046 0 0);
198
+ }
199
+
200
+ .theme-supabase.dark {
201
+ --background: oklch(0.1822 0 0);
202
+ --foreground: oklch(0.9288 0.0126 255.5078);
203
+ }
204
+
205
+ .theme-mono {
206
+ --background: oklch(1 0 0);
207
+ --foreground: oklch(0.1448 0 0);
208
+ }
209
+
210
+ .theme-mono.dark {
211
+ --background: oklch(0.1448 0 0);
212
+ --foreground: oklch(1 0 0);
213
+ }
214
+ ```
215
+
216
+ ---
217
+
97
218
  ## API Reference
98
219
 
99
220
  ### SpacemanThemeProvider
100
221
 
101
- Context provider for centralized theme state management.
222
+ Context provider for centralized theme state management for the ThemeSelector and ThemeSwitcher components. The SpacemanThemeProvider allows you to manage themes and color themes in your application with smooth animations and synchronized state and provide a state reference for the ThemeSelector and ThemeSwitcher components.
102
223
 
103
224
  ```tsx
104
225
  <SpacemanThemeProvider
@@ -111,7 +232,7 @@ Context provider for centralized theme state management.
111
232
  onThemeChange={theme => console.log('Theme:', theme)}
112
233
  onColorThemeChange={colorTheme => console.log('Color:', colorTheme)}
113
234
  >
114
- <App />
235
+ {children}
115
236
  </SpacemanThemeProvider>
116
237
  ```
117
238
 
@@ -316,46 +437,6 @@ interface ThemeSelectorProps {
316
437
 
317
438
  ---
318
439
 
319
- ## CSS Variables
320
-
321
- The library uses CSS custom properties for theming. Define these in your CSS:
322
-
323
- ```css
324
- /* Base theme variables */
325
- :root {
326
- --background: 0 0% 100%;
327
- --foreground: 222.2 84% 4.9%;
328
- }
329
-
330
- /* Dark theme */
331
- .dark {
332
- --background: 222.2 84% 4.9%;
333
- --foreground: 210 40% 98%;
334
- }
335
-
336
- /* Add other theme variants from design system or tweakcn */
337
-
338
- .theme-supabase {
339
- --background: oklch(0.9911 0 0);
340
- --foreground: oklch(0.2046 0 0);
341
- }
342
-
343
- .theme-supabase.dark {
344
- --background: oklch(0.1822 0 0);
345
- --foreground: oklch(0.9288 0.0126 255.5078);
346
- }
347
-
348
- .theme-mono {
349
- --background: oklch(1 0 0);
350
- --foreground: oklch(0.1448 0 0);
351
- }
352
-
353
- .theme-mono.dark {
354
- --background: oklch(0.1448 0 0);
355
- --foreground: oklch(1 0 0);
356
- }
357
- ```
358
-
359
440
  ---
360
441
 
361
442
  ## Browser Support
@@ -11,9 +11,9 @@ const SunIcon = () => ((0, jsx_runtime_1.jsxs)("svg", { width: "16", height: "16
11
11
  const MoonIcon = () => ((0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" }) }));
12
12
  const MonitorIcon = () => ((0, jsx_runtime_1.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)("rect", { width: "20", height: "14", x: "2", y: "3", rx: "2" }), (0, jsx_runtime_1.jsx)("line", { x1: "8", x2: "16", y1: "21", y2: "21" }), (0, jsx_runtime_1.jsx)("line", { x1: "12", x2: "12", y1: "17", y2: "21" })] }));
13
13
  const ThemeOption = ({ icon, value, isActive, isHovered, onClick, onMouseEnter, onMouseLeave, buttonRef, }) => {
14
- return ((0, jsx_runtime_1.jsxs)("button", { ref: isActive ? buttonRef : undefined, className: (0, clsx_1.clsx)('relative flex h-9 w-12 cursor-pointer items-center justify-center transition-all duration-200 ease-in-out', 'text-muted-foreground hover:text-foreground', isActive && 'text-foreground font-medium'), role: "radio", "aria-checked": isActive, "aria-label": `Switch to ${value} theme`, onClick: event => onClick(value, event), onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, style: {
14
+ return ((0, jsx_runtime_1.jsxs)("button", { ref: isActive ? buttonRef : undefined, className: (0, clsx_1.clsx)('relative flex h-9 w-12 cursor-pointer items-center justify-center', 'text-muted-foreground hover:text-foreground', isActive && 'text-foreground font-medium'), role: "radio", "aria-checked": isActive, "aria-label": `Switch to ${value} theme`, onClick: event => onClick(value, event), onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, style: {
15
15
  borderRadius: 'var(--radius)',
16
- }, children: [isHovered && ((0, jsx_runtime_1.jsx)(react_2.motion.div, { layoutId: "theme-hover-bg", className: "absolute inset-0 bg-muted backdrop-blur-sm", style: {
16
+ }, children: [isHovered && ((0, jsx_runtime_1.jsx)(react_2.motion.div, { layoutId: "theme-hover", className: "absolute inset-0 bg-muted", style: {
17
17
  borderRadius: 'var(--radius)',
18
18
  }, transition: {
19
19
  type: 'spring',
@@ -21,18 +21,8 @@ const ThemeOption = ({ icon, value, isActive, isHovered, onClick, onMouseEnter,
21
21
  stiffness: 100,
22
22
  damping: 10,
23
23
  duration: 0.3,
24
- } })), (0, jsx_runtime_1.jsx)(react_2.motion.div, { className: "relative z-10 flex items-center justify-center", animate: {
25
- scale: isActive ? 1.1 : 1,
26
- }, transition: {
27
- type: 'spring',
28
- bounce: 0.2,
29
- duration: 0.4,
30
- }, children: (0, jsx_runtime_1.jsx)("div", { className: "[&_svg]:size-4", children: icon }) }), isActive && ((0, jsx_runtime_1.jsx)(react_2.motion.div, { layoutId: "theme-active-indicator", className: "absolute bottom-0 left-1/2 h-0.5 w-6 -translate-x-1/2 rounded-full", style: {
24
+ } })), (0, jsx_runtime_1.jsx)("div", { className: "relative z-10 flex items-center justify-center", children: (0, jsx_runtime_1.jsx)("div", { className: "[&_svg]:size-4", children: icon }) }), isActive && ((0, jsx_runtime_1.jsx)("div", { className: "absolute bottom-0 left-1/2 h-0.5 w-6 -translate-x-1/2 rounded-full", style: {
31
25
  backgroundColor: 'hsl(var(--primary))',
32
- }, transition: {
33
- type: 'spring',
34
- bounce: 0.3,
35
- duration: 0.6,
36
26
  } }))] }));
37
27
  };
38
28
  const THEME_OPTIONS = [
@@ -50,15 +40,11 @@ const THEME_OPTIONS = [
50
40
  },
51
41
  ];
52
42
  const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentTheme, onThemeChange, animationType, duration, className, }) => {
53
- // Try to use SpacemanTheme context if available (controlled mode)
54
43
  let contextTheme = null;
55
44
  try {
56
45
  contextTheme = (0, SpacemanThemeProvider_1.useSpacemanTheme)();
57
46
  }
58
- catch {
59
- // Context not available, use standalone mode
60
- }
61
- // Use context if available, otherwise fall back to standalone hook
47
+ catch { }
62
48
  const standaloneHook = (0, use_theme_animation_1.useThemeAnimation)({
63
49
  animationType,
64
50
  duration,
@@ -71,30 +57,25 @@ const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentTheme, onT
71
57
  ? { ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme }
72
58
  : standaloneHook;
73
59
  const [isMounted, setIsMounted] = (0, react_1.useState)(false);
60
+ const [hoveredTheme, setHoveredTheme] = (0, react_1.useState)(null);
74
61
  (0, react_1.useEffect)(() => {
75
62
  setIsMounted(true);
76
63
  }, []);
77
64
  const handleThemeChange = async (newTheme, event) => {
78
65
  if (isControlled && contextTheme.switchThemeFromElement && event) {
79
- // Use controlled mode with animation from clicked element
80
66
  await contextTheme.switchThemeFromElement(newTheme, event.currentTarget);
81
- // Also call the callback if provided
82
67
  if (onThemeChange) {
83
68
  onThemeChange(newTheme);
84
69
  }
85
70
  }
86
71
  else {
87
- // Use standalone mode
88
72
  await switchTheme(newTheme);
89
73
  }
90
74
  };
91
- if (!isMounted) {
92
- return (0, jsx_runtime_1.jsx)("div", { className: "flex h-9 w-fit" });
93
- }
94
75
  const filteredOptions = THEME_OPTIONS.filter(option => themes.includes(option.value));
95
- return ((0, jsx_runtime_1.jsx)(react_2.motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.3 }, className: (0, clsx_1.clsx)('inline-flex items-center overflow-hidden bg-background shadow-sm border border-border', 'transition-all duration-200 ease-in-out', className), style: {
76
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, clsx_1.clsx)('inline-flex h-9 items-center overflow-hidden bg-background shadow-sm border border-border', isMounted ? 'opacity-100' : 'opacity-0', className), style: {
96
77
  borderRadius: 'var(--radius)',
97
78
  backgroundColor: 'hsl(var(--background))',
98
- }, role: "radiogroup", children: filteredOptions.map(option => ((0, jsx_runtime_1.jsx)(ThemeOption, { icon: option.icon, value: option.value, isActive: theme === option.value, isHovered: false, onClick: handleThemeChange, onMouseEnter: () => { }, onMouseLeave: () => { }, buttonRef: theme === option.value ? ref : undefined }, option.value))) }, String(isMounted)));
79
+ }, role: "radiogroup", onMouseLeave: () => setHoveredTheme(null), children: filteredOptions.map(option => ((0, jsx_runtime_1.jsx)(ThemeOption, { icon: option.icon, value: option.value, isActive: theme === option.value, isHovered: hoveredTheme === option.value, onClick: handleThemeChange, onMouseEnter: () => setHoveredTheme(option.value), onMouseLeave: () => { }, buttonRef: theme === option.value ? ref : undefined }, option.value))) }));
99
80
  };
100
81
  exports.ThemeSwitcher = ThemeSwitcher;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ type Theme = 'light' | 'dark' | 'system';
3
+ export interface ViteThemeContextType {
4
+ theme: Theme;
5
+ resolvedTheme: 'light' | 'dark';
6
+ setTheme: (theme: Theme) => void;
7
+ systemTheme: 'light' | 'dark';
8
+ }
9
+ interface ViteThemeProviderProps {
10
+ children: React.ReactNode;
11
+ attribute?: 'class' | 'data-theme';
12
+ defaultTheme?: Theme;
13
+ enableSystem?: boolean;
14
+ disableTransitionOnChange?: boolean;
15
+ storageKey?: string;
16
+ }
17
+ export declare const ViteThemeProvider: React.FC<ViteThemeProviderProps>;
18
+ export declare const useViteTheme: () => ViteThemeContextType;
19
+ export {};
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useViteTheme = exports.ViteThemeProvider = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ 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
+ }
22
+ });
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);
44
+ setTimeout(() => {
45
+ document.head.removeChild(css);
46
+ }, 1);
47
+ }
48
+ if (attribute === 'class') {
49
+ root.classList.remove('light', 'dark');
50
+ root.classList.add(resolvedTheme);
51
+ }
52
+ else {
53
+ root.setAttribute(attribute, resolvedTheme);
54
+ }
55
+ try {
56
+ localStorage.setItem(storageKey, theme);
57
+ }
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,
73
+ systemTheme,
74
+ };
75
+ return (0, jsx_runtime_1.jsx)(ViteThemeContext.Provider, { value: value, children: children });
76
+ };
77
+ exports.ViteThemeProvider = ViteThemeProvider;
78
+ const useViteTheme = () => {
79
+ const context = (0, react_1.useContext)(ViteThemeContext);
80
+ if (context === undefined) {
81
+ throw new Error('useViteTheme must be used within a ViteThemeProvider');
82
+ }
83
+ return context;
84
+ };
85
+ exports.useViteTheme = useViteTheme;
@@ -2,6 +2,8 @@ export { useThemeAnimation } from './hooks/use-theme-animation';
2
2
  export { ThemeSwitcher } from './components/ThemeSwitcher';
3
3
  export { ThemeSelector } from './components/ThemeSelector';
4
4
  export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
5
+ export { ViteThemeProvider, useViteTheme } from './components/ViteThemeProvider';
5
6
  export type { UseThemeAnimationProps, UseThemeAnimationReturn, ThemeSwitcherProps, ThemeSelectorProps, Theme, ColorTheme, } from './types';
7
+ export type { ViteThemeContextType } from './components/ViteThemeProvider';
6
8
  export { ThemeAnimationType } from './types';
7
9
  export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.prefersReducedMotion = exports.supportsViewTransitions = exports.resolveTheme = exports.getSystemTheme = exports.injectBaseStyles = exports.ThemeAnimationType = exports.useSpacemanTheme = exports.SpacemanThemeProvider = exports.ThemeSelector = exports.ThemeSwitcher = exports.useThemeAnimation = void 0;
3
+ exports.prefersReducedMotion = exports.supportsViewTransitions = exports.resolveTheme = exports.getSystemTheme = exports.injectBaseStyles = exports.ThemeAnimationType = exports.useViteTheme = exports.ViteThemeProvider = exports.useSpacemanTheme = exports.SpacemanThemeProvider = exports.ThemeSelector = exports.ThemeSwitcher = exports.useThemeAnimation = void 0;
4
4
  var use_theme_animation_1 = require("./hooks/use-theme-animation");
5
5
  Object.defineProperty(exports, "useThemeAnimation", { enumerable: true, get: function () { return use_theme_animation_1.useThemeAnimation; } });
6
6
  var ThemeSwitcher_1 = require("./components/ThemeSwitcher");
@@ -10,6 +10,9 @@ Object.defineProperty(exports, "ThemeSelector", { enumerable: true, get: functio
10
10
  var SpacemanThemeProvider_1 = require("./components/SpacemanThemeProvider");
11
11
  Object.defineProperty(exports, "SpacemanThemeProvider", { enumerable: true, get: function () { return SpacemanThemeProvider_1.SpacemanThemeProvider; } });
12
12
  Object.defineProperty(exports, "useSpacemanTheme", { enumerable: true, get: function () { return SpacemanThemeProvider_1.useSpacemanTheme; } });
13
+ var ViteThemeProvider_1 = require("./components/ViteThemeProvider");
14
+ Object.defineProperty(exports, "ViteThemeProvider", { enumerable: true, get: function () { return ViteThemeProvider_1.ViteThemeProvider; } });
15
+ Object.defineProperty(exports, "useViteTheme", { enumerable: true, get: function () { return ViteThemeProvider_1.useViteTheme; } });
13
16
  var types_1 = require("./types");
14
17
  Object.defineProperty(exports, "ThemeAnimationType", { enumerable: true, get: function () { return types_1.ThemeAnimationType; } });
15
18
  var animations_1 = require("./utils/animations");
@@ -8,9 +8,9 @@ const SunIcon = () => (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 2
8
8
  const MoonIcon = () => (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" }) }));
9
9
  const MonitorIcon = () => (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { width: "20", height: "14", x: "2", y: "3", rx: "2" }), _jsx("line", { x1: "8", x2: "16", y1: "21", y2: "21" }), _jsx("line", { x1: "12", x2: "12", y1: "17", y2: "21" })] }));
10
10
  const ThemeOption = ({ icon, value, isActive, isHovered, onClick, onMouseEnter, onMouseLeave, buttonRef, }) => {
11
- return (_jsxs("button", { ref: isActive ? buttonRef : undefined, className: clsx('relative flex h-9 w-12 cursor-pointer items-center justify-center transition-all duration-200 ease-in-out', 'text-muted-foreground hover:text-foreground', isActive && 'text-foreground font-medium'), role: "radio", "aria-checked": isActive, "aria-label": `Switch to ${value} theme`, onClick: event => onClick(value, event), onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, style: {
11
+ return (_jsxs("button", { ref: isActive ? buttonRef : undefined, className: clsx('relative flex h-9 w-12 cursor-pointer items-center justify-center', 'text-muted-foreground hover:text-foreground', isActive && 'text-foreground font-medium'), role: "radio", "aria-checked": isActive, "aria-label": `Switch to ${value} theme`, onClick: event => onClick(value, event), onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, style: {
12
12
  borderRadius: 'var(--radius)',
13
- }, children: [isHovered && (_jsx(motion.div, { layoutId: "theme-hover-bg", className: "absolute inset-0 bg-muted backdrop-blur-sm", style: {
13
+ }, children: [isHovered && (_jsx(motion.div, { layoutId: "theme-hover", className: "absolute inset-0 bg-muted", style: {
14
14
  borderRadius: 'var(--radius)',
15
15
  }, transition: {
16
16
  type: 'spring',
@@ -18,18 +18,8 @@ const ThemeOption = ({ icon, value, isActive, isHovered, onClick, onMouseEnter,
18
18
  stiffness: 100,
19
19
  damping: 10,
20
20
  duration: 0.3,
21
- } })), _jsx(motion.div, { className: "relative z-10 flex items-center justify-center", animate: {
22
- scale: isActive ? 1.1 : 1,
23
- }, transition: {
24
- type: 'spring',
25
- bounce: 0.2,
26
- duration: 0.4,
27
- }, children: _jsx("div", { className: "[&_svg]:size-4", children: icon }) }), isActive && (_jsx(motion.div, { layoutId: "theme-active-indicator", className: "absolute bottom-0 left-1/2 h-0.5 w-6 -translate-x-1/2 rounded-full", style: {
21
+ } })), _jsx("div", { className: "relative z-10 flex items-center justify-center", children: _jsx("div", { className: "[&_svg]:size-4", children: icon }) }), isActive && (_jsx("div", { className: "absolute bottom-0 left-1/2 h-0.5 w-6 -translate-x-1/2 rounded-full", style: {
28
22
  backgroundColor: 'hsl(var(--primary))',
29
- }, transition: {
30
- type: 'spring',
31
- bounce: 0.3,
32
- duration: 0.6,
33
23
  } }))] }));
34
24
  };
35
25
  const THEME_OPTIONS = [
@@ -47,15 +37,11 @@ const THEME_OPTIONS = [
47
37
  },
48
38
  ];
49
39
  export const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentTheme, onThemeChange, animationType, duration, className, }) => {
50
- // Try to use SpacemanTheme context if available (controlled mode)
51
40
  let contextTheme = null;
52
41
  try {
53
42
  contextTheme = useSpacemanTheme();
54
43
  }
55
- catch {
56
- // Context not available, use standalone mode
57
- }
58
- // Use context if available, otherwise fall back to standalone hook
44
+ catch { }
59
45
  const standaloneHook = useThemeAnimation({
60
46
  animationType,
61
47
  duration,
@@ -68,29 +54,24 @@ export const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentThe
68
54
  ? { ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme }
69
55
  : standaloneHook;
70
56
  const [isMounted, setIsMounted] = useState(false);
57
+ const [hoveredTheme, setHoveredTheme] = useState(null);
71
58
  useEffect(() => {
72
59
  setIsMounted(true);
73
60
  }, []);
74
61
  const handleThemeChange = async (newTheme, event) => {
75
62
  if (isControlled && contextTheme.switchThemeFromElement && event) {
76
- // Use controlled mode with animation from clicked element
77
63
  await contextTheme.switchThemeFromElement(newTheme, event.currentTarget);
78
- // Also call the callback if provided
79
64
  if (onThemeChange) {
80
65
  onThemeChange(newTheme);
81
66
  }
82
67
  }
83
68
  else {
84
- // Use standalone mode
85
69
  await switchTheme(newTheme);
86
70
  }
87
71
  };
88
- if (!isMounted) {
89
- return _jsx("div", { className: "flex h-9 w-fit" });
90
- }
91
72
  const filteredOptions = THEME_OPTIONS.filter(option => themes.includes(option.value));
92
- return (_jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.3 }, className: clsx('inline-flex items-center overflow-hidden bg-background shadow-sm border border-border', 'transition-all duration-200 ease-in-out', className), style: {
73
+ return (_jsx("div", { className: clsx('inline-flex h-9 items-center overflow-hidden bg-background shadow-sm border border-border', isMounted ? 'opacity-100' : 'opacity-0', className), style: {
93
74
  borderRadius: 'var(--radius)',
94
75
  backgroundColor: 'hsl(var(--background))',
95
- }, role: "radiogroup", children: filteredOptions.map(option => (_jsx(ThemeOption, { icon: option.icon, value: option.value, isActive: theme === option.value, isHovered: false, onClick: handleThemeChange, onMouseEnter: () => { }, onMouseLeave: () => { }, buttonRef: theme === option.value ? ref : undefined }, option.value))) }, String(isMounted)));
76
+ }, role: "radiogroup", onMouseLeave: () => setHoveredTheme(null), children: filteredOptions.map(option => (_jsx(ThemeOption, { icon: option.icon, value: option.value, isActive: theme === option.value, isHovered: hoveredTheme === option.value, onClick: handleThemeChange, onMouseEnter: () => setHoveredTheme(option.value), onMouseLeave: () => { }, buttonRef: theme === option.value ? ref : undefined }, option.value))) }));
96
77
  };
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ type Theme = 'light' | 'dark' | 'system';
3
+ export interface ViteThemeContextType {
4
+ theme: Theme;
5
+ resolvedTheme: 'light' | 'dark';
6
+ setTheme: (theme: Theme) => void;
7
+ systemTheme: 'light' | 'dark';
8
+ }
9
+ interface ViteThemeProviderProps {
10
+ children: React.ReactNode;
11
+ attribute?: 'class' | 'data-theme';
12
+ defaultTheme?: Theme;
13
+ enableSystem?: boolean;
14
+ disableTransitionOnChange?: boolean;
15
+ storageKey?: string;
16
+ }
17
+ export declare const ViteThemeProvider: React.FC<ViteThemeProviderProps>;
18
+ export declare const useViteTheme: () => ViteThemeContextType;
19
+ export {};
@@ -0,0 +1,80 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useState, useCallback } from 'react';
3
+ 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
+ }
19
+ });
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);
41
+ setTimeout(() => {
42
+ document.head.removeChild(css);
43
+ }, 1);
44
+ }
45
+ if (attribute === 'class') {
46
+ root.classList.remove('light', 'dark');
47
+ root.classList.add(resolvedTheme);
48
+ }
49
+ else {
50
+ root.setAttribute(attribute, resolvedTheme);
51
+ }
52
+ try {
53
+ localStorage.setItem(storageKey, theme);
54
+ }
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,
70
+ systemTheme,
71
+ };
72
+ return _jsx(ViteThemeContext.Provider, { value: value, children: children });
73
+ };
74
+ export const useViteTheme = () => {
75
+ const context = useContext(ViteThemeContext);
76
+ if (context === undefined) {
77
+ throw new Error('useViteTheme must be used within a ViteThemeProvider');
78
+ }
79
+ return context;
80
+ };
@@ -2,6 +2,8 @@ export { useThemeAnimation } from './hooks/use-theme-animation';
2
2
  export { ThemeSwitcher } from './components/ThemeSwitcher';
3
3
  export { ThemeSelector } from './components/ThemeSelector';
4
4
  export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
5
+ export { ViteThemeProvider, useViteTheme } from './components/ViteThemeProvider';
5
6
  export type { UseThemeAnimationProps, UseThemeAnimationReturn, ThemeSwitcherProps, ThemeSelectorProps, Theme, ColorTheme, } from './types';
7
+ export type { ViteThemeContextType } from './components/ViteThemeProvider';
6
8
  export { ThemeAnimationType } from './types';
7
9
  export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
package/dist/esm/index.js CHANGED
@@ -2,5 +2,6 @@ export { useThemeAnimation } from './hooks/use-theme-animation';
2
2
  export { ThemeSwitcher } from './components/ThemeSwitcher';
3
3
  export { ThemeSelector } from './components/ThemeSelector';
4
4
  export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
5
+ export { ViteThemeProvider, useViteTheme } from './components/ViteThemeProvider';
5
6
  export { ThemeAnimationType } from './types';
6
7
  export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@space-man/react-theme-animation",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "React theme switching with smooth view transition animations and multi-theme support",
5
5
  "keywords": [
6
6
  "react",