@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.
- package/README.md +72 -142
- package/dist/core/index.cjs +74 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/{esm/types/index.d.ts → core/index.d.cts} +26 -8
- package/dist/{cjs/types → core}/index.d.ts +26 -8
- package/dist/core/index.js +66 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index.cjs +1756 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1706 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +1742 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +224 -0
- package/dist/react/index.d.ts +224 -0
- package/dist/react/index.js +1697 -0
- package/dist/react/index.js.map +1 -0
- package/dist/tanstack/index.cjs +34 -0
- package/dist/tanstack/index.cjs.map +1 -0
- package/dist/tanstack/index.d.cts +20 -0
- package/dist/tanstack/index.d.ts +20 -0
- package/dist/tanstack/index.js +27 -0
- package/dist/tanstack/index.js.map +1 -0
- package/package.json +66 -37
- package/dist/cjs/components/NextThemeProvider.d.ts +0 -81
- package/dist/cjs/components/NextThemeProvider.js +0 -223
- package/dist/cjs/components/SpacemanThemeProvider.d.ts +0 -57
- package/dist/cjs/components/SpacemanThemeProvider.js +0 -113
- package/dist/cjs/components/TanStackThemeProvider.d.ts +0 -68
- package/dist/cjs/components/TanStackThemeProvider.js +0 -183
- package/dist/cjs/components/ThemeSelector.d.ts +0 -3
- package/dist/cjs/components/ThemeSelector.js +0 -39
- package/dist/cjs/components/ThemeSwitcher.d.ts +0 -3
- package/dist/cjs/components/ThemeSwitcher.js +0 -81
- package/dist/cjs/components/ViteThemeProvider.d.ts +0 -61
- package/dist/cjs/components/ViteThemeProvider.js +0 -140
- package/dist/cjs/components/ui/select.d.ts +0 -15
- package/dist/cjs/components/ui/select.js +0 -83
- package/dist/cjs/hooks/use-theme-animation.d.ts +0 -2
- package/dist/cjs/hooks/use-theme-animation.js +0 -234
- package/dist/cjs/index.d.ts +0 -10
- package/dist/cjs/index.js +0 -30
- package/dist/cjs/types/index.js +0 -9
- package/dist/cjs/utils/animations.d.ts +0 -21
- package/dist/cjs/utils/animations.js +0 -189
- package/dist/esm/components/NextThemeProvider.d.ts +0 -81
- package/dist/esm/components/NextThemeProvider.js +0 -185
- package/dist/esm/components/SpacemanThemeProvider.d.ts +0 -57
- package/dist/esm/components/SpacemanThemeProvider.js +0 -108
- package/dist/esm/components/TanStackThemeProvider.d.ts +0 -68
- package/dist/esm/components/TanStackThemeProvider.js +0 -178
- package/dist/esm/components/ThemeSelector.d.ts +0 -3
- package/dist/esm/components/ThemeSelector.js +0 -35
- package/dist/esm/components/ThemeSwitcher.d.ts +0 -3
- package/dist/esm/components/ThemeSwitcher.js +0 -77
- package/dist/esm/components/ViteThemeProvider.d.ts +0 -61
- package/dist/esm/components/ViteThemeProvider.js +0 -135
- package/dist/esm/components/ui/select.d.ts +0 -15
- package/dist/esm/components/ui/select.js +0 -39
- package/dist/esm/hooks/use-theme-animation.d.ts +0 -2
- package/dist/esm/hooks/use-theme-animation.js +0 -230
- package/dist/esm/index.d.ts +0 -10
- package/dist/esm/index.js +0 -10
- package/dist/esm/types/index.js +0 -6
- package/dist/esm/utils/animations.d.ts +0 -21
- 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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
9
|
+
Highlights in this release:
|
|
18
10
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
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
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
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 {
|
|
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
|
-
|
|
103
|
+
## Documentation
|
|
74
104
|
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
136
|
+
The root package exports:
|
|
211
137
|
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
+
|
|
3
|
+
declare enum ThemeAnimationType {
|
|
3
4
|
CIRCLE = "circle",
|
|
4
5
|
BLUR_CIRCLE = "blur-circle",
|
|
5
6
|
SLIDE = "slide"
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
+
|
|
3
|
+
declare enum ThemeAnimationType {
|
|
3
4
|
CIRCLE = "circle",
|
|
4
5
|
BLUR_CIRCLE = "blur-circle",
|
|
5
6
|
SLIDE = "slide"
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"]}
|