@space-man/react-theme-animation 1.0.0 → 1.0.1
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 +8 -247
- package/package.json +10 -2
- package/dist/components/SpacemanThemeProvider.d.ts +0 -51
- package/dist/components/SpacemanThemeProvider.js +0 -88
- package/dist/components/ThemeSelector.d.ts +0 -3
- package/dist/components/ThemeSelector.js +0 -35
- package/dist/components/ThemeSwitcher.d.ts +0 -3
- package/dist/components/ThemeSwitcher.js +0 -96
- package/dist/components/ui/select.d.ts +0 -15
- package/dist/components/ui/select.js +0 -39
- package/dist/hooks/use-theme-animation.d.ts +0 -2
- package/dist/hooks/use-theme-animation.js +0 -145
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -6
- package/dist/types/index.d.ts +0 -65
- package/dist/types/index.js +0 -5
- package/dist/utils/animations.d.ts +0 -18
- package/dist/utils/animations.js +0 -145
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @space-man/react-theme-animation
|
|
2
2
|
|
|
3
3
|
React theme switching with smooth view transition animations, multi-theme support, and synchronized state management.
|
|
4
4
|
|
|
@@ -20,7 +20,7 @@ React theme switching with smooth view transition animations, multi-theme suppor
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npm install @
|
|
23
|
+
npm install @space-man/react-theme-animation
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## Usage Patterns
|
|
@@ -30,6 +30,10 @@ This library supports two main usage patterns:
|
|
|
30
30
|
1. **Hook-Only Usage**: Direct hook usage for custom implementations
|
|
31
31
|
2. **Provider Pattern**: Centralized state management (recommended)
|
|
32
32
|
|
|
33
|
+
📚 **[See the complete Getting Started Guide](./getting-started.md)** for detailed examples, advanced configurations, and best practices.
|
|
34
|
+
|
|
35
|
+
🚀 **[View the Code Example](./example/)** to see this package in action with a complete implementation of both hook and provider pattern.
|
|
36
|
+
|
|
33
37
|
---
|
|
34
38
|
|
|
35
39
|
## 1. Hook-Only Usage For Light and Dark mode animation
|
|
@@ -39,7 +43,7 @@ Perfect for custom theme toggle buttons and complete control over animated theme
|
|
|
39
43
|
### Basic Theme Toggle
|
|
40
44
|
|
|
41
45
|
```tsx
|
|
42
|
-
import { useThemeAnimation } from '@
|
|
46
|
+
import { useThemeAnimation } from '@space-man/react-theme-animation';
|
|
43
47
|
|
|
44
48
|
function ThemeToggle() {
|
|
45
49
|
const { theme, toggleTheme, ref } = useThemeAnimation();
|
|
@@ -65,7 +69,7 @@ import {
|
|
|
65
69
|
SpacemanThemeProvider,
|
|
66
70
|
ThemeSwitcher,
|
|
67
71
|
ThemeSelector,
|
|
68
|
-
} from '@
|
|
72
|
+
} from '@space-man/react-theme-animation';
|
|
69
73
|
|
|
70
74
|
function App() {
|
|
71
75
|
return (
|
|
@@ -88,249 +92,6 @@ function App() {
|
|
|
88
92
|
}
|
|
89
93
|
```
|
|
90
94
|
|
|
91
|
-
### Advanced Provider Configuration
|
|
92
|
-
|
|
93
|
-
```tsx
|
|
94
|
-
import {
|
|
95
|
-
SpacemanThemeProvider,
|
|
96
|
-
ThemeSwitcher,
|
|
97
|
-
ThemeSelector,
|
|
98
|
-
ThemeAnimationType,
|
|
99
|
-
} from '@spaceman/react-theme-animation';
|
|
100
|
-
|
|
101
|
-
function App() {
|
|
102
|
-
return (
|
|
103
|
-
<SpacemanThemeProvider
|
|
104
|
-
defaultTheme="system"
|
|
105
|
-
defaultColorTheme="blue"
|
|
106
|
-
themes={['light', 'dark', 'system']}
|
|
107
|
-
colorThemes={['default', 'blue', 'green', 'purple', 'red']}
|
|
108
|
-
animationType={ThemeAnimationType.BLUR_CIRCLE}
|
|
109
|
-
blurAmount={10}
|
|
110
|
-
duration={1000}
|
|
111
|
-
onThemeChange={theme => {
|
|
112
|
-
// Analytics, localStorage, etc.
|
|
113
|
-
console.log('Global theme changed:', theme);
|
|
114
|
-
}}
|
|
115
|
-
onColorThemeChange={colorTheme => {
|
|
116
|
-
console.log('Global color theme changed:', colorTheme);
|
|
117
|
-
}}
|
|
118
|
-
>
|
|
119
|
-
<AppLayout />
|
|
120
|
-
</SpacemanThemeProvider>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function AppLayout() {
|
|
125
|
-
return (
|
|
126
|
-
<div className="app-layout">
|
|
127
|
-
<Header />
|
|
128
|
-
<Sidebar />
|
|
129
|
-
<MainContent />
|
|
130
|
-
</div>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function Header() {
|
|
135
|
-
return (
|
|
136
|
-
<header className="header">
|
|
137
|
-
<h1>My App</h1>
|
|
138
|
-
<ThemeSwitcher className="ml-auto" />
|
|
139
|
-
</header>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function Sidebar() {
|
|
144
|
-
return (
|
|
145
|
-
<aside className="sidebar">
|
|
146
|
-
<h2>Settings</h2>
|
|
147
|
-
<ThemeSelector colorThemes={['default', 'blue', 'green', 'purple', 'red']} />
|
|
148
|
-
</aside>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### Using the Provider Hook
|
|
154
|
-
|
|
155
|
-
Access theme state anywhere in your component tree. This hook also enables you to set a reference point for the animation to start from.
|
|
156
|
-
|
|
157
|
-
```tsx
|
|
158
|
-
import { useSpacemanTheme } from '@spaceman/react-theme-animation';
|
|
159
|
-
|
|
160
|
-
function ThemeStatus() {
|
|
161
|
-
const { theme, colorTheme, switchTheme, setColorTheme, switchThemeFromElement } =
|
|
162
|
-
useSpacemanTheme();
|
|
163
|
-
|
|
164
|
-
const handleCustomThemeSwitch = event => {
|
|
165
|
-
// Animate from the clicked element
|
|
166
|
-
switchThemeFromElement('dark', event.currentTarget);
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
return (
|
|
170
|
-
<div className="theme-status">
|
|
171
|
-
<p>Current theme: {theme}</p>
|
|
172
|
-
<p>Current color: {colorTheme}</p>
|
|
173
|
-
|
|
174
|
-
<button onClick={handleCustomThemeSwitch}>Switch to Dark (with animation)</button>
|
|
175
|
-
</div>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
## Complete Example: Advanced Theme System
|
|
183
|
-
|
|
184
|
-
Here's a complete example showcasing all features:
|
|
185
|
-
|
|
186
|
-
```tsx
|
|
187
|
-
import React from 'react';
|
|
188
|
-
import {
|
|
189
|
-
SpacemanThemeProvider,
|
|
190
|
-
ThemeSwitcher,
|
|
191
|
-
ThemeSelector,
|
|
192
|
-
useSpacemanTheme,
|
|
193
|
-
ThemeAnimationType,
|
|
194
|
-
} from '@spaceman/react-theme-animation';
|
|
195
|
-
|
|
196
|
-
// Main App with Provider
|
|
197
|
-
function App() {
|
|
198
|
-
return (
|
|
199
|
-
<SpacemanThemeProvider
|
|
200
|
-
defaultTheme="system"
|
|
201
|
-
defaultColorTheme="blue"
|
|
202
|
-
themes={['light', 'dark', 'system']}
|
|
203
|
-
colorThemes={['default', 'blue', 'green', 'purple', 'red', 'orange']}
|
|
204
|
-
animationType={ThemeAnimationType.CIRCLE}
|
|
205
|
-
duration={800}
|
|
206
|
-
onThemeChange={theme => {
|
|
207
|
-
// Save to localStorage, analytics, etc.
|
|
208
|
-
localStorage.setItem('preferred-theme', theme);
|
|
209
|
-
}}
|
|
210
|
-
onColorThemeChange={colorTheme => {
|
|
211
|
-
localStorage.setItem('preferred-color-theme', colorTheme);
|
|
212
|
-
}}
|
|
213
|
-
>
|
|
214
|
-
<AppContent />
|
|
215
|
-
</SpacemanThemeProvider>
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// App Layout
|
|
220
|
-
function AppContent() {
|
|
221
|
-
return (
|
|
222
|
-
<div className="min-h-screen bg-background text-foreground">
|
|
223
|
-
<Header />
|
|
224
|
-
<div className="container mx-auto px-4 py-8">
|
|
225
|
-
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
|
226
|
-
<Sidebar />
|
|
227
|
-
<MainContent />
|
|
228
|
-
</div>
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Header with Theme Switcher
|
|
235
|
-
function Header() {
|
|
236
|
-
return (
|
|
237
|
-
<header className="border-b bg-card">
|
|
238
|
-
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
|
239
|
-
<h1 className="text-2xl font-bold">Theme Demo</h1>
|
|
240
|
-
<div className="flex items-center gap-4">
|
|
241
|
-
<ThemeSwitcher className="rounded-lg" />
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
</header>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Sidebar with Theme Selector
|
|
249
|
-
function Sidebar() {
|
|
250
|
-
return (
|
|
251
|
-
<aside className="space-y-6">
|
|
252
|
-
<div className="bg-card p-6 rounded-lg border">
|
|
253
|
-
<h2 className="text-lg font-semibold mb-4">Theme Settings</h2>
|
|
254
|
-
<div className="space-y-4">
|
|
255
|
-
<div>
|
|
256
|
-
<label className="text-sm font-medium mb-2 block">Color Theme</label>
|
|
257
|
-
<ThemeSelector colorThemes={['default', 'blue', 'green', 'purple', 'red', 'orange']} />
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
|
|
262
|
-
<ThemeInfo />
|
|
263
|
-
</aside>
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Component using the theme hook
|
|
268
|
-
function ThemeInfo() {
|
|
269
|
-
const { theme, colorTheme, switchThemeFromElement } = useSpacemanTheme();
|
|
270
|
-
|
|
271
|
-
const quickSwitchTheme = (newTheme, event) => {
|
|
272
|
-
switchThemeFromElement(newTheme, event.currentTarget);
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
return (
|
|
276
|
-
<div className="bg-card p-6 rounded-lg border">
|
|
277
|
-
<h3 className="text-lg font-semibold mb-4">Current State</h3>
|
|
278
|
-
<div className="space-y-2 text-sm">
|
|
279
|
-
<p>
|
|
280
|
-
<strong>Theme:</strong> {theme}
|
|
281
|
-
</p>
|
|
282
|
-
<p>
|
|
283
|
-
<strong>Color:</strong> {colorTheme}
|
|
284
|
-
</p>
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
|
-
<div className="mt-4 space-y-2">
|
|
288
|
-
<p className="text-sm font-medium">Quick Switch:</p>
|
|
289
|
-
<div className="flex gap-2">
|
|
290
|
-
{['light', 'dark'].map(t => (
|
|
291
|
-
<button
|
|
292
|
-
key={t}
|
|
293
|
-
onClick={e => quickSwitchTheme(t, e)}
|
|
294
|
-
className="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors"
|
|
295
|
-
>
|
|
296
|
-
{t}
|
|
297
|
-
</button>
|
|
298
|
-
))}
|
|
299
|
-
</div>
|
|
300
|
-
</div>
|
|
301
|
-
</div>
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Main content area
|
|
306
|
-
function MainContent() {
|
|
307
|
-
return (
|
|
308
|
-
<main className="lg:col-span-3 space-y-6">
|
|
309
|
-
<div className="bg-card p-6 rounded-lg border">
|
|
310
|
-
<h2 className="text-xl font-semibold mb-4">Welcome to Theme Demo</h2>
|
|
311
|
-
<p className="text-muted-foreground">
|
|
312
|
-
This demo showcases the spaceman-react-theme-animation library with synchronized theme
|
|
313
|
-
state, smooth animations, and multiple color themes.
|
|
314
|
-
</p>
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
318
|
-
{[1, 2, 3, 4].map(i => (
|
|
319
|
-
<div key={i} className="bg-card p-6 rounded-lg border">
|
|
320
|
-
<h3 className="font-semibold mb-2">Card {i}</h3>
|
|
321
|
-
<p className="text-sm text-muted-foreground">
|
|
322
|
-
Sample content that adapts to the current theme and color scheme.
|
|
323
|
-
</p>
|
|
324
|
-
</div>
|
|
325
|
-
))}
|
|
326
|
-
</div>
|
|
327
|
-
</main>
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export default App;
|
|
332
|
-
```
|
|
333
|
-
|
|
334
95
|
---
|
|
335
96
|
|
|
336
97
|
## API Reference
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@space-man/react-theme-animation",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "React theme switching with smooth view transition animations and multi-theme support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -49,7 +49,14 @@
|
|
|
49
49
|
"format": "prettier --write . --config ./.prettierrc",
|
|
50
50
|
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
|
51
51
|
"lint:fix": "eslint --fix . --ext .{js,jsx,ts,tsx}",
|
|
52
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
52
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
53
|
+
"changeset": "changeset add --type patch",
|
|
54
|
+
"changeset:patch": "changeset add --type patch",
|
|
55
|
+
"changeset:feature": "changeset add --type minor",
|
|
56
|
+
"changeset:breaking": "changeset add --type major",
|
|
57
|
+
"changeset:custom": "changeset",
|
|
58
|
+
"version-packages": "changeset version",
|
|
59
|
+
"release": "npm run build && changeset publish"
|
|
53
60
|
},
|
|
54
61
|
"peerDependencies": {
|
|
55
62
|
"react": ">=17",
|
|
@@ -58,6 +65,7 @@
|
|
|
58
65
|
"@radix-ui/react-select": ">=2"
|
|
59
66
|
},
|
|
60
67
|
"devDependencies": {
|
|
68
|
+
"@changesets/cli": "^2.27.1",
|
|
61
69
|
"@types/react": "^18.3.12",
|
|
62
70
|
"@types/react-dom": "^18.3.1",
|
|
63
71
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import React, { ReactNode } from 'react';
|
|
2
|
-
import { Theme, ColorTheme, ThemeAnimationType } from '../types';
|
|
3
|
-
/**
|
|
4
|
-
* Context type for the Spaceman Theme Provider
|
|
5
|
-
*/
|
|
6
|
-
interface SpacemanThemeContextType {
|
|
7
|
-
theme: Theme;
|
|
8
|
-
colorTheme: ColorTheme;
|
|
9
|
-
resolvedTheme: 'light' | 'dark';
|
|
10
|
-
setTheme: (theme: Theme) => void;
|
|
11
|
-
setColorTheme: (colorTheme: ColorTheme) => void;
|
|
12
|
-
toggleTheme: () => Promise<void>;
|
|
13
|
-
switchTheme: (theme: Theme) => Promise<void>;
|
|
14
|
-
switchThemeFromElement: (theme: Theme, element: HTMLButtonElement) => Promise<void>;
|
|
15
|
-
ref: React.RefObject<HTMLButtonElement | null>;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Props for the Spaceman Theme Provider
|
|
19
|
-
*/
|
|
20
|
-
interface SpacemanThemeProviderProps {
|
|
21
|
-
children: ReactNode;
|
|
22
|
-
themes?: Theme[];
|
|
23
|
-
colorThemes?: ColorTheme[];
|
|
24
|
-
defaultTheme?: Theme;
|
|
25
|
-
defaultColorTheme?: ColorTheme;
|
|
26
|
-
animationType?: ThemeAnimationType;
|
|
27
|
-
duration?: number;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Spaceman Theme Provider - Provides centralized theme state management
|
|
31
|
-
*
|
|
32
|
-
* This provider creates a single source of truth for theme state using the useThemeAnimation hook.
|
|
33
|
-
* All theme-related components should consume from this context to ensure synchronization.
|
|
34
|
-
*
|
|
35
|
-
* @param children - React children to wrap with theme context
|
|
36
|
-
* @param themes - Available theme options (default: ['light', 'dark', 'system'])
|
|
37
|
-
* @param colorThemes - Available color theme options (default: ['default'])
|
|
38
|
-
* @param defaultTheme - Default theme to use (default: 'system')
|
|
39
|
-
* @param defaultColorTheme - Default color theme to use (default: 'default')
|
|
40
|
-
* @param animationType - Animation type for theme transitions (default: CIRCLE)
|
|
41
|
-
* @param duration - Animation duration in milliseconds (default: 750)
|
|
42
|
-
*/
|
|
43
|
-
export declare const SpacemanThemeProvider: React.FC<SpacemanThemeProviderProps>;
|
|
44
|
-
/**
|
|
45
|
-
* Hook to consume the Spaceman Theme context
|
|
46
|
-
*
|
|
47
|
-
* @returns Theme context value with all theme state and methods
|
|
48
|
-
* @throws Error if used outside of SpacemanThemeProvider
|
|
49
|
-
*/
|
|
50
|
-
export declare const useSpacemanTheme: () => SpacemanThemeContextType;
|
|
51
|
-
export {};
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext } from 'react';
|
|
3
|
-
import { useThemeAnimation } from '../hooks/use-theme-animation';
|
|
4
|
-
import { ThemeAnimationType } from '../types';
|
|
5
|
-
const SpacemanThemeContext = createContext(undefined);
|
|
6
|
-
/**
|
|
7
|
-
* Spaceman Theme Provider - Provides centralized theme state management
|
|
8
|
-
*
|
|
9
|
-
* This provider creates a single source of truth for theme state using the useThemeAnimation hook.
|
|
10
|
-
* All theme-related components should consume from this context to ensure synchronization.
|
|
11
|
-
*
|
|
12
|
-
* @param children - React children to wrap with theme context
|
|
13
|
-
* @param themes - Available theme options (default: ['light', 'dark', 'system'])
|
|
14
|
-
* @param colorThemes - Available color theme options (default: ['default'])
|
|
15
|
-
* @param defaultTheme - Default theme to use (default: 'system')
|
|
16
|
-
* @param defaultColorTheme - Default color theme to use (default: 'default')
|
|
17
|
-
* @param animationType - Animation type for theme transitions (default: CIRCLE)
|
|
18
|
-
* @param duration - Animation duration in milliseconds (default: 750)
|
|
19
|
-
*/
|
|
20
|
-
export const SpacemanThemeProvider = ({ children, themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', animationType = ThemeAnimationType.CIRCLE, duration = 750, }) => {
|
|
21
|
-
// This is our master hook instance - the single source of truth
|
|
22
|
-
const themeState = useThemeAnimation({
|
|
23
|
-
themes,
|
|
24
|
-
colorThemes,
|
|
25
|
-
defaultTheme,
|
|
26
|
-
defaultColorTheme,
|
|
27
|
-
animationType,
|
|
28
|
-
duration,
|
|
29
|
-
});
|
|
30
|
-
/**
|
|
31
|
-
* Switch theme with animation originating from a specific element
|
|
32
|
-
*
|
|
33
|
-
* @param theme - Theme to switch to
|
|
34
|
-
* @param element - HTML button element to use as animation origin
|
|
35
|
-
*/
|
|
36
|
-
const switchThemeFromElement = async (theme, element) => {
|
|
37
|
-
// Temporarily set the ref to the clicked element
|
|
38
|
-
if (themeState.ref.current) {
|
|
39
|
-
const originalRef = themeState.ref.current;
|
|
40
|
-
// Use Object.defineProperty to temporarily override the ref
|
|
41
|
-
Object.defineProperty(themeState.ref, 'current', {
|
|
42
|
-
value: element,
|
|
43
|
-
writable: true,
|
|
44
|
-
configurable: true
|
|
45
|
-
});
|
|
46
|
-
await themeState.switchTheme(theme);
|
|
47
|
-
// Restore the original ref after animation
|
|
48
|
-
Object.defineProperty(themeState.ref, 'current', {
|
|
49
|
-
value: originalRef,
|
|
50
|
-
writable: true,
|
|
51
|
-
configurable: true
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
Object.defineProperty(themeState.ref, 'current', {
|
|
56
|
-
value: element,
|
|
57
|
-
writable: true,
|
|
58
|
-
configurable: true
|
|
59
|
-
});
|
|
60
|
-
await themeState.switchTheme(theme);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
const contextValue = {
|
|
64
|
-
theme: themeState.theme,
|
|
65
|
-
colorTheme: themeState.colorTheme,
|
|
66
|
-
resolvedTheme: themeState.resolvedTheme,
|
|
67
|
-
setTheme: themeState.setTheme,
|
|
68
|
-
setColorTheme: themeState.setColorTheme,
|
|
69
|
-
toggleTheme: themeState.toggleTheme,
|
|
70
|
-
switchTheme: themeState.switchTheme,
|
|
71
|
-
switchThemeFromElement,
|
|
72
|
-
ref: themeState.ref,
|
|
73
|
-
};
|
|
74
|
-
return (_jsx(SpacemanThemeContext.Provider, { value: contextValue, children: children }));
|
|
75
|
-
};
|
|
76
|
-
/**
|
|
77
|
-
* Hook to consume the Spaceman Theme context
|
|
78
|
-
*
|
|
79
|
-
* @returns Theme context value with all theme state and methods
|
|
80
|
-
* @throws Error if used outside of SpacemanThemeProvider
|
|
81
|
-
*/
|
|
82
|
-
export const useSpacemanTheme = () => {
|
|
83
|
-
const context = useContext(SpacemanThemeContext);
|
|
84
|
-
if (context === undefined) {
|
|
85
|
-
throw new Error('useSpacemanTheme must be used within a SpacemanThemeProvider');
|
|
86
|
-
}
|
|
87
|
-
return context;
|
|
88
|
-
};
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useThemeAnimation } from '../hooks/use-theme-animation';
|
|
3
|
-
import { useSpacemanTheme } from './SpacemanThemeProvider';
|
|
4
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
|
5
|
-
export const ThemeSelector = ({ themes = ['light', 'dark', 'system'], colorThemes = ['default'], currentColorTheme, onColorThemeChange, animationType, duration, }) => {
|
|
6
|
-
// Try to use SpacemanTheme context if available (controlled mode)
|
|
7
|
-
let contextTheme = null;
|
|
8
|
-
try {
|
|
9
|
-
contextTheme = useSpacemanTheme();
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
// Context not available, use standalone mode
|
|
13
|
-
}
|
|
14
|
-
// Use context if available, otherwise fall back to standalone hook
|
|
15
|
-
const standaloneHook = useThemeAnimation({
|
|
16
|
-
animationType,
|
|
17
|
-
duration,
|
|
18
|
-
themes,
|
|
19
|
-
colorThemes,
|
|
20
|
-
...(currentColorTheme !== undefined && { colorTheme: currentColorTheme }),
|
|
21
|
-
onColorThemeChange,
|
|
22
|
-
});
|
|
23
|
-
const isControlled = contextTheme !== null;
|
|
24
|
-
const { colorTheme, setColorTheme } = isControlled ?
|
|
25
|
-
{ colorTheme: contextTheme.colorTheme, setColorTheme: contextTheme.setColorTheme } :
|
|
26
|
-
standaloneHook;
|
|
27
|
-
const handleColorThemeChange = (newColorTheme) => {
|
|
28
|
-
setColorTheme(newColorTheme);
|
|
29
|
-
// Also call the callback if provided
|
|
30
|
-
if (onColorThemeChange) {
|
|
31
|
-
onColorThemeChange(newColorTheme);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
return (_jsx(_Fragment, { children: colorThemes.length > 1 && (_jsx("div", { className: "flex flex-col gap-2", children: _jsxs(Select, { value: colorTheme, onValueChange: (v) => handleColorThemeChange(v), children: [_jsx(SelectTrigger, { className: "capitalize", children: _jsx(SelectValue, { placeholder: "Choose a color theme" }) }), _jsx(SelectContent, { children: colorThemes.map(theme => (_jsx(SelectItem, { className: "capitalize", value: theme, children: theme }, theme))) })] }) })) }));
|
|
35
|
-
};
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
3
|
-
import { motion } from 'motion/react';
|
|
4
|
-
import { clsx } from 'clsx';
|
|
5
|
-
import { useThemeAnimation } from '../hooks/use-theme-animation';
|
|
6
|
-
import { useSpacemanTheme } from './SpacemanThemeProvider';
|
|
7
|
-
const SunIcon = () => (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("circle", { cx: "12", cy: "12", r: "5" }), _jsx("path", { d: "M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" })] }));
|
|
8
|
-
const MoonIcon = () => (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" }) }));
|
|
9
|
-
const MonitorIcon = () => (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { width: "20", height: "14", x: "2", y: "3", rx: "2" }), _jsx("line", { x1: "8", x2: "16", y1: "21", y2: "21" }), _jsx("line", { x1: "12", x2: "12", y1: "17", y2: "21" })] }));
|
|
10
|
-
const ThemeOption = ({ icon, value, isActive, isHovered, onClick, onMouseEnter, onMouseLeave, buttonRef, }) => {
|
|
11
|
-
return (_jsxs("button", { ref: isActive ? buttonRef : undefined, className: clsx('relative flex h-9 w-12 cursor-pointer items-center justify-center transition-all duration-200 ease-in-out', 'text-muted-foreground hover:text-foreground', isActive && 'text-foreground font-medium'), role: "radio", "aria-checked": isActive, "aria-label": `Switch to ${value} theme`, onClick: (event) => onClick(value, event), onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, style: {
|
|
12
|
-
borderRadius: 'var(--radius)',
|
|
13
|
-
}, children: [isHovered && (_jsx(motion.div, { layoutId: "theme-hover-bg", className: "absolute inset-0 bg-muted backdrop-blur-sm", style: {
|
|
14
|
-
borderRadius: 'var(--radius)',
|
|
15
|
-
}, transition: {
|
|
16
|
-
type: 'spring',
|
|
17
|
-
bounce: 0,
|
|
18
|
-
stiffness: 100,
|
|
19
|
-
damping: 10,
|
|
20
|
-
duration: 0.3,
|
|
21
|
-
} })), _jsx(motion.div, { className: "relative z-10 flex items-center justify-center", animate: {
|
|
22
|
-
scale: isActive ? 1.1 : 1,
|
|
23
|
-
}, transition: {
|
|
24
|
-
type: 'spring',
|
|
25
|
-
bounce: 0.2,
|
|
26
|
-
duration: 0.4,
|
|
27
|
-
}, children: _jsx("div", { className: "[&_svg]:size-4", children: icon }) }), isActive && (_jsx(motion.div, { layoutId: "theme-active-indicator", className: "absolute bottom-0 left-1/2 h-0.5 w-6 -translate-x-1/2 rounded-full", style: {
|
|
28
|
-
backgroundColor: 'hsl(var(--primary))',
|
|
29
|
-
}, transition: {
|
|
30
|
-
type: 'spring',
|
|
31
|
-
bounce: 0.3,
|
|
32
|
-
duration: 0.6,
|
|
33
|
-
} }))] }));
|
|
34
|
-
};
|
|
35
|
-
const THEME_OPTIONS = [
|
|
36
|
-
{
|
|
37
|
-
icon: _jsx(MonitorIcon, {}),
|
|
38
|
-
value: 'system',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
icon: _jsx(SunIcon, {}),
|
|
42
|
-
value: 'light',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
icon: _jsx(MoonIcon, {}),
|
|
46
|
-
value: 'dark',
|
|
47
|
-
},
|
|
48
|
-
];
|
|
49
|
-
export const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentTheme, onThemeChange, animationType, duration, className, }) => {
|
|
50
|
-
// Try to use SpacemanTheme context if available (controlled mode)
|
|
51
|
-
let contextTheme = null;
|
|
52
|
-
try {
|
|
53
|
-
contextTheme = useSpacemanTheme();
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
// Context not available, use standalone mode
|
|
57
|
-
}
|
|
58
|
-
// Use context if available, otherwise fall back to standalone hook
|
|
59
|
-
const standaloneHook = useThemeAnimation({
|
|
60
|
-
animationType,
|
|
61
|
-
duration,
|
|
62
|
-
themes,
|
|
63
|
-
...(currentTheme !== undefined && { theme: currentTheme }),
|
|
64
|
-
onThemeChange,
|
|
65
|
-
});
|
|
66
|
-
const isControlled = contextTheme !== null;
|
|
67
|
-
const { ref, theme, switchTheme } = isControlled ?
|
|
68
|
-
{ ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme } :
|
|
69
|
-
standaloneHook;
|
|
70
|
-
const [isMounted, setIsMounted] = useState(false);
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
setIsMounted(true);
|
|
73
|
-
}, []);
|
|
74
|
-
const handleThemeChange = async (newTheme, event) => {
|
|
75
|
-
if (isControlled && contextTheme.switchThemeFromElement && event) {
|
|
76
|
-
// Use controlled mode with animation from clicked element
|
|
77
|
-
await contextTheme.switchThemeFromElement(newTheme, event.currentTarget);
|
|
78
|
-
// Also call the callback if provided
|
|
79
|
-
if (onThemeChange) {
|
|
80
|
-
onThemeChange(newTheme);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
// Use standalone mode
|
|
85
|
-
await switchTheme(newTheme);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
if (!isMounted) {
|
|
89
|
-
return _jsx("div", { className: "flex h-9 w-fit" });
|
|
90
|
-
}
|
|
91
|
-
const filteredOptions = THEME_OPTIONS.filter(option => themes.includes(option.value));
|
|
92
|
-
return (_jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.3 }, className: clsx('inline-flex items-center overflow-hidden bg-background shadow-sm border border-border', 'transition-all duration-200 ease-in-out', className), style: {
|
|
93
|
-
borderRadius: 'var(--radius)',
|
|
94
|
-
backgroundColor: 'hsl(var(--background))',
|
|
95
|
-
}, role: "radiogroup", children: filteredOptions.map(option => (_jsx(ThemeOption, { icon: option.icon, value: option.value, isActive: theme === option.value, isHovered: false, onClick: handleThemeChange, onMouseEnter: () => { }, onMouseLeave: () => { }, buttonRef: theme === option.value ? ref : undefined }, option.value))) }, String(isMounted)));
|
|
96
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
3
|
-
declare function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
|
|
4
|
-
declare function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>): import("react/jsx-runtime").JSX.Element;
|
|
5
|
-
declare function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>): import("react/jsx-runtime").JSX.Element;
|
|
6
|
-
declare function SelectTrigger({ className, size, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
7
|
-
size?: "sm" | "default";
|
|
8
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
9
|
-
declare function SelectContent({ className, children, position, ...props }: React.ComponentProps<typeof SelectPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
|
|
10
|
-
declare function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>): import("react/jsx-runtime").JSX.Element;
|
|
11
|
-
declare function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>): import("react/jsx-runtime").JSX.Element;
|
|
12
|
-
declare function SelectSeparator({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>): import("react/jsx-runtime").JSX.Element;
|
|
13
|
-
declare function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>): import("react/jsx-runtime").JSX.Element;
|
|
14
|
-
declare function SelectScrollDownButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>): import("react/jsx-runtime").JSX.Element;
|
|
15
|
-
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, };
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
3
|
-
import { clsx } from "clsx";
|
|
4
|
-
function Select({ ...props }) {
|
|
5
|
-
return _jsx(SelectPrimitive.Root, { "data-slot": "select", ...props });
|
|
6
|
-
}
|
|
7
|
-
function SelectGroup({ ...props }) {
|
|
8
|
-
return _jsx(SelectPrimitive.Group, { "data-slot": "select-group", ...props });
|
|
9
|
-
}
|
|
10
|
-
function SelectValue({ ...props }) {
|
|
11
|
-
return _jsx(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
|
|
12
|
-
}
|
|
13
|
-
const ChevronDownIcon = () => (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m6 9 6 6 6-6" }) }));
|
|
14
|
-
const ChevronUpIcon = () => (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m18 15-6-6-6 6" }) }));
|
|
15
|
-
const CheckIcon = () => (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) }));
|
|
16
|
-
function SelectTrigger({ className, size = "default", children, ...props }) {
|
|
17
|
-
return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: clsx("border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDownIcon, {}) })] }));
|
|
18
|
-
}
|
|
19
|
-
function SelectContent({ className, children, position = "popper", ...props }) {
|
|
20
|
-
return (_jsx(SelectPrimitive.Portal, { children: _jsx(SelectPrimitive.Content, { "data-slot": "select-content", className: clsx("bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md", position === "popper" &&
|
|
21
|
-
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className), position: position, ...props, children: _jsx(SelectPrimitive.Viewport, { className: clsx("p-1", position === "popper" &&
|
|
22
|
-
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"), children: children }) }) }));
|
|
23
|
-
}
|
|
24
|
-
function SelectLabel({ className, ...props }) {
|
|
25
|
-
return (_jsx(SelectPrimitive.Label, { "data-slot": "select-label", className: clsx("py-1.5 pl-8 pr-2 text-sm font-semibold", className), ...props }));
|
|
26
|
-
}
|
|
27
|
-
function SelectItem({ className, children, ...props }) {
|
|
28
|
-
return (_jsxs(SelectPrimitive.Item, { "data-slot": "select-item", className: clsx("focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(CheckIcon, {}) }) }), _jsx(SelectPrimitive.ItemText, { children: children })] }));
|
|
29
|
-
}
|
|
30
|
-
function SelectSeparator({ className, ...props }) {
|
|
31
|
-
return (_jsx(SelectPrimitive.Separator, { "data-slot": "select-separator", className: clsx("-mx-1 my-1 h-px bg-muted", className), ...props }));
|
|
32
|
-
}
|
|
33
|
-
function SelectScrollUpButton({ className, ...props }) {
|
|
34
|
-
return (_jsx(SelectPrimitive.ScrollUpButton, { "data-slot": "select-scroll-up-button", className: clsx("flex cursor-default items-center justify-center py-1", className), ...props, children: _jsx(ChevronUpIcon, {}) }));
|
|
35
|
-
}
|
|
36
|
-
function SelectScrollDownButton({ className, ...props }) {
|
|
37
|
-
return (_jsx(SelectPrimitive.ScrollDownButton, { "data-slot": "select-scroll-down-button", className: clsx("flex cursor-default items-center justify-center py-1", className), ...props, children: _jsx(ChevronDownIcon, {}) }));
|
|
38
|
-
}
|
|
39
|
-
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, };
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
-
import { flushSync } from 'react-dom';
|
|
3
|
-
import { ThemeAnimationType, } from '../types';
|
|
4
|
-
import { injectBaseStyles, resolveTheme, getSystemTheme, supportsViewTransitions, prefersReducedMotion, createCircleAnimation, createBlurCircleAnimation, } from '../utils/animations';
|
|
5
|
-
const isBrowser = typeof window !== 'undefined';
|
|
6
|
-
export const useThemeAnimation = (props = {}) => {
|
|
7
|
-
const { duration: propsDuration = 750, easing = 'ease-in-out', animationType = ThemeAnimationType.CIRCLE, blurAmount = 2, styleId = 'spaceman-theme-style', themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', globalClassName = 'dark', colorThemePrefix = 'theme-', storageKey = 'theme', colorStorageKey = 'color-theme', theme: externalTheme, colorTheme: externalColorTheme, onThemeChange, onColorThemeChange, } = props;
|
|
8
|
-
const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
|
|
9
|
-
const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
injectBaseStyles();
|
|
12
|
-
}, []);
|
|
13
|
-
const [internalTheme, setInternalTheme] = useState(() => {
|
|
14
|
-
if (!isBrowser)
|
|
15
|
-
return defaultTheme;
|
|
16
|
-
const saved = localStorage.getItem(storageKey);
|
|
17
|
-
return saved && themes.indexOf(saved) !== -1 ? saved : defaultTheme;
|
|
18
|
-
});
|
|
19
|
-
const [internalColorTheme, setInternalColorTheme] = useState(() => {
|
|
20
|
-
if (!isBrowser)
|
|
21
|
-
return defaultColorTheme;
|
|
22
|
-
const saved = localStorage.getItem(colorStorageKey);
|
|
23
|
-
return saved && colorThemes.indexOf(saved) !== -1 ? saved : defaultColorTheme;
|
|
24
|
-
});
|
|
25
|
-
const currentTheme = externalTheme ?? internalTheme;
|
|
26
|
-
const currentColorTheme = externalColorTheme ?? internalColorTheme;
|
|
27
|
-
const [, setSystemTheme] = useState(() => getSystemTheme());
|
|
28
|
-
const resolvedTheme = resolveTheme(currentTheme);
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
if (!isBrowser)
|
|
31
|
-
return;
|
|
32
|
-
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
33
|
-
const handleChange = () => setSystemTheme(getSystemTheme());
|
|
34
|
-
mediaQuery.addEventListener('change', handleChange);
|
|
35
|
-
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
36
|
-
}, []);
|
|
37
|
-
// Apply theme classes to DOM
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
if (!isBrowser)
|
|
40
|
-
return;
|
|
41
|
-
// Apply theme class (light/dark) - use resolvedTheme instead of manual resolution
|
|
42
|
-
if (resolvedTheme === 'dark') {
|
|
43
|
-
document.documentElement.classList.add(globalClassName);
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
document.documentElement.classList.remove(globalClassName);
|
|
47
|
-
}
|
|
48
|
-
// Apply color theme classes
|
|
49
|
-
// Remove all existing color theme classes
|
|
50
|
-
colorThemes.forEach(theme => {
|
|
51
|
-
document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
|
|
52
|
-
});
|
|
53
|
-
// Add current color theme class
|
|
54
|
-
document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
55
|
-
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
56
|
-
const ref = useRef(null);
|
|
57
|
-
const setTheme = useCallback((newTheme) => {
|
|
58
|
-
// Always save to localStorage
|
|
59
|
-
if (isBrowser) {
|
|
60
|
-
localStorage.setItem(storageKey, newTheme);
|
|
61
|
-
}
|
|
62
|
-
// Update internal state if no external control
|
|
63
|
-
if (externalTheme === undefined) {
|
|
64
|
-
setInternalTheme(newTheme);
|
|
65
|
-
}
|
|
66
|
-
// Call external callback if provided
|
|
67
|
-
if (onThemeChange) {
|
|
68
|
-
onThemeChange(newTheme);
|
|
69
|
-
}
|
|
70
|
-
}, [onThemeChange, externalTheme, storageKey]);
|
|
71
|
-
const setColorTheme = useCallback((newColorTheme) => {
|
|
72
|
-
// Always save to localStorage
|
|
73
|
-
if (isBrowser) {
|
|
74
|
-
localStorage.setItem(colorStorageKey, newColorTheme);
|
|
75
|
-
}
|
|
76
|
-
// Update internal state if no external control
|
|
77
|
-
if (externalColorTheme === undefined) {
|
|
78
|
-
setInternalColorTheme(newColorTheme);
|
|
79
|
-
}
|
|
80
|
-
// Call external callback if provided
|
|
81
|
-
if (onColorThemeChange) {
|
|
82
|
-
onColorThemeChange(newColorTheme);
|
|
83
|
-
}
|
|
84
|
-
}, [onColorThemeChange, externalColorTheme, colorStorageKey]);
|
|
85
|
-
const switchTheme = useCallback(async (newTheme) => {
|
|
86
|
-
if (!ref.current || !supportsViewTransitions() || prefersReducedMotion()) {
|
|
87
|
-
setTheme(newTheme);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const { top, left, width, height } = ref.current.getBoundingClientRect();
|
|
91
|
-
const x = left + width / 2;
|
|
92
|
-
const y = top + height / 2;
|
|
93
|
-
const animationConfig = {
|
|
94
|
-
x,
|
|
95
|
-
y,
|
|
96
|
-
duration,
|
|
97
|
-
easing,
|
|
98
|
-
animationType,
|
|
99
|
-
blurAmount,
|
|
100
|
-
styleId,
|
|
101
|
-
};
|
|
102
|
-
if (animationType === ThemeAnimationType.BLUR_CIRCLE) {
|
|
103
|
-
createBlurCircleAnimation(animationConfig);
|
|
104
|
-
}
|
|
105
|
-
await document.startViewTransition(() => {
|
|
106
|
-
flushSync(() => {
|
|
107
|
-
setTheme(newTheme);
|
|
108
|
-
});
|
|
109
|
-
}).ready;
|
|
110
|
-
if (animationType === ThemeAnimationType.CIRCLE) {
|
|
111
|
-
createCircleAnimation(animationConfig);
|
|
112
|
-
}
|
|
113
|
-
}, [setTheme, duration, easing, animationType, blurAmount, styleId]);
|
|
114
|
-
const toggleTheme = useCallback(async () => {
|
|
115
|
-
const newTheme = resolvedTheme === 'dark' ? 'light' : 'dark';
|
|
116
|
-
await switchTheme(newTheme);
|
|
117
|
-
}, [resolvedTheme, switchTheme]);
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
if (!isBrowser)
|
|
120
|
-
return;
|
|
121
|
-
const root = document.documentElement;
|
|
122
|
-
if (resolvedTheme === 'dark') {
|
|
123
|
-
root.classList.add(globalClassName);
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
root.classList.remove(globalClassName);
|
|
127
|
-
}
|
|
128
|
-
colorThemes.forEach(theme => {
|
|
129
|
-
root.classList.remove(`${colorThemePrefix}${theme}`);
|
|
130
|
-
});
|
|
131
|
-
if (currentColorTheme !== 'default') {
|
|
132
|
-
root.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
133
|
-
}
|
|
134
|
-
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
135
|
-
return {
|
|
136
|
-
ref,
|
|
137
|
-
theme: currentTheme,
|
|
138
|
-
colorTheme: currentColorTheme,
|
|
139
|
-
resolvedTheme,
|
|
140
|
-
setTheme,
|
|
141
|
-
setColorTheme,
|
|
142
|
-
toggleTheme,
|
|
143
|
-
switchTheme,
|
|
144
|
-
};
|
|
145
|
-
};
|
package/dist/index.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { useThemeAnimation } from './hooks/use-theme-animation';
|
|
2
|
-
export { ThemeSwitcher } from './components/ThemeSwitcher';
|
|
3
|
-
export { ThemeSelector } from './components/ThemeSelector';
|
|
4
|
-
export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
|
|
5
|
-
export type { UseThemeAnimationProps, UseThemeAnimationReturn, ThemeSwitcherProps, ThemeSelectorProps, Theme, ColorTheme, } from './types';
|
|
6
|
-
export { ThemeAnimationType } from './types';
|
|
7
|
-
export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
|
package/dist/index.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { useThemeAnimation } from './hooks/use-theme-animation';
|
|
2
|
-
export { ThemeSwitcher } from './components/ThemeSwitcher';
|
|
3
|
-
export { ThemeSelector } from './components/ThemeSelector';
|
|
4
|
-
export { SpacemanThemeProvider, useSpacemanTheme } from './components/SpacemanThemeProvider';
|
|
5
|
-
export { ThemeAnimationType } from './types';
|
|
6
|
-
export { injectBaseStyles, getSystemTheme, resolveTheme, supportsViewTransitions, prefersReducedMotion, } from './utils/animations';
|
package/dist/types/index.d.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { RefObject } from 'react';
|
|
2
|
-
export declare enum ThemeAnimationType {
|
|
3
|
-
CIRCLE = "circle",
|
|
4
|
-
BLUR_CIRCLE = "blur-circle"
|
|
5
|
-
}
|
|
6
|
-
export type Theme = 'light' | 'dark' | 'system';
|
|
7
|
-
export type ColorTheme = string;
|
|
8
|
-
export interface UseThemeAnimationProps {
|
|
9
|
-
duration?: number;
|
|
10
|
-
easing?: string;
|
|
11
|
-
animationType?: ThemeAnimationType;
|
|
12
|
-
blurAmount?: number;
|
|
13
|
-
styleId?: string;
|
|
14
|
-
themes?: Theme[];
|
|
15
|
-
colorThemes?: ColorTheme[];
|
|
16
|
-
defaultTheme?: Theme;
|
|
17
|
-
defaultColorTheme?: ColorTheme;
|
|
18
|
-
globalClassName?: string;
|
|
19
|
-
colorThemePrefix?: string;
|
|
20
|
-
storageKey?: string;
|
|
21
|
-
colorStorageKey?: string;
|
|
22
|
-
theme?: Theme;
|
|
23
|
-
colorTheme?: ColorTheme;
|
|
24
|
-
onThemeChange?: (theme: Theme) => void;
|
|
25
|
-
onColorThemeChange?: (colorTheme: ColorTheme) => void;
|
|
26
|
-
}
|
|
27
|
-
export interface UseThemeAnimationReturn {
|
|
28
|
-
ref: RefObject<HTMLButtonElement | null>;
|
|
29
|
-
theme: Theme;
|
|
30
|
-
colorTheme: ColorTheme;
|
|
31
|
-
resolvedTheme: 'light' | 'dark';
|
|
32
|
-
setTheme: (theme: Theme) => void;
|
|
33
|
-
setColorTheme: (colorTheme: ColorTheme) => void;
|
|
34
|
-
toggleTheme: () => Promise<void>;
|
|
35
|
-
switchTheme: (theme: Theme) => Promise<void>;
|
|
36
|
-
}
|
|
37
|
-
export interface ThemeSwitcherProps {
|
|
38
|
-
themes?: Theme[];
|
|
39
|
-
currentTheme?: Theme;
|
|
40
|
-
onThemeChange?: (theme: Theme) => void;
|
|
41
|
-
animationType?: ThemeAnimationType;
|
|
42
|
-
duration?: number;
|
|
43
|
-
className?: string;
|
|
44
|
-
size?: 'sm' | 'md' | 'lg';
|
|
45
|
-
variant?: 'default' | 'outline' | 'ghost';
|
|
46
|
-
icons?: {
|
|
47
|
-
light?: React.ReactNode;
|
|
48
|
-
dark?: React.ReactNode;
|
|
49
|
-
system?: React.ReactNode;
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
export interface ThemeSelectorProps {
|
|
53
|
-
themes?: Theme[];
|
|
54
|
-
colorThemes?: ColorTheme[];
|
|
55
|
-
currentTheme?: Theme;
|
|
56
|
-
currentColorTheme?: ColorTheme;
|
|
57
|
-
onThemeChange?: (theme: Theme) => void;
|
|
58
|
-
onColorThemeChange?: (colorTheme: ColorTheme) => void;
|
|
59
|
-
animationType?: ThemeAnimationType;
|
|
60
|
-
duration?: number;
|
|
61
|
-
className?: string;
|
|
62
|
-
placeholder?: string;
|
|
63
|
-
themeLabel?: string;
|
|
64
|
-
colorThemeLabel?: string;
|
|
65
|
-
}
|
package/dist/types/index.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { ThemeAnimationType } from '../types';
|
|
2
|
-
export declare const injectBaseStyles: () => void;
|
|
3
|
-
export declare const createBlurCircleMask: (blur: number) => string;
|
|
4
|
-
export declare const getSystemTheme: () => "light" | "dark";
|
|
5
|
-
export declare const resolveTheme: (theme: string) => "light" | "dark";
|
|
6
|
-
export declare const supportsViewTransitions: () => boolean;
|
|
7
|
-
export declare const prefersReducedMotion: () => boolean;
|
|
8
|
-
export interface AnimationConfig {
|
|
9
|
-
x: number;
|
|
10
|
-
y: number;
|
|
11
|
-
duration: number;
|
|
12
|
-
easing: string;
|
|
13
|
-
animationType: ThemeAnimationType;
|
|
14
|
-
blurAmount: number;
|
|
15
|
-
styleId: string;
|
|
16
|
-
}
|
|
17
|
-
export declare const createCircleAnimation: (config: AnimationConfig) => void;
|
|
18
|
-
export declare const createBlurCircleAnimation: (config: AnimationConfig) => void;
|
package/dist/utils/animations.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
const isBrowser = typeof window !== 'undefined';
|
|
2
|
-
export const injectBaseStyles = () => {
|
|
3
|
-
if (isBrowser) {
|
|
4
|
-
const styleId = 'spaceman-theme-base-style';
|
|
5
|
-
if (!document.getElementById(styleId)) {
|
|
6
|
-
const style = document.createElement('style');
|
|
7
|
-
style.id = styleId;
|
|
8
|
-
const isHighResolution = window.innerWidth >= 3000 || window.innerHeight >= 2000;
|
|
9
|
-
style.textContent = `
|
|
10
|
-
::view-transition-old(root),
|
|
11
|
-
::view-transition-new(root) {
|
|
12
|
-
animation: none;
|
|
13
|
-
mix-blend-mode: normal;
|
|
14
|
-
${isHighResolution ? 'transform: translateZ(0);' : ''}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
${isHighResolution
|
|
18
|
-
? `
|
|
19
|
-
::view-transition-group(root),
|
|
20
|
-
::view-transition-image-pair(root),
|
|
21
|
-
::view-transition-old(root),
|
|
22
|
-
::view-transition-new(root) {
|
|
23
|
-
backface-visibility: hidden;
|
|
24
|
-
perspective: 1000px;
|
|
25
|
-
transform: translate3d(0, 0, 0);
|
|
26
|
-
}
|
|
27
|
-
`
|
|
28
|
-
: ''}
|
|
29
|
-
`;
|
|
30
|
-
document.head.appendChild(style);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
export const createBlurCircleMask = (blur) => {
|
|
35
|
-
const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
|
|
36
|
-
const blurFilter = `<filter id="blur"><feGaussianBlur stdDeviation="${blur}" /></filter>`;
|
|
37
|
-
const circleRadius = isHighResolution ? 20 : 25;
|
|
38
|
-
return `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 100 100"><defs>${blurFilter}</defs><circle cx="0" cy="0" r="${circleRadius}" fill="white" filter="url(%23blur)"/></svg>')`;
|
|
39
|
-
};
|
|
40
|
-
export const getSystemTheme = () => {
|
|
41
|
-
if (!isBrowser)
|
|
42
|
-
return 'light';
|
|
43
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
44
|
-
};
|
|
45
|
-
export const resolveTheme = (theme) => {
|
|
46
|
-
if (theme === 'system') {
|
|
47
|
-
return getSystemTheme();
|
|
48
|
-
}
|
|
49
|
-
return theme === 'dark' ? 'dark' : 'light';
|
|
50
|
-
};
|
|
51
|
-
export const supportsViewTransitions = () => {
|
|
52
|
-
return isBrowser && 'startViewTransition' in document;
|
|
53
|
-
};
|
|
54
|
-
export const prefersReducedMotion = () => {
|
|
55
|
-
return isBrowser && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
56
|
-
};
|
|
57
|
-
export const createCircleAnimation = (config) => {
|
|
58
|
-
const { x, y, duration, easing } = config;
|
|
59
|
-
// Calculate the distance to each corner of the viewport
|
|
60
|
-
const topLeft = Math.hypot(x, y);
|
|
61
|
-
const topRight = Math.hypot(window.innerWidth - x, y);
|
|
62
|
-
const bottomLeft = Math.hypot(x, window.innerHeight - y);
|
|
63
|
-
const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y);
|
|
64
|
-
// Find the maximum distance to ensure animation covers the entire viewport
|
|
65
|
-
const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight);
|
|
66
|
-
document.documentElement.animate({
|
|
67
|
-
clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${maxRadius}px at ${x}px ${y}px)`],
|
|
68
|
-
}, {
|
|
69
|
-
duration,
|
|
70
|
-
easing,
|
|
71
|
-
pseudoElement: '::view-transition-new(root)',
|
|
72
|
-
});
|
|
73
|
-
};
|
|
74
|
-
export const createBlurCircleAnimation = (config) => {
|
|
75
|
-
const { x, y, duration, easing, blurAmount, styleId } = config;
|
|
76
|
-
const existingStyle = document.getElementById(styleId);
|
|
77
|
-
if (existingStyle) {
|
|
78
|
-
existingStyle.remove();
|
|
79
|
-
}
|
|
80
|
-
const viewportSize = Math.max(window.innerWidth, window.innerHeight) + 200;
|
|
81
|
-
const isHighResolution = window.innerWidth >= 3000 || window.innerHeight >= 2000;
|
|
82
|
-
const scaleFactor = isHighResolution ? 2.5 : 4;
|
|
83
|
-
const optimalMaskSize = isHighResolution
|
|
84
|
-
? Math.min(viewportSize * scaleFactor, 5000)
|
|
85
|
-
: viewportSize * scaleFactor;
|
|
86
|
-
const topLeft = Math.hypot(x, y);
|
|
87
|
-
const topRight = Math.hypot(window.innerWidth - x, y);
|
|
88
|
-
const bottomLeft = Math.hypot(x, window.innerHeight - y);
|
|
89
|
-
const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y);
|
|
90
|
-
const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight);
|
|
91
|
-
const styleElement = document.createElement('style');
|
|
92
|
-
styleElement.id = styleId;
|
|
93
|
-
const blurFactor = isHighResolution ? 1.5 : 1.2;
|
|
94
|
-
const finalMaskSize = Math.max(optimalMaskSize, maxRadius * 2.5);
|
|
95
|
-
styleElement.textContent = `
|
|
96
|
-
::view-transition-group(root) {
|
|
97
|
-
animation-duration: ${duration}ms;
|
|
98
|
-
animation-timing-function: ${isHighResolution
|
|
99
|
-
? 'cubic-bezier(0.2, 0, 0.2, 1)'
|
|
100
|
-
: 'linear(' +
|
|
101
|
-
'0 0%, 0.2342 12.49%, 0.4374 24.99%,' +
|
|
102
|
-
'0.6093 37.49%, 0.6835 43.74%,' +
|
|
103
|
-
'0.7499 49.99%, 0.8086 56.25%,' +
|
|
104
|
-
'0.8593 62.5%, 0.9023 68.75%, 0.9375 75%,' +
|
|
105
|
-
'0.9648 81.25%, 0.9844 87.5%,' +
|
|
106
|
-
'0.9961 93.75%, 1 100%' +
|
|
107
|
-
')'};
|
|
108
|
-
will-change: transform;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
::view-transition-new(root) {
|
|
112
|
-
mask: ${createBlurCircleMask(blurAmount * blurFactor)} 0 0 / 100% 100% no-repeat;
|
|
113
|
-
mask-position: ${x}px ${y}px;
|
|
114
|
-
animation: spaceman-mask-scale ${duration}ms ${easing};
|
|
115
|
-
transform-origin: ${x}px ${y}px;
|
|
116
|
-
will-change: mask-size, mask-position;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
::view-transition-old(root),
|
|
120
|
-
.dark::view-transition-old(root) {
|
|
121
|
-
animation: spaceman-mask-scale ${duration}ms ${easing};
|
|
122
|
-
transform-origin: ${x}px ${y}px;
|
|
123
|
-
z-index: -1;
|
|
124
|
-
will-change: mask-size, mask-position;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
@keyframes spaceman-mask-scale {
|
|
128
|
-
0% {
|
|
129
|
-
mask-size: 0px;
|
|
130
|
-
mask-position: ${x}px ${y}px;
|
|
131
|
-
}
|
|
132
|
-
100% {
|
|
133
|
-
mask-size: ${finalMaskSize}px;
|
|
134
|
-
mask-position: ${x - finalMaskSize / 2}px ${y - finalMaskSize / 2}px;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
`;
|
|
138
|
-
document.head.appendChild(styleElement);
|
|
139
|
-
setTimeout(() => {
|
|
140
|
-
const styleElement = document.getElementById(styleId);
|
|
141
|
-
if (styleElement) {
|
|
142
|
-
styleElement.remove();
|
|
143
|
-
}
|
|
144
|
-
}, duration);
|
|
145
|
-
};
|