@space-man/react-theme-animation 2.1.0 → 2.3.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.
Files changed (68) hide show
  1. package/README.md +72 -142
  2. package/dist/core/index.cjs +74 -0
  3. package/dist/core/index.cjs.map +1 -0
  4. package/dist/{esm/types/index.d.ts → core/index.d.cts} +26 -8
  5. package/dist/{cjs/types → core}/index.d.ts +26 -8
  6. package/dist/core/index.js +66 -0
  7. package/dist/core/index.js.map +1 -0
  8. package/dist/index.cjs +1756 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.d.cts +4 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.js +1706 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/react/index.cjs +1742 -0
  15. package/dist/react/index.cjs.map +1 -0
  16. package/dist/react/index.d.cts +224 -0
  17. package/dist/react/index.d.ts +224 -0
  18. package/dist/react/index.js +1697 -0
  19. package/dist/react/index.js.map +1 -0
  20. package/dist/tanstack/index.cjs +34 -0
  21. package/dist/tanstack/index.cjs.map +1 -0
  22. package/dist/tanstack/index.d.cts +20 -0
  23. package/dist/tanstack/index.d.ts +20 -0
  24. package/dist/tanstack/index.js +27 -0
  25. package/dist/tanstack/index.js.map +1 -0
  26. package/package.json +66 -37
  27. package/dist/cjs/components/NextThemeProvider.d.ts +0 -81
  28. package/dist/cjs/components/NextThemeProvider.js +0 -223
  29. package/dist/cjs/components/SpacemanThemeProvider.d.ts +0 -57
  30. package/dist/cjs/components/SpacemanThemeProvider.js +0 -113
  31. package/dist/cjs/components/TanStackThemeProvider.d.ts +0 -68
  32. package/dist/cjs/components/TanStackThemeProvider.js +0 -183
  33. package/dist/cjs/components/ThemeSelector.d.ts +0 -3
  34. package/dist/cjs/components/ThemeSelector.js +0 -39
  35. package/dist/cjs/components/ThemeSwitcher.d.ts +0 -3
  36. package/dist/cjs/components/ThemeSwitcher.js +0 -81
  37. package/dist/cjs/components/ViteThemeProvider.d.ts +0 -61
  38. package/dist/cjs/components/ViteThemeProvider.js +0 -140
  39. package/dist/cjs/components/ui/select.d.ts +0 -15
  40. package/dist/cjs/components/ui/select.js +0 -83
  41. package/dist/cjs/hooks/use-theme-animation.d.ts +0 -2
  42. package/dist/cjs/hooks/use-theme-animation.js +0 -234
  43. package/dist/cjs/index.d.ts +0 -10
  44. package/dist/cjs/index.js +0 -30
  45. package/dist/cjs/types/index.js +0 -9
  46. package/dist/cjs/utils/animations.d.ts +0 -21
  47. package/dist/cjs/utils/animations.js +0 -189
  48. package/dist/esm/components/NextThemeProvider.d.ts +0 -81
  49. package/dist/esm/components/NextThemeProvider.js +0 -185
  50. package/dist/esm/components/SpacemanThemeProvider.d.ts +0 -57
  51. package/dist/esm/components/SpacemanThemeProvider.js +0 -108
  52. package/dist/esm/components/TanStackThemeProvider.d.ts +0 -68
  53. package/dist/esm/components/TanStackThemeProvider.js +0 -178
  54. package/dist/esm/components/ThemeSelector.d.ts +0 -3
  55. package/dist/esm/components/ThemeSelector.js +0 -35
  56. package/dist/esm/components/ThemeSwitcher.d.ts +0 -3
  57. package/dist/esm/components/ThemeSwitcher.js +0 -77
  58. package/dist/esm/components/ViteThemeProvider.d.ts +0 -61
  59. package/dist/esm/components/ViteThemeProvider.js +0 -135
  60. package/dist/esm/components/ui/select.d.ts +0 -15
  61. package/dist/esm/components/ui/select.js +0 -39
  62. package/dist/esm/hooks/use-theme-animation.d.ts +0 -2
  63. package/dist/esm/hooks/use-theme-animation.js +0 -230
  64. package/dist/esm/index.d.ts +0 -10
  65. package/dist/esm/index.js +0 -10
  66. package/dist/esm/types/index.js +0 -6
  67. package/dist/esm/utils/animations.d.ts +0 -21
  68. package/dist/esm/utils/animations.js +0 -177
package/README.md CHANGED
@@ -1,32 +1,18 @@
1
1
  # @space-man/react-theme-animation
2
2
 
3
- https://github.com/user-attachments/assets/2819cf70-c474-478e-b821-6b26457c8d4a
4
-
5
3
  React theme switching with smooth view transition animations, multi-theme support, and synchronized state management.
6
4
 
7
- ## What's New in v2.1
8
-
9
- - **animationOff Parameter**: Instantly switch themes without animation via optional parameter on all toggle functions
10
- - **Universal SSR Support**: Pre-hydration script prevents flash in Next.js, Remix, and SSR frameworks
11
- - **Three Framework-Specific Providers**: NextThemeProvider (SSR/Next.js), TanStackThemeProvider (TanStack Start), ViteThemeProvider (Vite SPAs)
12
- - **Consistent Hook API**: All providers expose identical hook interface with 15+ methods
13
- - **Enhanced Theme Control**: Direct functions for toggleLightTheme(), toggleDarkTheme(), createColorThemeToggle()
5
+ ## Spaceman React Theme Animation v2 release
14
6
 
15
- ## Live Demo
7
+ Version 2 moves the library into a workspace-friendly monorepo, keeps the published package name stable, and standardizes the public API around flat root imports.
16
8
 
17
- [View Live Demo](https://spaceman-rta-vite.netlify.app/)
9
+ Highlights in this release:
18
10
 
19
- ## Features
20
-
21
- - Smooth view transition animations with customizable origins
22
- - Multi-theme support (light, dark, system)
23
- - Color theme variants (brand colors, custom themes)
24
- - Three framework-optimized providers
25
- - Powerful useThemeAnimation hook for custom implementations
26
- - Ready-to-use ThemeSwitcher and ThemeSelector components
27
- - State synchronization across components
28
- - Full TypeScript support
29
- - Performance optimized with reduced motion support
11
+ - root-first public API via `@space-man/react-theme-animation`
12
+ - framework-specific providers for Next.js, TanStack Start, and Vite
13
+ - TanStack SSR helpers exposed from the root package
14
+ - workspace examples for Next.js, TanStack Start, and TanStack Start SSR
15
+ - build and packaging updates for ESM, CJS, and declaration output
30
16
 
31
17
  ## Installation
32
18
 
@@ -34,22 +20,66 @@ React theme switching with smooth view transition animations, multi-theme suppor
34
20
  npm install @space-man/react-theme-animation
35
21
  ```
36
22
 
23
+ ```bash
24
+ pnpm add @space-man/react-theme-animation
25
+ ```
26
+
27
+ ```bash
28
+ yarn add @space-man/react-theme-animation
29
+ ```
30
+
31
+ ## Imports
32
+
33
+ Import everything from the package root:
34
+
35
+ ```tsx
36
+ import {
37
+ NextThemeProvider,
38
+ TanStackThemeProvider,
39
+ ViteThemeProvider,
40
+ ThemeSwitcher,
41
+ ThemeSelector,
42
+ useThemeAnimation,
43
+ } from '@space-man/react-theme-animation'
44
+ ```
45
+
46
+ Compatibility subpath exports still exist for migration safety, but the package root is the documented public API.
47
+
48
+ For Next.js App Router, import providers/hooks from the package root inside client components. A
49
+ typical setup is a local `app/providers.tsx` file with `'use client'`, rendered by the server
50
+ `app/layout.tsx`.
51
+
52
+ ## Tailwind Setup For Prebuilt Components
53
+
54
+ `ThemeSwitcher`, `ThemeSelector`, and the bundled select primitives use Tailwind utility classes. The package does not ship a compiled stylesheet for those components, so the consuming app must include the installed package in Tailwind's source scan.
55
+
56
+ For Tailwind v4, add an `@source` directive next to your Tailwind import:
57
+
58
+ ```css
59
+ @import 'tailwindcss';
60
+ @source '../node_modules/@space-man/react-theme-animation/dist';
61
+ ```
62
+
63
+ Adjust the relative path to match your app structure.
64
+
37
65
  ## Provider Selection
38
66
 
39
67
  Choose the right provider for your framework:
40
68
 
41
- | Provider | Best For | Key Features |
42
- | --------------------- | ------------------------------ | ------------------------------------------------ |
43
- | NextThemeProvider | Next.js, Remix, SSR frameworks | Pre-hydration script, CSP support, animations |
44
- | TanStackThemeProvider | TanStack Start apps | Isomorphic rendering, useHydrated() integration |
45
- | ViteThemeProvider | Vite React SPAs | Lightweight, transition control, no SSR overhead |
69
+ | Provider | Best For | Key Features |
70
+ | --------------------- | ------------------------------ | ------------------------------------------------- |
71
+ | SpacemanThemeProvider | General React apps | Universal provider with synchronized theme state |
72
+ | NextThemeProvider | Next.js, Remix, SSR frameworks | Pre-hydration script, CSP support, animations |
73
+ | TanStackThemeProvider | TanStack Start apps | Cookie SSR, pre-hydration script, system CSS mode |
74
+ | ViteThemeProvider | Vite React SPAs | Lightweight, transition control, no SSR overhead |
46
75
 
47
76
  ## Quick Start
48
77
 
49
- Basic pattern that works across all providers:
50
-
51
78
  ```tsx
52
- import { SpacemanThemeProvider, useSpacemanTheme } from '@space-man/react-theme-animation'
79
+ import {
80
+ SpacemanThemeProvider,
81
+ useSpacemanTheme,
82
+ } from '@space-man/react-theme-animation'
53
83
 
54
84
  function App() {
55
85
  return (
@@ -70,19 +100,17 @@ function ThemeToggle() {
70
100
  }
71
101
  ```
72
102
 
73
- **Choose your framework setup guide:**
103
+ ## Documentation
74
104
 
75
- - **[Next.js / SSR Setup Guide](./docs/nextjs-setup.md)** - Complete guide with App Router, Pages Router, CSP support
76
- - **[TanStack Start Setup Guide](./docs/tanstack-start-setup.md)** - Isomorphic rendering, hydration-safe patterns
77
- - **[Vite React SPA Setup Guide](./docs/vite-setup.md)** - Client-side setup, flash prevention, routing
105
+ Repository documentation, framework guides, migration notes, and examples are linked from the GitHub docs hub:
78
106
 
79
- **[View Complete Examples](./example/)** - Next.js implementation with both hook and provider patterns
107
+ - https://github.com/tonyedgal/spaceman-react-theme-animation#documentation
80
108
 
81
109
  ## API Reference
82
110
 
83
111
  ### Shared Hook Return (All Providers)
84
112
 
85
- All providers expose the same hook interface:
113
+ All providers expose the same core hook interface:
86
114
 
87
115
  | Property | Type | Description |
88
116
  | ---------------------- | ------------------------------------------------------- | ---------------------------------- |
@@ -103,115 +131,17 @@ All providers expose the same hook interface:
103
131
  | isColorThemeActive | (colorTheme: string) => boolean | Check if color theme active |
104
132
  | switchThemeFromElement | (theme: Theme, element: HTMLElement) => Promise<void> | Switch with animation from element |
105
133
 
106
- ### Provider Props
107
-
108
- | Prop | Spaceman | NextTheme | TanStack | Vite | Type | Default |
109
- | ------------------------- | -------- | --------- | -------- | ---- | ------------------ | --------------------------- |
110
- | defaultTheme | ✓ | ✓ | ✓ | ✓ | Theme | 'system' |
111
- | defaultColorTheme | ✓ | ✓ | ✓ | ✓ | ColorTheme | 'default' |
112
- | themes | ✓ | ✓ | ✓ | ✓ | Theme[] | ['light', 'dark', 'system'] |
113
- | colorThemes | ✓ | ✓ | ✓ | ✓ | ColorTheme[] | ['default'] |
114
- | animationType | ✓ | ✓ | ✓ | ✓ | ThemeAnimationType | CIRCLE |
115
- | duration | ✓ | ✓ | ✓ | ✓ | number | 500 |
116
- | storageKey | ✓ | ✓ | ✓ | ✓ | string | varies |
117
- | colorStorageKey | ✓ | ✓ | ✓ | ✓ | string | varies |
118
- | nonce | ✗ | ✓ | ✗ | ✗ | string | - |
119
- | disablePreHydrationScript | ✗ | ✓ | ✗ | ✗ | boolean | false |
120
- | disableTransitionOnChange | ✗ | ✗ | ✗ | ✓ | boolean | false |
121
- | onThemeChange | ✓ | ✓ | ✓ | ✓ | function | - |
122
- | onColorThemeChange | ✓ | ✓ | ✓ | ✓ | function | - |
123
-
124
- ### Components
125
-
126
- **ThemeSwitcher**: Pre-built theme toggle buttons with animations
127
- **ThemeSelector**: Dropdown selector for color themes
128
-
129
- See framework-specific guides for component usage examples.
130
-
131
- ---
132
-
133
- <details>
134
- <summary>CSS Variables Setup</summary>
135
-
136
- Define theme variables in your global CSS file:
137
-
138
- ```css
139
- :root {
140
- --background: 0 0% 100%;
141
- --foreground: 222.2 84% 4.9%;
142
- --primary: 221.2 83.2% 53.3%;
143
- }
144
-
145
- .dark {
146
- --background: 222.2 84% 4.9%;
147
- --foreground: 210 40% 98%;
148
- --primary: 217.2 91.2% 59.8%;
149
- }
150
-
151
- /* Color theme variants */
152
- .theme-blue {
153
- --primary: 221.2 83.2% 53.3%;
154
- }
155
-
156
- .theme-blue.dark {
157
- --primary: 217.2 91.2% 59.8%;
158
- }
159
-
160
- .theme-green {
161
- --primary: 142.1 76.2% 36.3%;
162
- }
163
-
164
- .theme-green.dark {
165
- --primary: 142.1 70.6% 45.3%;
166
- }
167
- ```
168
-
169
- </details>
170
-
171
- <details>
172
- <summary>Browser Support</summary>
173
-
174
- - View Transitions API: Chrome 111+, Edge 111+
175
- - Fallback: All modern browsers with CSS transitions
176
- - Reduced Motion: Respects prefers-reduced-motion
177
- - Framework Support: React 16.8+ (hooks required)
178
-
179
- </details>
180
-
181
- <details>
182
- <summary>Advanced Configuration</summary>
183
-
184
- ### Animation Control
185
-
186
- ```tsx
187
- const { switchTheme, toggleTheme } = useSpacemanTheme()
188
-
189
- // With animation (default)
190
- await switchTheme('dark')
191
-
192
- // Without animation
193
- await switchTheme('dark', true)
194
- await toggleTheme(true)
195
- ```
196
-
197
- ### Custom Hook Usage
198
-
199
- ```tsx
200
- import { useThemeAnimation } from '@space-man/react-theme-animation'
201
-
202
- const { theme, toggleTheme, ref } = useThemeAnimation({
203
- animationType: ThemeAnimationType.BLUR_CIRCLE,
204
- duration: 750,
205
- colorThemes: ['default', 'blue', 'green'],
206
- onThemeChange: theme => console.log('Theme:', theme),
207
- })
208
- ```
134
+ ### Root Exports
209
135
 
210
- </details>
136
+ The root package exports:
211
137
 
212
- ## Contributing
138
+ - providers and framework hooks
139
+ - prebuilt `ThemeSwitcher` and `ThemeSelector`
140
+ - `useThemeAnimation`
141
+ - shared types and animation utilities
142
+ - TanStack server helpers such as `buildServerThemeData`, `STORAGE_KEY`, and `COLOR_STORAGE_KEY`
213
143
 
214
- Fork the repository, make your changes, and submit a merge request for review.
144
+ See the repository docs hub for provider-specific setup guides and examples.
215
145
 
216
146
  ## License
217
147
 
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ // src/core/types/index.ts
4
+ var ThemeAnimationType = /* @__PURE__ */ ((ThemeAnimationType2) => {
5
+ ThemeAnimationType2["CIRCLE"] = "circle";
6
+ ThemeAnimationType2["BLUR_CIRCLE"] = "blur-circle";
7
+ ThemeAnimationType2["SLIDE"] = "slide";
8
+ return ThemeAnimationType2;
9
+ })(ThemeAnimationType || {});
10
+
11
+ // src/core/utils/animations.ts
12
+ var isBrowser = typeof window !== "undefined";
13
+ var injectBaseStyles = () => {
14
+ if (isBrowser) {
15
+ const styleId = "spaceman-theme-base-style";
16
+ if (!document.getElementById(styleId)) {
17
+ const style = document.createElement("style");
18
+ style.id = styleId;
19
+ const isHighResolution = window.innerWidth >= 3e3 || window.innerHeight >= 2e3;
20
+ style.textContent = `
21
+ ::view-transition-old(root),
22
+ ::view-transition-new(root) {
23
+ animation: none;
24
+ mix-blend-mode: normal;
25
+ ${isHighResolution ? "transform: translateZ(0);" : ""}
26
+ }
27
+
28
+ ${isHighResolution ? `
29
+ ::view-transition-group(root),
30
+ ::view-transition-image-pair(root),
31
+ ::view-transition-old(root),
32
+ ::view-transition-new(root) {
33
+ backface-visibility: hidden;
34
+ perspective: 1000px;
35
+ transform: translate3d(0, 0, 0);
36
+ }
37
+ ` : ""}
38
+ `;
39
+ document.head.appendChild(style);
40
+ }
41
+ }
42
+ };
43
+ var getSystemTheme = () => {
44
+ if (!isBrowser) return "light";
45
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
46
+ };
47
+ var resolveTheme = (theme) => {
48
+ if (theme === "system") {
49
+ return getSystemTheme();
50
+ }
51
+ return theme === "dark" ? "dark" : "light";
52
+ };
53
+ var resolveThemeForServer = (theme) => {
54
+ if (theme === "system") {
55
+ return "system";
56
+ }
57
+ return theme === "dark" ? "dark" : "light";
58
+ };
59
+ var supportsViewTransitions = () => {
60
+ return isBrowser && "startViewTransition" in document;
61
+ };
62
+ var prefersReducedMotion = () => {
63
+ return isBrowser && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
64
+ };
65
+
66
+ exports.ThemeAnimationType = ThemeAnimationType;
67
+ exports.getSystemTheme = getSystemTheme;
68
+ exports.injectBaseStyles = injectBaseStyles;
69
+ exports.prefersReducedMotion = prefersReducedMotion;
70
+ exports.resolveTheme = resolveTheme;
71
+ exports.resolveThemeForServer = resolveThemeForServer;
72
+ exports.supportsViewTransitions = supportsViewTransitions;
73
+ //# sourceMappingURL=index.cjs.map
74
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/types/index.ts","../../src/core/utils/animations.ts"],"names":["ThemeAnimationType"],"mappings":";;;AAEO,IAAK,kBAAA,qBAAAA,mBAAAA,KAAL;AACL,EAAAA,oBAAA,QAAA,CAAA,GAAS,QAAA;AACT,EAAAA,oBAAA,aAAA,CAAA,GAAc,aAAA;AACd,EAAAA,oBAAA,OAAA,CAAA,GAAQ,OAAA;AAHE,EAAA,OAAAA,mBAAAA;AAAA,CAAA,EAAA,kBAAA,IAAA,EAAA;;;ACAZ,IAAM,SAAA,GAAY,OAAO,MAAA,KAAW,WAAA;AAE7B,IAAM,mBAAmB,MAAY;AAC1C,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,MAAM,OAAA,GAAU,2BAAA;AAChB,IAAA,IAAI,CAAC,QAAA,CAAS,cAAA,CAAe,OAAO,CAAA,EAAG;AACrC,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,aAAA,CAAc,OAAO,CAAA;AAC5C,MAAA,KAAA,CAAM,EAAA,GAAK,OAAA;AACX,MAAA,MAAM,gBAAA,GACJ,MAAA,CAAO,UAAA,IAAc,GAAA,IAAQ,OAAO,WAAA,IAAe,GAAA;AAErD,MAAA,KAAA,CAAM,WAAA,GAAc;AAAA;AAAA;AAAA;AAAA;AAAA,UAAA,EAKd,gBAAA,GAAmB,8BAA8B,EAAE;AAAA;AAAA;AAAA,QAAA,EAIrD,gBAAA,GACI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA,CAAA,GAUA,EACN;AAAA,MAAA,CAAA;AAEF,MAAA,QAAA,CAAS,IAAA,CAAK,YAAY,KAAK,CAAA;AAAA,IACjC;AAAA,EACF;AACF;AAYO,IAAM,iBAAiB,MAAwB;AACpD,EAAA,IAAI,CAAC,WAAW,OAAO,OAAA;AACvB,EAAA,OAAO,MAAA,CAAO,UAAA,CAAW,8BAA8B,CAAA,CAAE,UACrD,MAAA,GACA,OAAA;AACN;AAEO,IAAM,YAAA,GAAe,CAAC,KAAA,KAAoC;AAC/D,EAAA,IAAI,UAAU,QAAA,EAAU;AACtB,IAAA,OAAO,cAAA,EAAe;AAAA,EACxB;AACA,EAAA,OAAO,KAAA,KAAU,SAAS,MAAA,GAAS,OAAA;AACrC;AAEO,IAAM,qBAAA,GAAwB,CACnC,KAAA,KACgC;AAChC,EAAA,IAAI,UAAU,QAAA,EAAU;AACtB,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAO,KAAA,KAAU,SAAS,MAAA,GAAS,OAAA;AACrC;AAEO,IAAM,0BAA0B,MAAe;AACpD,EAAA,OAAO,aAAa,qBAAA,IAAyB,QAAA;AAC/C;AAEO,IAAM,uBAAuB,MAAe;AACjD,EAAA,OACE,SAAA,IAAa,MAAA,CAAO,UAAA,CAAW,kCAAkC,CAAA,CAAE,OAAA;AAEvE","file":"index.cjs","sourcesContent":["import { RefObject } from 'react'\n\nexport enum ThemeAnimationType {\n CIRCLE = 'circle',\n BLUR_CIRCLE = 'blur-circle',\n SLIDE = 'slide',\n}\n\nexport type SlideDirection =\n | 'left'\n | 'right'\n | 'top'\n | 'bottom'\n | 'top-left'\n | 'top-right'\n | 'bottom-left'\n | 'bottom-right'\n\nexport type Theme = 'light' | 'dark' | 'system'\nexport type ColorTheme = string\nexport type SystemThemeMode = 'css' | 'js'\n\nexport interface UseThemeAnimationProps {\n duration?: number\n easing?: string\n animationType?: ThemeAnimationType\n blurAmount?: number\n styleId?: string\n\n themes?: Theme[]\n colorThemes?: ColorTheme[]\n defaultTheme?: Theme\n defaultColorTheme?: ColorTheme\n\n globalClassName?: string\n colorThemePrefix?: string\n attribute?: 'class' | 'data-theme'\n value?: Record<string, string>\n enableColorScheme?: boolean\n\n storageKey?: string\n colorStorageKey?: string\n\n theme?: Theme\n colorTheme?: ColorTheme\n\n onThemeChange?: (theme: Theme) => void\n onColorThemeChange?: (colorTheme: ColorTheme) => void\n\n initialTheme?: Theme\n initialColorTheme?: ColorTheme\n systemThemeMode?: SystemThemeMode\n\n // Slide animation options (only valid when animationType is SLIDE)\n slideDirection?: SlideDirection\n slideFromX?: number // custom from translate X (%)\n slideFromY?: number // custom from translate Y (%)\n slideToX?: number // custom to translate X (%)\n slideToY?: number // custom to translate Y (%)\n}\n\nexport interface UseThemeAnimationReturn {\n ref: RefObject<HTMLButtonElement | null>\n\n theme: Theme\n colorTheme: ColorTheme\n resolvedTheme: 'light' | 'dark'\n systemTheme: 'light' | 'dark'\n\n setTheme: (theme: Theme) => void\n setColorTheme: (colorTheme: ColorTheme) => void\n\n switchTheme: (theme: Theme, animationOff?: boolean) => Promise<void>\n switchColorTheme: (colorTheme: string) => void\n\n toggleTheme: (animationOff?: boolean) => Promise<void>\n toggleLightTheme: (animationOff?: boolean) => Promise<void>\n toggleDarkTheme: (animationOff?: boolean) => Promise<void>\n toggleColorTheme: () => void\n\n createColorThemeToggle: (targetColorTheme: string) => () => void\n isColorThemeActive: (targetColorTheme: string) => boolean\n}\n\nexport interface ThemeSwitcherProps {\n themes?: Theme[]\n currentTheme?: Theme\n onThemeChange?: (theme: Theme) => void\n\n animationType?: ThemeAnimationType\n duration?: number\n\n className?: string\n size?: 'sm' | 'md' | 'lg'\n variant?: 'default' | 'outline' | 'ghost'\n\n icons?: {\n light?: React.ReactNode\n dark?: React.ReactNode\n system?: React.ReactNode\n }\n}\n\nexport interface ThemeSelectorProps {\n themes?: Theme[]\n colorThemes?: ColorTheme[]\n currentTheme?: Theme\n currentColorTheme?: ColorTheme\n\n onThemeChange?: (theme: Theme) => void\n onColorThemeChange?: (colorTheme: ColorTheme) => void\n\n animationType?: ThemeAnimationType\n duration?: number\n\n className?: string\n placeholder?: string\n\n themeLabel?: string\n colorThemeLabel?: string\n}\n","import { ThemeAnimationType } from '../types'\n\nconst isBrowser = typeof window !== 'undefined'\n\nexport const injectBaseStyles = (): void => {\n if (isBrowser) {\n const styleId = 'spaceman-theme-base-style'\n if (!document.getElementById(styleId)) {\n const style = document.createElement('style')\n style.id = styleId\n const isHighResolution =\n window.innerWidth >= 3000 || window.innerHeight >= 2000\n\n style.textContent = `\n ::view-transition-old(root),\n ::view-transition-new(root) {\n animation: none;\n mix-blend-mode: normal;\n ${isHighResolution ? 'transform: translateZ(0);' : ''}\n }\n \n ${\n isHighResolution\n ? `\n ::view-transition-group(root),\n ::view-transition-image-pair(root),\n ::view-transition-old(root),\n ::view-transition-new(root) {\n backface-visibility: hidden;\n perspective: 1000px;\n transform: translate3d(0, 0, 0);\n }\n `\n : ''\n }\n `\n document.head.appendChild(style)\n }\n }\n}\n\nexport const createBlurCircleMask = (blur: number): string => {\n const isHighResolution =\n isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000)\n\n const blurFilter = `<filter id=\"blur\"><feGaussianBlur stdDeviation=\"${blur}\" /></filter>`\n const circleRadius = isHighResolution ? 20 : 25\n\n 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>')`\n}\n\nexport const getSystemTheme = (): 'light' | 'dark' => {\n if (!isBrowser) return 'light'\n return window.matchMedia('(prefers-color-scheme: dark)').matches\n ? 'dark'\n : 'light'\n}\n\nexport const resolveTheme = (theme: string): 'light' | 'dark' => {\n if (theme === 'system') {\n return getSystemTheme()\n }\n return theme === 'dark' ? 'dark' : 'light'\n}\n\nexport const resolveThemeForServer = (\n theme: string\n): 'light' | 'dark' | 'system' => {\n if (theme === 'system') {\n return 'system'\n }\n return theme === 'dark' ? 'dark' : 'light'\n}\n\nexport const supportsViewTransitions = (): boolean => {\n return isBrowser && 'startViewTransition' in document\n}\n\nexport const prefersReducedMotion = (): boolean => {\n return (\n isBrowser && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n )\n}\n\nexport interface AnimationConfig {\n a?: number\n b?: number\n x: number\n y: number\n duration: number\n easing: string\n animationType: ThemeAnimationType\n blurAmount: number\n styleId: string\n}\n\nexport const createCircleAnimation = (config: AnimationConfig): void => {\n const { x, y, duration, easing } = config\n\n // Calculate the distance to each corner of the viewport\n const topLeft = Math.hypot(x, y)\n const topRight = Math.hypot(window.innerWidth - x, y)\n const bottomLeft = Math.hypot(x, window.innerHeight - y)\n const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y)\n\n // Find the maximum distance to ensure animation covers the entire viewport\n const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight)\n\n document.documentElement.animate(\n {\n clipPath: [\n `circle(0px at ${x}px ${y}px)`,\n `circle(${maxRadius}px at ${x}px ${y}px)`,\n ],\n },\n {\n duration,\n easing,\n pseudoElement: '::view-transition-new(root)',\n }\n )\n}\n\nexport const createSlideAnimation = (config: AnimationConfig): void => {\n const { a = -100, b = 0, x = 0, y = 0, duration, styleId } = config\n\n const styleElement = document.createElement('style')\n styleElement.id = styleId\n\n styleElement.textContent = `\n ::view-transition-old(root) {\n animation-delay: ${duration}ms;\n }\n \n ::view-transition-new(root) {\n animation: move-in ${duration}ms;\n animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);\n }\n \n @keyframes move-in {\n from {\n translate: ${a}% ${b}%;\n }\n to {\n translate: ${x}% ${y}%;\n }\n }\n `\n\n document.head.appendChild(styleElement)\n\n // Clean up after animation\n setTimeout(() => {\n const styleElement = document.getElementById(styleId)\n if (styleElement) {\n styleElement.remove()\n }\n }, duration)\n}\n\nexport const createBlurCircleAnimation = (config: AnimationConfig): void => {\n const { x, y, duration, easing, blurAmount, styleId } = config\n\n const existingStyle = document.getElementById(styleId)\n if (existingStyle) {\n existingStyle.remove()\n }\n\n const viewportSize = Math.max(window.innerWidth, window.innerHeight) + 200\n const isHighResolution =\n window.innerWidth >= 3000 || window.innerHeight >= 2000\n const scaleFactor = isHighResolution ? 2.5 : 4\n const optimalMaskSize = isHighResolution\n ? Math.min(viewportSize * scaleFactor, 5000)\n : viewportSize * scaleFactor\n\n const topLeft = Math.hypot(x, y)\n const topRight = Math.hypot(window.innerWidth - x, y)\n const bottomLeft = Math.hypot(x, window.innerHeight - y)\n const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y)\n const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight)\n\n const styleElement = document.createElement('style')\n styleElement.id = styleId\n\n const blurFactor = isHighResolution ? 1.5 : 1.2\n const finalMaskSize = Math.max(optimalMaskSize, maxRadius * 2.5)\n\n styleElement.textContent = `\n ::view-transition-group(root) {\n animation-duration: ${duration}ms;\n animation-timing-function: ${\n isHighResolution\n ? 'cubic-bezier(0.2, 0, 0.2, 1)'\n : 'linear(' +\n '0 0%, 0.2342 12.49%, 0.4374 24.99%,' +\n '0.6093 37.49%, 0.6835 43.74%,' +\n '0.7499 49.99%, 0.8086 56.25%,' +\n '0.8593 62.5%, 0.9023 68.75%, 0.9375 75%,' +\n '0.9648 81.25%, 0.9844 87.5%,' +\n '0.9961 93.75%, 1 100%' +\n ')'\n };\n will-change: transform;\n }\n \n ::view-transition-new(root) {\n mask: ${createBlurCircleMask(blurAmount * blurFactor)} 0 0 / 100% 100% no-repeat;\n mask-position: ${x}px ${y}px;\n animation: spaceman-mask-scale ${duration}ms ${easing};\n transform-origin: ${x}px ${y}px;\n will-change: mask-size, mask-position;\n }\n \n ::view-transition-old(root),\n .dark::view-transition-old(root) {\n animation: spaceman-mask-scale ${duration}ms ${easing};\n transform-origin: ${x}px ${y}px;\n z-index: -1;\n will-change: mask-size, mask-position;\n }\n \n @keyframes spaceman-mask-scale {\n 0% {\n mask-size: 0px;\n mask-position: ${x}px ${y}px;\n }\n 100% {\n mask-size: ${finalMaskSize}px;\n mask-position: ${x - finalMaskSize / 2}px ${y - finalMaskSize / 2}px;\n }\n }\n `\n\n document.head.appendChild(styleElement)\n\n setTimeout(() => {\n const styleElement = document.getElementById(styleId)\n if (styleElement) {\n styleElement.remove()\n }\n }, duration)\n}\n"]}
@@ -1,13 +1,15 @@
1
1
  import { RefObject } from 'react';
2
- export declare enum ThemeAnimationType {
2
+
3
+ declare enum ThemeAnimationType {
3
4
  CIRCLE = "circle",
4
5
  BLUR_CIRCLE = "blur-circle",
5
6
  SLIDE = "slide"
6
7
  }
7
- export type SlideDirection = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
8
- export type Theme = 'light' | 'dark' | 'system';
9
- export type ColorTheme = string;
10
- export interface UseThemeAnimationProps {
8
+ type SlideDirection = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
9
+ type Theme = 'light' | 'dark' | 'system';
10
+ type ColorTheme = string;
11
+ type SystemThemeMode = 'css' | 'js';
12
+ interface UseThemeAnimationProps {
11
13
  duration?: number;
12
14
  easing?: string;
13
15
  animationType?: ThemeAnimationType;
@@ -19,23 +21,30 @@ export interface UseThemeAnimationProps {
19
21
  defaultColorTheme?: ColorTheme;
20
22
  globalClassName?: string;
21
23
  colorThemePrefix?: string;
24
+ attribute?: 'class' | 'data-theme';
25
+ value?: Record<string, string>;
26
+ enableColorScheme?: boolean;
22
27
  storageKey?: string;
23
28
  colorStorageKey?: string;
24
29
  theme?: Theme;
25
30
  colorTheme?: ColorTheme;
26
31
  onThemeChange?: (theme: Theme) => void;
27
32
  onColorThemeChange?: (colorTheme: ColorTheme) => void;
33
+ initialTheme?: Theme;
34
+ initialColorTheme?: ColorTheme;
35
+ systemThemeMode?: SystemThemeMode;
28
36
  slideDirection?: SlideDirection;
29
37
  slideFromX?: number;
30
38
  slideFromY?: number;
31
39
  slideToX?: number;
32
40
  slideToY?: number;
33
41
  }
34
- export interface UseThemeAnimationReturn {
42
+ interface UseThemeAnimationReturn {
35
43
  ref: RefObject<HTMLButtonElement | null>;
36
44
  theme: Theme;
37
45
  colorTheme: ColorTheme;
38
46
  resolvedTheme: 'light' | 'dark';
47
+ systemTheme: 'light' | 'dark';
39
48
  setTheme: (theme: Theme) => void;
40
49
  setColorTheme: (colorTheme: ColorTheme) => void;
41
50
  switchTheme: (theme: Theme, animationOff?: boolean) => Promise<void>;
@@ -47,7 +56,7 @@ export interface UseThemeAnimationReturn {
47
56
  createColorThemeToggle: (targetColorTheme: string) => () => void;
48
57
  isColorThemeActive: (targetColorTheme: string) => boolean;
49
58
  }
50
- export interface ThemeSwitcherProps {
59
+ interface ThemeSwitcherProps {
51
60
  themes?: Theme[];
52
61
  currentTheme?: Theme;
53
62
  onThemeChange?: (theme: Theme) => void;
@@ -62,7 +71,7 @@ export interface ThemeSwitcherProps {
62
71
  system?: React.ReactNode;
63
72
  };
64
73
  }
65
- export interface ThemeSelectorProps {
74
+ interface ThemeSelectorProps {
66
75
  themes?: Theme[];
67
76
  colorThemes?: ColorTheme[];
68
77
  currentTheme?: Theme;
@@ -76,3 +85,12 @@ export interface ThemeSelectorProps {
76
85
  themeLabel?: string;
77
86
  colorThemeLabel?: string;
78
87
  }
88
+
89
+ declare const injectBaseStyles: () => void;
90
+ declare const getSystemTheme: () => "light" | "dark";
91
+ declare const resolveTheme: (theme: string) => "light" | "dark";
92
+ declare const resolveThemeForServer: (theme: string) => "light" | "dark" | "system";
93
+ declare const supportsViewTransitions: () => boolean;
94
+ declare const prefersReducedMotion: () => boolean;
95
+
96
+ export { type ColorTheme, type SlideDirection, type SystemThemeMode, type Theme, ThemeAnimationType, type ThemeSelectorProps, type ThemeSwitcherProps, type UseThemeAnimationProps, type UseThemeAnimationReturn, getSystemTheme, injectBaseStyles, prefersReducedMotion, resolveTheme, resolveThemeForServer, supportsViewTransitions };
@@ -1,13 +1,15 @@
1
1
  import { RefObject } from 'react';
2
- export declare enum ThemeAnimationType {
2
+
3
+ declare enum ThemeAnimationType {
3
4
  CIRCLE = "circle",
4
5
  BLUR_CIRCLE = "blur-circle",
5
6
  SLIDE = "slide"
6
7
  }
7
- export type SlideDirection = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
8
- export type Theme = 'light' | 'dark' | 'system';
9
- export type ColorTheme = string;
10
- export interface UseThemeAnimationProps {
8
+ type SlideDirection = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
9
+ type Theme = 'light' | 'dark' | 'system';
10
+ type ColorTheme = string;
11
+ type SystemThemeMode = 'css' | 'js';
12
+ interface UseThemeAnimationProps {
11
13
  duration?: number;
12
14
  easing?: string;
13
15
  animationType?: ThemeAnimationType;
@@ -19,23 +21,30 @@ export interface UseThemeAnimationProps {
19
21
  defaultColorTheme?: ColorTheme;
20
22
  globalClassName?: string;
21
23
  colorThemePrefix?: string;
24
+ attribute?: 'class' | 'data-theme';
25
+ value?: Record<string, string>;
26
+ enableColorScheme?: boolean;
22
27
  storageKey?: string;
23
28
  colorStorageKey?: string;
24
29
  theme?: Theme;
25
30
  colorTheme?: ColorTheme;
26
31
  onThemeChange?: (theme: Theme) => void;
27
32
  onColorThemeChange?: (colorTheme: ColorTheme) => void;
33
+ initialTheme?: Theme;
34
+ initialColorTheme?: ColorTheme;
35
+ systemThemeMode?: SystemThemeMode;
28
36
  slideDirection?: SlideDirection;
29
37
  slideFromX?: number;
30
38
  slideFromY?: number;
31
39
  slideToX?: number;
32
40
  slideToY?: number;
33
41
  }
34
- export interface UseThemeAnimationReturn {
42
+ interface UseThemeAnimationReturn {
35
43
  ref: RefObject<HTMLButtonElement | null>;
36
44
  theme: Theme;
37
45
  colorTheme: ColorTheme;
38
46
  resolvedTheme: 'light' | 'dark';
47
+ systemTheme: 'light' | 'dark';
39
48
  setTheme: (theme: Theme) => void;
40
49
  setColorTheme: (colorTheme: ColorTheme) => void;
41
50
  switchTheme: (theme: Theme, animationOff?: boolean) => Promise<void>;
@@ -47,7 +56,7 @@ export interface UseThemeAnimationReturn {
47
56
  createColorThemeToggle: (targetColorTheme: string) => () => void;
48
57
  isColorThemeActive: (targetColorTheme: string) => boolean;
49
58
  }
50
- export interface ThemeSwitcherProps {
59
+ interface ThemeSwitcherProps {
51
60
  themes?: Theme[];
52
61
  currentTheme?: Theme;
53
62
  onThemeChange?: (theme: Theme) => void;
@@ -62,7 +71,7 @@ export interface ThemeSwitcherProps {
62
71
  system?: React.ReactNode;
63
72
  };
64
73
  }
65
- export interface ThemeSelectorProps {
74
+ interface ThemeSelectorProps {
66
75
  themes?: Theme[];
67
76
  colorThemes?: ColorTheme[];
68
77
  currentTheme?: Theme;
@@ -76,3 +85,12 @@ export interface ThemeSelectorProps {
76
85
  themeLabel?: string;
77
86
  colorThemeLabel?: string;
78
87
  }
88
+
89
+ declare const injectBaseStyles: () => void;
90
+ declare const getSystemTheme: () => "light" | "dark";
91
+ declare const resolveTheme: (theme: string) => "light" | "dark";
92
+ declare const resolveThemeForServer: (theme: string) => "light" | "dark" | "system";
93
+ declare const supportsViewTransitions: () => boolean;
94
+ declare const prefersReducedMotion: () => boolean;
95
+
96
+ export { type ColorTheme, type SlideDirection, type SystemThemeMode, type Theme, ThemeAnimationType, type ThemeSelectorProps, type ThemeSwitcherProps, type UseThemeAnimationProps, type UseThemeAnimationReturn, getSystemTheme, injectBaseStyles, prefersReducedMotion, resolveTheme, resolveThemeForServer, supportsViewTransitions };
@@ -0,0 +1,66 @@
1
+ // src/core/types/index.ts
2
+ var ThemeAnimationType = /* @__PURE__ */ ((ThemeAnimationType2) => {
3
+ ThemeAnimationType2["CIRCLE"] = "circle";
4
+ ThemeAnimationType2["BLUR_CIRCLE"] = "blur-circle";
5
+ ThemeAnimationType2["SLIDE"] = "slide";
6
+ return ThemeAnimationType2;
7
+ })(ThemeAnimationType || {});
8
+
9
+ // src/core/utils/animations.ts
10
+ var isBrowser = typeof window !== "undefined";
11
+ var injectBaseStyles = () => {
12
+ if (isBrowser) {
13
+ const styleId = "spaceman-theme-base-style";
14
+ if (!document.getElementById(styleId)) {
15
+ const style = document.createElement("style");
16
+ style.id = styleId;
17
+ const isHighResolution = window.innerWidth >= 3e3 || window.innerHeight >= 2e3;
18
+ style.textContent = `
19
+ ::view-transition-old(root),
20
+ ::view-transition-new(root) {
21
+ animation: none;
22
+ mix-blend-mode: normal;
23
+ ${isHighResolution ? "transform: translateZ(0);" : ""}
24
+ }
25
+
26
+ ${isHighResolution ? `
27
+ ::view-transition-group(root),
28
+ ::view-transition-image-pair(root),
29
+ ::view-transition-old(root),
30
+ ::view-transition-new(root) {
31
+ backface-visibility: hidden;
32
+ perspective: 1000px;
33
+ transform: translate3d(0, 0, 0);
34
+ }
35
+ ` : ""}
36
+ `;
37
+ document.head.appendChild(style);
38
+ }
39
+ }
40
+ };
41
+ var getSystemTheme = () => {
42
+ if (!isBrowser) return "light";
43
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
44
+ };
45
+ var resolveTheme = (theme) => {
46
+ if (theme === "system") {
47
+ return getSystemTheme();
48
+ }
49
+ return theme === "dark" ? "dark" : "light";
50
+ };
51
+ var resolveThemeForServer = (theme) => {
52
+ if (theme === "system") {
53
+ return "system";
54
+ }
55
+ return theme === "dark" ? "dark" : "light";
56
+ };
57
+ var supportsViewTransitions = () => {
58
+ return isBrowser && "startViewTransition" in document;
59
+ };
60
+ var prefersReducedMotion = () => {
61
+ return isBrowser && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
62
+ };
63
+
64
+ export { ThemeAnimationType, getSystemTheme, injectBaseStyles, prefersReducedMotion, resolveTheme, resolveThemeForServer, supportsViewTransitions };
65
+ //# sourceMappingURL=index.js.map
66
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/types/index.ts","../../src/core/utils/animations.ts"],"names":["ThemeAnimationType"],"mappings":";AAEO,IAAK,kBAAA,qBAAAA,mBAAAA,KAAL;AACL,EAAAA,oBAAA,QAAA,CAAA,GAAS,QAAA;AACT,EAAAA,oBAAA,aAAA,CAAA,GAAc,aAAA;AACd,EAAAA,oBAAA,OAAA,CAAA,GAAQ,OAAA;AAHE,EAAA,OAAAA,mBAAAA;AAAA,CAAA,EAAA,kBAAA,IAAA,EAAA;;;ACAZ,IAAM,SAAA,GAAY,OAAO,MAAA,KAAW,WAAA;AAE7B,IAAM,mBAAmB,MAAY;AAC1C,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,MAAM,OAAA,GAAU,2BAAA;AAChB,IAAA,IAAI,CAAC,QAAA,CAAS,cAAA,CAAe,OAAO,CAAA,EAAG;AACrC,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,aAAA,CAAc,OAAO,CAAA;AAC5C,MAAA,KAAA,CAAM,EAAA,GAAK,OAAA;AACX,MAAA,MAAM,gBAAA,GACJ,MAAA,CAAO,UAAA,IAAc,GAAA,IAAQ,OAAO,WAAA,IAAe,GAAA;AAErD,MAAA,KAAA,CAAM,WAAA,GAAc;AAAA;AAAA;AAAA;AAAA;AAAA,UAAA,EAKd,gBAAA,GAAmB,8BAA8B,EAAE;AAAA;AAAA;AAAA,QAAA,EAIrD,gBAAA,GACI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA,CAAA,GAUA,EACN;AAAA,MAAA,CAAA;AAEF,MAAA,QAAA,CAAS,IAAA,CAAK,YAAY,KAAK,CAAA;AAAA,IACjC;AAAA,EACF;AACF;AAYO,IAAM,iBAAiB,MAAwB;AACpD,EAAA,IAAI,CAAC,WAAW,OAAO,OAAA;AACvB,EAAA,OAAO,MAAA,CAAO,UAAA,CAAW,8BAA8B,CAAA,CAAE,UACrD,MAAA,GACA,OAAA;AACN;AAEO,IAAM,YAAA,GAAe,CAAC,KAAA,KAAoC;AAC/D,EAAA,IAAI,UAAU,QAAA,EAAU;AACtB,IAAA,OAAO,cAAA,EAAe;AAAA,EACxB;AACA,EAAA,OAAO,KAAA,KAAU,SAAS,MAAA,GAAS,OAAA;AACrC;AAEO,IAAM,qBAAA,GAAwB,CACnC,KAAA,KACgC;AAChC,EAAA,IAAI,UAAU,QAAA,EAAU;AACtB,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAO,KAAA,KAAU,SAAS,MAAA,GAAS,OAAA;AACrC;AAEO,IAAM,0BAA0B,MAAe;AACpD,EAAA,OAAO,aAAa,qBAAA,IAAyB,QAAA;AAC/C;AAEO,IAAM,uBAAuB,MAAe;AACjD,EAAA,OACE,SAAA,IAAa,MAAA,CAAO,UAAA,CAAW,kCAAkC,CAAA,CAAE,OAAA;AAEvE","file":"index.js","sourcesContent":["import { RefObject } from 'react'\n\nexport enum ThemeAnimationType {\n CIRCLE = 'circle',\n BLUR_CIRCLE = 'blur-circle',\n SLIDE = 'slide',\n}\n\nexport type SlideDirection =\n | 'left'\n | 'right'\n | 'top'\n | 'bottom'\n | 'top-left'\n | 'top-right'\n | 'bottom-left'\n | 'bottom-right'\n\nexport type Theme = 'light' | 'dark' | 'system'\nexport type ColorTheme = string\nexport type SystemThemeMode = 'css' | 'js'\n\nexport interface UseThemeAnimationProps {\n duration?: number\n easing?: string\n animationType?: ThemeAnimationType\n blurAmount?: number\n styleId?: string\n\n themes?: Theme[]\n colorThemes?: ColorTheme[]\n defaultTheme?: Theme\n defaultColorTheme?: ColorTheme\n\n globalClassName?: string\n colorThemePrefix?: string\n attribute?: 'class' | 'data-theme'\n value?: Record<string, string>\n enableColorScheme?: boolean\n\n storageKey?: string\n colorStorageKey?: string\n\n theme?: Theme\n colorTheme?: ColorTheme\n\n onThemeChange?: (theme: Theme) => void\n onColorThemeChange?: (colorTheme: ColorTheme) => void\n\n initialTheme?: Theme\n initialColorTheme?: ColorTheme\n systemThemeMode?: SystemThemeMode\n\n // Slide animation options (only valid when animationType is SLIDE)\n slideDirection?: SlideDirection\n slideFromX?: number // custom from translate X (%)\n slideFromY?: number // custom from translate Y (%)\n slideToX?: number // custom to translate X (%)\n slideToY?: number // custom to translate Y (%)\n}\n\nexport interface UseThemeAnimationReturn {\n ref: RefObject<HTMLButtonElement | null>\n\n theme: Theme\n colorTheme: ColorTheme\n resolvedTheme: 'light' | 'dark'\n systemTheme: 'light' | 'dark'\n\n setTheme: (theme: Theme) => void\n setColorTheme: (colorTheme: ColorTheme) => void\n\n switchTheme: (theme: Theme, animationOff?: boolean) => Promise<void>\n switchColorTheme: (colorTheme: string) => void\n\n toggleTheme: (animationOff?: boolean) => Promise<void>\n toggleLightTheme: (animationOff?: boolean) => Promise<void>\n toggleDarkTheme: (animationOff?: boolean) => Promise<void>\n toggleColorTheme: () => void\n\n createColorThemeToggle: (targetColorTheme: string) => () => void\n isColorThemeActive: (targetColorTheme: string) => boolean\n}\n\nexport interface ThemeSwitcherProps {\n themes?: Theme[]\n currentTheme?: Theme\n onThemeChange?: (theme: Theme) => void\n\n animationType?: ThemeAnimationType\n duration?: number\n\n className?: string\n size?: 'sm' | 'md' | 'lg'\n variant?: 'default' | 'outline' | 'ghost'\n\n icons?: {\n light?: React.ReactNode\n dark?: React.ReactNode\n system?: React.ReactNode\n }\n}\n\nexport interface ThemeSelectorProps {\n themes?: Theme[]\n colorThemes?: ColorTheme[]\n currentTheme?: Theme\n currentColorTheme?: ColorTheme\n\n onThemeChange?: (theme: Theme) => void\n onColorThemeChange?: (colorTheme: ColorTheme) => void\n\n animationType?: ThemeAnimationType\n duration?: number\n\n className?: string\n placeholder?: string\n\n themeLabel?: string\n colorThemeLabel?: string\n}\n","import { ThemeAnimationType } from '../types'\n\nconst isBrowser = typeof window !== 'undefined'\n\nexport const injectBaseStyles = (): void => {\n if (isBrowser) {\n const styleId = 'spaceman-theme-base-style'\n if (!document.getElementById(styleId)) {\n const style = document.createElement('style')\n style.id = styleId\n const isHighResolution =\n window.innerWidth >= 3000 || window.innerHeight >= 2000\n\n style.textContent = `\n ::view-transition-old(root),\n ::view-transition-new(root) {\n animation: none;\n mix-blend-mode: normal;\n ${isHighResolution ? 'transform: translateZ(0);' : ''}\n }\n \n ${\n isHighResolution\n ? `\n ::view-transition-group(root),\n ::view-transition-image-pair(root),\n ::view-transition-old(root),\n ::view-transition-new(root) {\n backface-visibility: hidden;\n perspective: 1000px;\n transform: translate3d(0, 0, 0);\n }\n `\n : ''\n }\n `\n document.head.appendChild(style)\n }\n }\n}\n\nexport const createBlurCircleMask = (blur: number): string => {\n const isHighResolution =\n isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000)\n\n const blurFilter = `<filter id=\"blur\"><feGaussianBlur stdDeviation=\"${blur}\" /></filter>`\n const circleRadius = isHighResolution ? 20 : 25\n\n 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>')`\n}\n\nexport const getSystemTheme = (): 'light' | 'dark' => {\n if (!isBrowser) return 'light'\n return window.matchMedia('(prefers-color-scheme: dark)').matches\n ? 'dark'\n : 'light'\n}\n\nexport const resolveTheme = (theme: string): 'light' | 'dark' => {\n if (theme === 'system') {\n return getSystemTheme()\n }\n return theme === 'dark' ? 'dark' : 'light'\n}\n\nexport const resolveThemeForServer = (\n theme: string\n): 'light' | 'dark' | 'system' => {\n if (theme === 'system') {\n return 'system'\n }\n return theme === 'dark' ? 'dark' : 'light'\n}\n\nexport const supportsViewTransitions = (): boolean => {\n return isBrowser && 'startViewTransition' in document\n}\n\nexport const prefersReducedMotion = (): boolean => {\n return (\n isBrowser && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n )\n}\n\nexport interface AnimationConfig {\n a?: number\n b?: number\n x: number\n y: number\n duration: number\n easing: string\n animationType: ThemeAnimationType\n blurAmount: number\n styleId: string\n}\n\nexport const createCircleAnimation = (config: AnimationConfig): void => {\n const { x, y, duration, easing } = config\n\n // Calculate the distance to each corner of the viewport\n const topLeft = Math.hypot(x, y)\n const topRight = Math.hypot(window.innerWidth - x, y)\n const bottomLeft = Math.hypot(x, window.innerHeight - y)\n const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y)\n\n // Find the maximum distance to ensure animation covers the entire viewport\n const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight)\n\n document.documentElement.animate(\n {\n clipPath: [\n `circle(0px at ${x}px ${y}px)`,\n `circle(${maxRadius}px at ${x}px ${y}px)`,\n ],\n },\n {\n duration,\n easing,\n pseudoElement: '::view-transition-new(root)',\n }\n )\n}\n\nexport const createSlideAnimation = (config: AnimationConfig): void => {\n const { a = -100, b = 0, x = 0, y = 0, duration, styleId } = config\n\n const styleElement = document.createElement('style')\n styleElement.id = styleId\n\n styleElement.textContent = `\n ::view-transition-old(root) {\n animation-delay: ${duration}ms;\n }\n \n ::view-transition-new(root) {\n animation: move-in ${duration}ms;\n animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);\n }\n \n @keyframes move-in {\n from {\n translate: ${a}% ${b}%;\n }\n to {\n translate: ${x}% ${y}%;\n }\n }\n `\n\n document.head.appendChild(styleElement)\n\n // Clean up after animation\n setTimeout(() => {\n const styleElement = document.getElementById(styleId)\n if (styleElement) {\n styleElement.remove()\n }\n }, duration)\n}\n\nexport const createBlurCircleAnimation = (config: AnimationConfig): void => {\n const { x, y, duration, easing, blurAmount, styleId } = config\n\n const existingStyle = document.getElementById(styleId)\n if (existingStyle) {\n existingStyle.remove()\n }\n\n const viewportSize = Math.max(window.innerWidth, window.innerHeight) + 200\n const isHighResolution =\n window.innerWidth >= 3000 || window.innerHeight >= 2000\n const scaleFactor = isHighResolution ? 2.5 : 4\n const optimalMaskSize = isHighResolution\n ? Math.min(viewportSize * scaleFactor, 5000)\n : viewportSize * scaleFactor\n\n const topLeft = Math.hypot(x, y)\n const topRight = Math.hypot(window.innerWidth - x, y)\n const bottomLeft = Math.hypot(x, window.innerHeight - y)\n const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y)\n const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight)\n\n const styleElement = document.createElement('style')\n styleElement.id = styleId\n\n const blurFactor = isHighResolution ? 1.5 : 1.2\n const finalMaskSize = Math.max(optimalMaskSize, maxRadius * 2.5)\n\n styleElement.textContent = `\n ::view-transition-group(root) {\n animation-duration: ${duration}ms;\n animation-timing-function: ${\n isHighResolution\n ? 'cubic-bezier(0.2, 0, 0.2, 1)'\n : 'linear(' +\n '0 0%, 0.2342 12.49%, 0.4374 24.99%,' +\n '0.6093 37.49%, 0.6835 43.74%,' +\n '0.7499 49.99%, 0.8086 56.25%,' +\n '0.8593 62.5%, 0.9023 68.75%, 0.9375 75%,' +\n '0.9648 81.25%, 0.9844 87.5%,' +\n '0.9961 93.75%, 1 100%' +\n ')'\n };\n will-change: transform;\n }\n \n ::view-transition-new(root) {\n mask: ${createBlurCircleMask(blurAmount * blurFactor)} 0 0 / 100% 100% no-repeat;\n mask-position: ${x}px ${y}px;\n animation: spaceman-mask-scale ${duration}ms ${easing};\n transform-origin: ${x}px ${y}px;\n will-change: mask-size, mask-position;\n }\n \n ::view-transition-old(root),\n .dark::view-transition-old(root) {\n animation: spaceman-mask-scale ${duration}ms ${easing};\n transform-origin: ${x}px ${y}px;\n z-index: -1;\n will-change: mask-size, mask-position;\n }\n \n @keyframes spaceman-mask-scale {\n 0% {\n mask-size: 0px;\n mask-position: ${x}px ${y}px;\n }\n 100% {\n mask-size: ${finalMaskSize}px;\n mask-position: ${x - finalMaskSize / 2}px ${y - finalMaskSize / 2}px;\n }\n }\n `\n\n document.head.appendChild(styleElement)\n\n setTimeout(() => {\n const styleElement = document.getElementById(styleId)\n if (styleElement) {\n styleElement.remove()\n }\n }, duration)\n}\n"]}