@space-man/react-theme-animation 1.0.1 → 1.0.3
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 +46 -46
- package/dist/cjs/components/SpacemanThemeProvider.js +21 -13
- package/dist/cjs/components/ThemeSelector.js +3 -3
- package/dist/cjs/components/ThemeSwitcher.js +4 -4
- package/dist/cjs/components/ui/select.d.ts +3 -3
- package/dist/cjs/components/ui/select.js +9 -9
- package/dist/cjs/hooks/use-theme-animation.js +26 -17
- package/dist/esm/components/SpacemanThemeProvider.js +22 -14
- package/dist/esm/components/ThemeSelector.js +3 -3
- package/dist/esm/components/ThemeSwitcher.js +4 -4
- package/dist/esm/components/ui/select.d.ts +3 -3
- package/dist/esm/components/ui/select.js +11 -11
- package/dist/esm/hooks/use-theme-animation.js +26 -17
- package/package.json +14 -5
package/README.md
CHANGED
|
@@ -43,16 +43,16 @@ Perfect for custom theme toggle buttons and complete control over animated theme
|
|
|
43
43
|
### Basic Theme Toggle
|
|
44
44
|
|
|
45
45
|
```tsx
|
|
46
|
-
import { useThemeAnimation } from '@space-man/react-theme-animation'
|
|
46
|
+
import { useThemeAnimation } from '@space-man/react-theme-animation'
|
|
47
47
|
|
|
48
48
|
function ThemeToggle() {
|
|
49
|
-
const { theme, toggleTheme, ref } = useThemeAnimation()
|
|
49
|
+
const { theme, toggleTheme, ref } = useThemeAnimation()
|
|
50
50
|
|
|
51
51
|
return (
|
|
52
52
|
<button ref={ref} onClick={toggleTheme} className="theme-toggle-btn">
|
|
53
53
|
{theme === 'light' ? '🌙' : '🌞'} {theme}
|
|
54
54
|
</button>
|
|
55
|
-
)
|
|
55
|
+
)
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
SpacemanThemeProvider,
|
|
70
70
|
ThemeSwitcher,
|
|
71
71
|
ThemeSelector,
|
|
72
|
-
} from '@space-man/react-theme-animation'
|
|
72
|
+
} from '@space-man/react-theme-animation'
|
|
73
73
|
|
|
74
74
|
function App() {
|
|
75
75
|
return (
|
|
@@ -88,7 +88,7 @@ function App() {
|
|
|
88
88
|
</main>
|
|
89
89
|
</div>
|
|
90
90
|
</SpacemanThemeProvider>
|
|
91
|
-
)
|
|
91
|
+
)
|
|
92
92
|
}
|
|
93
93
|
```
|
|
94
94
|
|
|
@@ -135,7 +135,7 @@ Hook to access theme state from SpacemanThemeProvider context.
|
|
|
135
135
|
|
|
136
136
|
```tsx
|
|
137
137
|
const { theme, colorTheme, switchTheme, setColorTheme, switchThemeFromElement, ref } =
|
|
138
|
-
useSpacemanTheme()
|
|
138
|
+
useSpacemanTheme()
|
|
139
139
|
```
|
|
140
140
|
|
|
141
141
|
#### Returns
|
|
@@ -161,7 +161,7 @@ const { theme, colorTheme, switchTheme, setColorTheme, toggleTheme, ref } = useT
|
|
|
161
161
|
duration: 800,
|
|
162
162
|
onThemeChange: theme => console.log('Theme changed:', theme),
|
|
163
163
|
onColorThemeChange: colorTheme => console.log('Color changed:', colorTheme),
|
|
164
|
-
})
|
|
164
|
+
})
|
|
165
165
|
```
|
|
166
166
|
|
|
167
167
|
#### Options
|
|
@@ -258,59 +258,59 @@ enum ThemeAnimationType {
|
|
|
258
258
|
### Types
|
|
259
259
|
|
|
260
260
|
```tsx
|
|
261
|
-
type Theme = 'light' | 'dark' | 'system'
|
|
262
|
-
type ColorTheme = string
|
|
261
|
+
type Theme = 'light' | 'dark' | 'system'
|
|
262
|
+
type ColorTheme = string // e.g., 'default', 'blue', 'green', etc.
|
|
263
263
|
|
|
264
264
|
interface SpacemanThemeProviderProps {
|
|
265
|
-
children: React.ReactNode
|
|
266
|
-
defaultTheme?: Theme
|
|
267
|
-
defaultColorTheme?: ColorTheme
|
|
268
|
-
themes?: Theme[]
|
|
269
|
-
colorThemes?: ColorTheme[]
|
|
270
|
-
animationType?: ThemeAnimationType
|
|
271
|
-
duration?: number
|
|
272
|
-
blurAmount?: number
|
|
273
|
-
onThemeChange?: (theme: Theme) => void
|
|
274
|
-
onColorThemeChange?: (colorTheme: ColorTheme) => void
|
|
265
|
+
children: React.ReactNode
|
|
266
|
+
defaultTheme?: Theme
|
|
267
|
+
defaultColorTheme?: ColorTheme
|
|
268
|
+
themes?: Theme[]
|
|
269
|
+
colorThemes?: ColorTheme[]
|
|
270
|
+
animationType?: ThemeAnimationType
|
|
271
|
+
duration?: number
|
|
272
|
+
blurAmount?: number
|
|
273
|
+
onThemeChange?: (theme: Theme) => void
|
|
274
|
+
onColorThemeChange?: (colorTheme: ColorTheme) => void
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
interface UseThemeAnimationProps {
|
|
278
|
-
themes?: Theme[]
|
|
279
|
-
colorThemes?: ColorTheme[]
|
|
280
|
-
theme?: Theme
|
|
281
|
-
colorTheme?: ColorTheme
|
|
282
|
-
animationType?: ThemeAnimationType
|
|
283
|
-
duration?: number
|
|
284
|
-
blurAmount?: number
|
|
285
|
-
onThemeChange?: (theme: Theme) => void
|
|
286
|
-
onColorThemeChange?: (colorTheme: ColorTheme) => void
|
|
278
|
+
themes?: Theme[]
|
|
279
|
+
colorThemes?: ColorTheme[]
|
|
280
|
+
theme?: Theme
|
|
281
|
+
colorTheme?: ColorTheme
|
|
282
|
+
animationType?: ThemeAnimationType
|
|
283
|
+
duration?: number
|
|
284
|
+
blurAmount?: number
|
|
285
|
+
onThemeChange?: (theme: Theme) => void
|
|
286
|
+
onColorThemeChange?: (colorTheme: ColorTheme) => void
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
interface UseThemeAnimationReturn {
|
|
290
|
-
theme: Theme
|
|
291
|
-
colorTheme: ColorTheme
|
|
292
|
-
switchTheme: (theme: Theme) => Promise<void
|
|
293
|
-
setColorTheme: (colorTheme: ColorTheme) => void
|
|
294
|
-
toggleTheme: () => Promise<void
|
|
295
|
-
ref: RefObject<HTMLElement
|
|
290
|
+
theme: Theme
|
|
291
|
+
colorTheme: ColorTheme
|
|
292
|
+
switchTheme: (theme: Theme) => Promise<void>
|
|
293
|
+
setColorTheme: (colorTheme: ColorTheme) => void
|
|
294
|
+
toggleTheme: () => Promise<void>
|
|
295
|
+
ref: RefObject<HTMLElement>
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
interface ThemeSwitcherProps {
|
|
299
|
-
themes?: Theme[]
|
|
300
|
-
currentTheme?: Theme
|
|
301
|
-
onThemeChange?: (theme: Theme) => void
|
|
302
|
-
animationType?: ThemeAnimationType
|
|
303
|
-
duration?: number
|
|
304
|
-
className?: string
|
|
299
|
+
themes?: Theme[]
|
|
300
|
+
currentTheme?: Theme
|
|
301
|
+
onThemeChange?: (theme: Theme) => void
|
|
302
|
+
animationType?: ThemeAnimationType
|
|
303
|
+
duration?: number
|
|
304
|
+
className?: string
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
interface ThemeSelectorProps {
|
|
308
|
-
themes?: Theme[]
|
|
309
|
-
colorThemes?: ColorTheme[]
|
|
310
|
-
currentColorTheme?: ColorTheme
|
|
311
|
-
onColorThemeChange?: (colorTheme: ColorTheme) => void
|
|
312
|
-
animationType?: ThemeAnimationType
|
|
313
|
-
duration?: number
|
|
308
|
+
themes?: Theme[]
|
|
309
|
+
colorThemes?: ColorTheme[]
|
|
310
|
+
currentColorTheme?: ColorTheme
|
|
311
|
+
onColorThemeChange?: (colorTheme: ColorTheme) => void
|
|
312
|
+
animationType?: ThemeAnimationType
|
|
313
|
+
duration?: number
|
|
314
314
|
}
|
|
315
315
|
```
|
|
316
316
|
|
|
@@ -21,7 +21,7 @@ const SpacemanThemeContext = (0, react_1.createContext)(undefined);
|
|
|
21
21
|
* @param duration - Animation duration in milliseconds (default: 750)
|
|
22
22
|
*/
|
|
23
23
|
const SpacemanThemeProvider = ({ children, themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', animationType = types_1.ThemeAnimationType.CIRCLE, duration = 750, }) => {
|
|
24
|
-
|
|
24
|
+
const [mounted, setMounted] = (0, react_1.useState)(false);
|
|
25
25
|
const themeState = (0, use_theme_animation_1.useThemeAnimation)({
|
|
26
26
|
themes,
|
|
27
27
|
colorThemes,
|
|
@@ -30,39 +30,47 @@ const SpacemanThemeProvider = ({ children, themes = ['light', 'dark', 'system'],
|
|
|
30
30
|
animationType,
|
|
31
31
|
duration,
|
|
32
32
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* @param theme - Theme to switch to
|
|
37
|
-
* @param element - HTML button element to use as animation origin
|
|
38
|
-
*/
|
|
33
|
+
(0, react_1.useEffect)(() => {
|
|
34
|
+
setMounted(true);
|
|
35
|
+
}, []);
|
|
39
36
|
const switchThemeFromElement = async (theme, element) => {
|
|
40
|
-
// Temporarily set the ref to the clicked element
|
|
41
37
|
if (themeState.ref.current) {
|
|
42
38
|
const originalRef = themeState.ref.current;
|
|
43
|
-
// Use Object.defineProperty to temporarily override the ref
|
|
44
39
|
Object.defineProperty(themeState.ref, 'current', {
|
|
45
40
|
value: element,
|
|
46
41
|
writable: true,
|
|
47
|
-
configurable: true
|
|
42
|
+
configurable: true,
|
|
48
43
|
});
|
|
49
44
|
await themeState.switchTheme(theme);
|
|
50
|
-
// Restore the original ref after animation
|
|
51
45
|
Object.defineProperty(themeState.ref, 'current', {
|
|
52
46
|
value: originalRef,
|
|
53
47
|
writable: true,
|
|
54
|
-
configurable: true
|
|
48
|
+
configurable: true,
|
|
55
49
|
});
|
|
56
50
|
}
|
|
57
51
|
else {
|
|
58
52
|
Object.defineProperty(themeState.ref, 'current', {
|
|
59
53
|
value: element,
|
|
60
54
|
writable: true,
|
|
61
|
-
configurable: true
|
|
55
|
+
configurable: true,
|
|
62
56
|
});
|
|
63
57
|
await themeState.switchTheme(theme);
|
|
64
58
|
}
|
|
65
59
|
};
|
|
60
|
+
if (!mounted) {
|
|
61
|
+
const loadingContextValue = {
|
|
62
|
+
theme: defaultTheme,
|
|
63
|
+
colorTheme: defaultColorTheme,
|
|
64
|
+
resolvedTheme: defaultTheme === 'dark' ? 'dark' : 'light',
|
|
65
|
+
setTheme: () => { },
|
|
66
|
+
setColorTheme: () => { },
|
|
67
|
+
toggleTheme: async () => { },
|
|
68
|
+
switchTheme: async () => { },
|
|
69
|
+
switchThemeFromElement: async () => { },
|
|
70
|
+
ref: { current: null },
|
|
71
|
+
};
|
|
72
|
+
return ((0, jsx_runtime_1.jsx)(SpacemanThemeContext.Provider, { value: loadingContextValue, children: children }));
|
|
73
|
+
}
|
|
66
74
|
const contextValue = {
|
|
67
75
|
theme: themeState.theme,
|
|
68
76
|
colorTheme: themeState.colorTheme,
|
|
@@ -24,9 +24,9 @@ const ThemeSelector = ({ themes = ['light', 'dark', 'system'], colorThemes = ['d
|
|
|
24
24
|
onColorThemeChange,
|
|
25
25
|
});
|
|
26
26
|
const isControlled = contextTheme !== null;
|
|
27
|
-
const { colorTheme, setColorTheme } = isControlled
|
|
28
|
-
{ colorTheme: contextTheme.colorTheme, setColorTheme: contextTheme.setColorTheme }
|
|
29
|
-
standaloneHook;
|
|
27
|
+
const { colorTheme, setColorTheme } = isControlled
|
|
28
|
+
? { colorTheme: contextTheme.colorTheme, setColorTheme: contextTheme.setColorTheme }
|
|
29
|
+
: standaloneHook;
|
|
30
30
|
const handleColorThemeChange = (newColorTheme) => {
|
|
31
31
|
setColorTheme(newColorTheme);
|
|
32
32
|
// Also call the callback if provided
|
|
@@ -11,7 +11,7 @@ const SunIcon = () => ((0, jsx_runtime_1.jsxs)("svg", { width: "16", height: "16
|
|
|
11
11
|
const MoonIcon = () => ((0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" }) }));
|
|
12
12
|
const MonitorIcon = () => ((0, jsx_runtime_1.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)("rect", { width: "20", height: "14", x: "2", y: "3", rx: "2" }), (0, jsx_runtime_1.jsx)("line", { x1: "8", x2: "16", y1: "21", y2: "21" }), (0, jsx_runtime_1.jsx)("line", { x1: "12", x2: "12", y1: "17", y2: "21" })] }));
|
|
13
13
|
const ThemeOption = ({ icon, value, isActive, isHovered, onClick, onMouseEnter, onMouseLeave, buttonRef, }) => {
|
|
14
|
-
return ((0, jsx_runtime_1.jsxs)("button", { ref: isActive ? buttonRef : undefined, className: (0, clsx_1.clsx)('relative flex h-9 w-12 cursor-pointer items-center justify-center transition-all duration-200 ease-in-out', 'text-muted-foreground hover:text-foreground', isActive && 'text-foreground font-medium'), role: "radio", "aria-checked": isActive, "aria-label": `Switch to ${value} theme`, onClick:
|
|
14
|
+
return ((0, jsx_runtime_1.jsxs)("button", { ref: isActive ? buttonRef : undefined, className: (0, clsx_1.clsx)('relative flex h-9 w-12 cursor-pointer items-center justify-center transition-all duration-200 ease-in-out', 'text-muted-foreground hover:text-foreground', isActive && 'text-foreground font-medium'), role: "radio", "aria-checked": isActive, "aria-label": `Switch to ${value} theme`, onClick: event => onClick(value, event), onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, style: {
|
|
15
15
|
borderRadius: 'var(--radius)',
|
|
16
16
|
}, children: [isHovered && ((0, jsx_runtime_1.jsx)(react_2.motion.div, { layoutId: "theme-hover-bg", className: "absolute inset-0 bg-muted backdrop-blur-sm", style: {
|
|
17
17
|
borderRadius: 'var(--radius)',
|
|
@@ -67,9 +67,9 @@ const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentTheme, onT
|
|
|
67
67
|
onThemeChange,
|
|
68
68
|
});
|
|
69
69
|
const isControlled = contextTheme !== null;
|
|
70
|
-
const { ref, theme, switchTheme } = isControlled
|
|
71
|
-
{ ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme }
|
|
72
|
-
standaloneHook;
|
|
70
|
+
const { ref, theme, switchTheme } = isControlled
|
|
71
|
+
? { ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme }
|
|
72
|
+
: standaloneHook;
|
|
73
73
|
const [isMounted, setIsMounted] = (0, react_1.useState)(false);
|
|
74
74
|
(0, react_1.useEffect)(() => {
|
|
75
75
|
setIsMounted(true);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import * as SelectPrimitive from
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
3
3
|
declare function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
declare function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>): import("react/jsx-runtime").JSX.Element;
|
|
5
5
|
declare function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>): import("react/jsx-runtime").JSX.Element;
|
|
6
6
|
declare function SelectTrigger({ className, size, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
7
|
-
size?:
|
|
7
|
+
size?: 'sm' | 'default';
|
|
8
8
|
}): import("react/jsx-runtime").JSX.Element;
|
|
9
9
|
declare function SelectContent({ className, children, position, ...props }: React.ComponentProps<typeof SelectPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
|
|
10
10
|
declare function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -58,26 +58,26 @@ function SelectValue({ ...props }) {
|
|
|
58
58
|
const ChevronDownIcon = () => ((0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("path", { d: "m6 9 6 6 6-6" }) }));
|
|
59
59
|
const ChevronUpIcon = () => ((0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("path", { d: "m18 15-6-6-6 6" }) }));
|
|
60
60
|
const CheckIcon = () => ((0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: (0, jsx_runtime_1.jsx)("path", { d: "M20 6 9 17l-5-5" }) }));
|
|
61
|
-
function SelectTrigger({ className, size =
|
|
61
|
+
function SelectTrigger({ className, size = 'default', children, ...props }) {
|
|
62
62
|
return ((0, jsx_runtime_1.jsxs)(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: (0, clsx_1.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, (0, jsx_runtime_1.jsx)(SelectPrimitive.Icon, { asChild: true, children: (0, jsx_runtime_1.jsx)(ChevronDownIcon, {}) })] }));
|
|
63
63
|
}
|
|
64
|
-
function SelectContent({ className, children, position =
|
|
65
|
-
return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Portal, { children: (0, jsx_runtime_1.jsx)(SelectPrimitive.Content, { "data-slot": "select-content", className: (0, clsx_1.clsx)(
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
function SelectContent({ className, children, position = 'popper', ...props }) {
|
|
65
|
+
return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Portal, { children: (0, jsx_runtime_1.jsx)(SelectPrimitive.Content, { "data-slot": "select-content", className: (0, clsx_1.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' &&
|
|
66
|
+
'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: (0, jsx_runtime_1.jsx)(SelectPrimitive.Viewport, { className: (0, clsx_1.clsx)('p-1', position === 'popper' &&
|
|
67
|
+
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'), children: children }) }) }));
|
|
68
68
|
}
|
|
69
69
|
function SelectLabel({ className, ...props }) {
|
|
70
|
-
return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Label, { "data-slot": "select-label", className: (0, clsx_1.clsx)(
|
|
70
|
+
return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Label, { "data-slot": "select-label", className: (0, clsx_1.clsx)('py-1.5 pl-8 pr-2 text-sm font-semibold', className), ...props }));
|
|
71
71
|
}
|
|
72
72
|
function SelectItem({ className, children, ...props }) {
|
|
73
73
|
return ((0, jsx_runtime_1.jsxs)(SelectPrimitive.Item, { "data-slot": "select-item", className: (0, clsx_1.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: [(0, jsx_runtime_1.jsx)("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: (0, jsx_runtime_1.jsx)(SelectPrimitive.ItemIndicator, { children: (0, jsx_runtime_1.jsx)(CheckIcon, {}) }) }), (0, jsx_runtime_1.jsx)(SelectPrimitive.ItemText, { children: children })] }));
|
|
74
74
|
}
|
|
75
75
|
function SelectSeparator({ className, ...props }) {
|
|
76
|
-
return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Separator, { "data-slot": "select-separator", className: (0, clsx_1.clsx)(
|
|
76
|
+
return ((0, jsx_runtime_1.jsx)(SelectPrimitive.Separator, { "data-slot": "select-separator", className: (0, clsx_1.clsx)('-mx-1 my-1 h-px bg-muted', className), ...props }));
|
|
77
77
|
}
|
|
78
78
|
function SelectScrollUpButton({ className, ...props }) {
|
|
79
|
-
return ((0, jsx_runtime_1.jsx)(SelectPrimitive.ScrollUpButton, { "data-slot": "select-scroll-up-button", className: (0, clsx_1.clsx)(
|
|
79
|
+
return ((0, jsx_runtime_1.jsx)(SelectPrimitive.ScrollUpButton, { "data-slot": "select-scroll-up-button", className: (0, clsx_1.clsx)('flex cursor-default items-center justify-center py-1', className), ...props, children: (0, jsx_runtime_1.jsx)(ChevronUpIcon, {}) }));
|
|
80
80
|
}
|
|
81
81
|
function SelectScrollDownButton({ className, ...props }) {
|
|
82
|
-
return ((0, jsx_runtime_1.jsx)(SelectPrimitive.ScrollDownButton, { "data-slot": "select-scroll-down-button", className: (0, clsx_1.clsx)(
|
|
82
|
+
return ((0, jsx_runtime_1.jsx)(SelectPrimitive.ScrollDownButton, { "data-slot": "select-scroll-down-button", className: (0, clsx_1.clsx)('flex cursor-default items-center justify-center py-1', className), ...props, children: (0, jsx_runtime_1.jsx)(ChevronDownIcon, {}) }));
|
|
83
83
|
}
|
|
@@ -10,21 +10,35 @@ const useThemeAnimation = (props = {}) => {
|
|
|
10
10
|
const { duration: propsDuration = 750, easing = 'ease-in-out', animationType = types_1.ThemeAnimationType.CIRCLE, blurAmount = 2, styleId = 'spaceman-theme-style', themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', globalClassName = 'dark', colorThemePrefix = 'theme-', storageKey = 'theme', colorStorageKey = 'color-theme', theme: externalTheme, colorTheme: externalColorTheme, onThemeChange, onColorThemeChange, } = props;
|
|
11
11
|
const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
|
|
12
12
|
const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
|
|
13
|
+
const [mounted, setMounted] = (0, react_1.useState)(false);
|
|
13
14
|
(0, react_1.useEffect)(() => {
|
|
14
15
|
(0, animations_1.injectBaseStyles)();
|
|
15
16
|
}, []);
|
|
16
17
|
const [internalTheme, setInternalTheme] = (0, react_1.useState)(() => {
|
|
17
18
|
if (!isBrowser)
|
|
18
19
|
return defaultTheme;
|
|
19
|
-
|
|
20
|
-
return saved && themes.indexOf(saved) !== -1 ? saved : defaultTheme;
|
|
20
|
+
return defaultTheme;
|
|
21
21
|
});
|
|
22
22
|
const [internalColorTheme, setInternalColorTheme] = (0, react_1.useState)(() => {
|
|
23
23
|
if (!isBrowser)
|
|
24
24
|
return defaultColorTheme;
|
|
25
|
-
|
|
26
|
-
return saved && colorThemes.indexOf(saved) !== -1 ? saved : defaultColorTheme;
|
|
25
|
+
return defaultColorTheme;
|
|
27
26
|
});
|
|
27
|
+
(0, react_1.useEffect)(() => {
|
|
28
|
+
setMounted(true);
|
|
29
|
+
if (isBrowser && !externalTheme) {
|
|
30
|
+
const saved = localStorage.getItem(storageKey);
|
|
31
|
+
if (saved && themes.indexOf(saved) !== -1) {
|
|
32
|
+
setInternalTheme(saved);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (isBrowser && !externalColorTheme) {
|
|
36
|
+
const saved = localStorage.getItem(colorStorageKey);
|
|
37
|
+
if (saved && colorThemes.indexOf(saved) !== -1) {
|
|
38
|
+
setInternalColorTheme(saved);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
28
42
|
const currentTheme = externalTheme ?? internalTheme;
|
|
29
43
|
const currentColorTheme = externalColorTheme ?? internalColorTheme;
|
|
30
44
|
const [, setSystemTheme] = (0, react_1.useState)(() => (0, animations_1.getSystemTheme)());
|
|
@@ -39,23 +53,19 @@ const useThemeAnimation = (props = {}) => {
|
|
|
39
53
|
}, []);
|
|
40
54
|
// Apply theme classes to DOM
|
|
41
55
|
(0, react_1.useEffect)(() => {
|
|
42
|
-
if (!isBrowser)
|
|
56
|
+
if (!isBrowser || !mounted)
|
|
43
57
|
return;
|
|
44
|
-
// Apply theme class (light/dark) - use resolvedTheme instead of manual resolution
|
|
45
58
|
if (resolvedTheme === 'dark') {
|
|
46
59
|
document.documentElement.classList.add(globalClassName);
|
|
47
60
|
}
|
|
48
61
|
else {
|
|
49
62
|
document.documentElement.classList.remove(globalClassName);
|
|
50
63
|
}
|
|
51
|
-
// Apply color theme classes
|
|
52
|
-
// Remove all existing color theme classes
|
|
53
64
|
colorThemes.forEach(theme => {
|
|
54
65
|
document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
|
|
55
66
|
});
|
|
56
|
-
// Add current color theme class
|
|
57
67
|
document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
58
|
-
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
68
|
+
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes, mounted]);
|
|
59
69
|
const ref = (0, react_1.useRef)(null);
|
|
60
70
|
const setTheme = (0, react_1.useCallback)((newTheme) => {
|
|
61
71
|
// Always save to localStorage
|
|
@@ -119,22 +129,21 @@ const useThemeAnimation = (props = {}) => {
|
|
|
119
129
|
await switchTheme(newTheme);
|
|
120
130
|
}, [resolvedTheme, switchTheme]);
|
|
121
131
|
(0, react_1.useEffect)(() => {
|
|
122
|
-
if (!isBrowser)
|
|
132
|
+
if (!isBrowser || !mounted)
|
|
123
133
|
return;
|
|
124
|
-
const root = document.documentElement;
|
|
125
134
|
if (resolvedTheme === 'dark') {
|
|
126
|
-
|
|
135
|
+
document.documentElement.classList.add(globalClassName);
|
|
127
136
|
}
|
|
128
137
|
else {
|
|
129
|
-
|
|
138
|
+
document.documentElement.classList.remove(globalClassName);
|
|
130
139
|
}
|
|
131
140
|
colorThemes.forEach(theme => {
|
|
132
|
-
|
|
141
|
+
document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
|
|
133
142
|
});
|
|
134
143
|
if (currentColorTheme !== 'default') {
|
|
135
|
-
|
|
144
|
+
document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
136
145
|
}
|
|
137
|
-
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
146
|
+
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes, mounted]);
|
|
138
147
|
return {
|
|
139
148
|
ref,
|
|
140
149
|
theme: currentTheme,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext } from 'react';
|
|
2
|
+
import { createContext, useContext, useState, useEffect } from 'react';
|
|
3
3
|
import { useThemeAnimation } from '../hooks/use-theme-animation';
|
|
4
4
|
import { ThemeAnimationType } from '../types';
|
|
5
5
|
const SpacemanThemeContext = createContext(undefined);
|
|
@@ -18,7 +18,7 @@ const SpacemanThemeContext = createContext(undefined);
|
|
|
18
18
|
* @param duration - Animation duration in milliseconds (default: 750)
|
|
19
19
|
*/
|
|
20
20
|
export const SpacemanThemeProvider = ({ children, themes = ['light', 'dark', 'system'], colorThemes = ['default'], defaultTheme = 'system', defaultColorTheme = 'default', animationType = ThemeAnimationType.CIRCLE, duration = 750, }) => {
|
|
21
|
-
|
|
21
|
+
const [mounted, setMounted] = useState(false);
|
|
22
22
|
const themeState = useThemeAnimation({
|
|
23
23
|
themes,
|
|
24
24
|
colorThemes,
|
|
@@ -27,39 +27,47 @@ export const SpacemanThemeProvider = ({ children, themes = ['light', 'dark', 'sy
|
|
|
27
27
|
animationType,
|
|
28
28
|
duration,
|
|
29
29
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* @param theme - Theme to switch to
|
|
34
|
-
* @param element - HTML button element to use as animation origin
|
|
35
|
-
*/
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setMounted(true);
|
|
32
|
+
}, []);
|
|
36
33
|
const switchThemeFromElement = async (theme, element) => {
|
|
37
|
-
// Temporarily set the ref to the clicked element
|
|
38
34
|
if (themeState.ref.current) {
|
|
39
35
|
const originalRef = themeState.ref.current;
|
|
40
|
-
// Use Object.defineProperty to temporarily override the ref
|
|
41
36
|
Object.defineProperty(themeState.ref, 'current', {
|
|
42
37
|
value: element,
|
|
43
38
|
writable: true,
|
|
44
|
-
configurable: true
|
|
39
|
+
configurable: true,
|
|
45
40
|
});
|
|
46
41
|
await themeState.switchTheme(theme);
|
|
47
|
-
// Restore the original ref after animation
|
|
48
42
|
Object.defineProperty(themeState.ref, 'current', {
|
|
49
43
|
value: originalRef,
|
|
50
44
|
writable: true,
|
|
51
|
-
configurable: true
|
|
45
|
+
configurable: true,
|
|
52
46
|
});
|
|
53
47
|
}
|
|
54
48
|
else {
|
|
55
49
|
Object.defineProperty(themeState.ref, 'current', {
|
|
56
50
|
value: element,
|
|
57
51
|
writable: true,
|
|
58
|
-
configurable: true
|
|
52
|
+
configurable: true,
|
|
59
53
|
});
|
|
60
54
|
await themeState.switchTheme(theme);
|
|
61
55
|
}
|
|
62
56
|
};
|
|
57
|
+
if (!mounted) {
|
|
58
|
+
const loadingContextValue = {
|
|
59
|
+
theme: defaultTheme,
|
|
60
|
+
colorTheme: defaultColorTheme,
|
|
61
|
+
resolvedTheme: defaultTheme === 'dark' ? 'dark' : 'light',
|
|
62
|
+
setTheme: () => { },
|
|
63
|
+
setColorTheme: () => { },
|
|
64
|
+
toggleTheme: async () => { },
|
|
65
|
+
switchTheme: async () => { },
|
|
66
|
+
switchThemeFromElement: async () => { },
|
|
67
|
+
ref: { current: null },
|
|
68
|
+
};
|
|
69
|
+
return (_jsx(SpacemanThemeContext.Provider, { value: loadingContextValue, children: children }));
|
|
70
|
+
}
|
|
63
71
|
const contextValue = {
|
|
64
72
|
theme: themeState.theme,
|
|
65
73
|
colorTheme: themeState.colorTheme,
|
|
@@ -21,9 +21,9 @@ export const ThemeSelector = ({ themes = ['light', 'dark', 'system'], colorTheme
|
|
|
21
21
|
onColorThemeChange,
|
|
22
22
|
});
|
|
23
23
|
const isControlled = contextTheme !== null;
|
|
24
|
-
const { colorTheme, setColorTheme } = isControlled
|
|
25
|
-
{ colorTheme: contextTheme.colorTheme, setColorTheme: contextTheme.setColorTheme }
|
|
26
|
-
standaloneHook;
|
|
24
|
+
const { colorTheme, setColorTheme } = isControlled
|
|
25
|
+
? { colorTheme: contextTheme.colorTheme, setColorTheme: contextTheme.setColorTheme }
|
|
26
|
+
: standaloneHook;
|
|
27
27
|
const handleColorThemeChange = (newColorTheme) => {
|
|
28
28
|
setColorTheme(newColorTheme);
|
|
29
29
|
// Also call the callback if provided
|
|
@@ -8,7 +8,7 @@ const SunIcon = () => (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 2
|
|
|
8
8
|
const MoonIcon = () => (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" }) }));
|
|
9
9
|
const MonitorIcon = () => (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { width: "20", height: "14", x: "2", y: "3", rx: "2" }), _jsx("line", { x1: "8", x2: "16", y1: "21", y2: "21" }), _jsx("line", { x1: "12", x2: "12", y1: "17", y2: "21" })] }));
|
|
10
10
|
const ThemeOption = ({ icon, value, isActive, isHovered, onClick, onMouseEnter, onMouseLeave, buttonRef, }) => {
|
|
11
|
-
return (_jsxs("button", { ref: isActive ? buttonRef : undefined, className: clsx('relative flex h-9 w-12 cursor-pointer items-center justify-center transition-all duration-200 ease-in-out', 'text-muted-foreground hover:text-foreground', isActive && 'text-foreground font-medium'), role: "radio", "aria-checked": isActive, "aria-label": `Switch to ${value} theme`, onClick:
|
|
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
12
|
borderRadius: 'var(--radius)',
|
|
13
13
|
}, children: [isHovered && (_jsx(motion.div, { layoutId: "theme-hover-bg", className: "absolute inset-0 bg-muted backdrop-blur-sm", style: {
|
|
14
14
|
borderRadius: 'var(--radius)',
|
|
@@ -64,9 +64,9 @@ export const ThemeSwitcher = ({ themes = ['light', 'dark', 'system'], currentThe
|
|
|
64
64
|
onThemeChange,
|
|
65
65
|
});
|
|
66
66
|
const isControlled = contextTheme !== null;
|
|
67
|
-
const { ref, theme, switchTheme } = isControlled
|
|
68
|
-
{ ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme }
|
|
69
|
-
standaloneHook;
|
|
67
|
+
const { ref, theme, switchTheme } = isControlled
|
|
68
|
+
? { ref: contextTheme.ref, theme: contextTheme.theme, switchTheme: contextTheme.switchTheme }
|
|
69
|
+
: standaloneHook;
|
|
70
70
|
const [isMounted, setIsMounted] = useState(false);
|
|
71
71
|
useEffect(() => {
|
|
72
72
|
setIsMounted(true);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import * as SelectPrimitive from
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
3
3
|
declare function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
declare function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>): import("react/jsx-runtime").JSX.Element;
|
|
5
5
|
declare function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>): import("react/jsx-runtime").JSX.Element;
|
|
6
6
|
declare function SelectTrigger({ className, size, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
7
|
-
size?:
|
|
7
|
+
size?: 'sm' | 'default';
|
|
8
8
|
}): import("react/jsx-runtime").JSX.Element;
|
|
9
9
|
declare function SelectContent({ className, children, position, ...props }: React.ComponentProps<typeof SelectPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
|
|
10
10
|
declare function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import * as SelectPrimitive from
|
|
3
|
-
import { clsx } from
|
|
2
|
+
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
3
|
+
import { clsx } from 'clsx';
|
|
4
4
|
function Select({ ...props }) {
|
|
5
5
|
return _jsx(SelectPrimitive.Root, { "data-slot": "select", ...props });
|
|
6
6
|
}
|
|
@@ -13,27 +13,27 @@ function SelectValue({ ...props }) {
|
|
|
13
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
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
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 =
|
|
16
|
+
function SelectTrigger({ className, size = 'default', children, ...props }) {
|
|
17
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
18
|
}
|
|
19
|
-
function SelectContent({ className, children, position =
|
|
20
|
-
return (_jsx(SelectPrimitive.Portal, { children: _jsx(SelectPrimitive.Content, { "data-slot": "select-content", className: clsx(
|
|
21
|
-
|
|
22
|
-
|
|
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
23
|
}
|
|
24
24
|
function SelectLabel({ className, ...props }) {
|
|
25
|
-
return (_jsx(SelectPrimitive.Label, { "data-slot": "select-label", className: clsx(
|
|
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
26
|
}
|
|
27
27
|
function SelectItem({ className, children, ...props }) {
|
|
28
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
29
|
}
|
|
30
30
|
function SelectSeparator({ className, ...props }) {
|
|
31
|
-
return (_jsx(SelectPrimitive.Separator, { "data-slot": "select-separator", className: clsx(
|
|
31
|
+
return (_jsx(SelectPrimitive.Separator, { "data-slot": "select-separator", className: clsx('-mx-1 my-1 h-px bg-muted', className), ...props }));
|
|
32
32
|
}
|
|
33
33
|
function SelectScrollUpButton({ className, ...props }) {
|
|
34
|
-
return (_jsx(SelectPrimitive.ScrollUpButton, { "data-slot": "select-scroll-up-button", className: clsx(
|
|
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
35
|
}
|
|
36
36
|
function SelectScrollDownButton({ className, ...props }) {
|
|
37
|
-
return (_jsx(SelectPrimitive.ScrollDownButton, { "data-slot": "select-scroll-down-button", className: clsx(
|
|
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
38
|
}
|
|
39
39
|
export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, };
|
|
@@ -7,21 +7,35 @@ export const useThemeAnimation = (props = {}) => {
|
|
|
7
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
8
|
const isHighResolution = isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000);
|
|
9
9
|
const duration = isHighResolution ? Math.max(propsDuration * 0.8, 500) : propsDuration;
|
|
10
|
+
const [mounted, setMounted] = useState(false);
|
|
10
11
|
useEffect(() => {
|
|
11
12
|
injectBaseStyles();
|
|
12
13
|
}, []);
|
|
13
14
|
const [internalTheme, setInternalTheme] = useState(() => {
|
|
14
15
|
if (!isBrowser)
|
|
15
16
|
return defaultTheme;
|
|
16
|
-
|
|
17
|
-
return saved && themes.indexOf(saved) !== -1 ? saved : defaultTheme;
|
|
17
|
+
return defaultTheme;
|
|
18
18
|
});
|
|
19
19
|
const [internalColorTheme, setInternalColorTheme] = useState(() => {
|
|
20
20
|
if (!isBrowser)
|
|
21
21
|
return defaultColorTheme;
|
|
22
|
-
|
|
23
|
-
return saved && colorThemes.indexOf(saved) !== -1 ? saved : defaultColorTheme;
|
|
22
|
+
return defaultColorTheme;
|
|
24
23
|
});
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setMounted(true);
|
|
26
|
+
if (isBrowser && !externalTheme) {
|
|
27
|
+
const saved = localStorage.getItem(storageKey);
|
|
28
|
+
if (saved && themes.indexOf(saved) !== -1) {
|
|
29
|
+
setInternalTheme(saved);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (isBrowser && !externalColorTheme) {
|
|
33
|
+
const saved = localStorage.getItem(colorStorageKey);
|
|
34
|
+
if (saved && colorThemes.indexOf(saved) !== -1) {
|
|
35
|
+
setInternalColorTheme(saved);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
25
39
|
const currentTheme = externalTheme ?? internalTheme;
|
|
26
40
|
const currentColorTheme = externalColorTheme ?? internalColorTheme;
|
|
27
41
|
const [, setSystemTheme] = useState(() => getSystemTheme());
|
|
@@ -36,23 +50,19 @@ export const useThemeAnimation = (props = {}) => {
|
|
|
36
50
|
}, []);
|
|
37
51
|
// Apply theme classes to DOM
|
|
38
52
|
useEffect(() => {
|
|
39
|
-
if (!isBrowser)
|
|
53
|
+
if (!isBrowser || !mounted)
|
|
40
54
|
return;
|
|
41
|
-
// Apply theme class (light/dark) - use resolvedTheme instead of manual resolution
|
|
42
55
|
if (resolvedTheme === 'dark') {
|
|
43
56
|
document.documentElement.classList.add(globalClassName);
|
|
44
57
|
}
|
|
45
58
|
else {
|
|
46
59
|
document.documentElement.classList.remove(globalClassName);
|
|
47
60
|
}
|
|
48
|
-
// Apply color theme classes
|
|
49
|
-
// Remove all existing color theme classes
|
|
50
61
|
colorThemes.forEach(theme => {
|
|
51
62
|
document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
|
|
52
63
|
});
|
|
53
|
-
// Add current color theme class
|
|
54
64
|
document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
55
|
-
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
65
|
+
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes, mounted]);
|
|
56
66
|
const ref = useRef(null);
|
|
57
67
|
const setTheme = useCallback((newTheme) => {
|
|
58
68
|
// Always save to localStorage
|
|
@@ -116,22 +126,21 @@ export const useThemeAnimation = (props = {}) => {
|
|
|
116
126
|
await switchTheme(newTheme);
|
|
117
127
|
}, [resolvedTheme, switchTheme]);
|
|
118
128
|
useEffect(() => {
|
|
119
|
-
if (!isBrowser)
|
|
129
|
+
if (!isBrowser || !mounted)
|
|
120
130
|
return;
|
|
121
|
-
const root = document.documentElement;
|
|
122
131
|
if (resolvedTheme === 'dark') {
|
|
123
|
-
|
|
132
|
+
document.documentElement.classList.add(globalClassName);
|
|
124
133
|
}
|
|
125
134
|
else {
|
|
126
|
-
|
|
135
|
+
document.documentElement.classList.remove(globalClassName);
|
|
127
136
|
}
|
|
128
137
|
colorThemes.forEach(theme => {
|
|
129
|
-
|
|
138
|
+
document.documentElement.classList.remove(`${colorThemePrefix}${theme}`);
|
|
130
139
|
});
|
|
131
140
|
if (currentColorTheme !== 'default') {
|
|
132
|
-
|
|
141
|
+
document.documentElement.classList.add(`${colorThemePrefix}${currentColorTheme}`);
|
|
133
142
|
}
|
|
134
|
-
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes]);
|
|
143
|
+
}, [resolvedTheme, currentColorTheme, globalClassName, colorThemePrefix, colorThemes, mounted]);
|
|
135
144
|
return {
|
|
136
145
|
ref,
|
|
137
146
|
theme: currentTheme,
|
package/package.json
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@space-man/react-theme-animation",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "React theme switching with smooth view transition animations and multi-theme support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
7
|
-
"reactjs",
|
|
8
|
-
"hooks",
|
|
9
7
|
"theme",
|
|
8
|
+
"dark mode",
|
|
9
|
+
"light mode",
|
|
10
|
+
"theme switching",
|
|
11
|
+
"view transitions",
|
|
12
|
+
"hooks",
|
|
10
13
|
"animation",
|
|
11
14
|
"switch",
|
|
12
15
|
"typescript",
|
|
13
|
-
"nextjs",
|
|
14
16
|
"dark",
|
|
15
17
|
"light",
|
|
16
18
|
"view-transition",
|
|
19
|
+
"view-transition-api",
|
|
20
|
+
"tailwind",
|
|
17
21
|
"tailwindcss",
|
|
18
|
-
"shadcn"
|
|
22
|
+
"shadcn",
|
|
23
|
+
"reactjs",
|
|
24
|
+
"nextjs",
|
|
25
|
+
"vite"
|
|
19
26
|
],
|
|
20
27
|
"publishConfig": {
|
|
21
28
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -50,6 +57,8 @@
|
|
|
50
57
|
"lint": "eslint \"**/*.{js,jsx,ts,tsx}\"",
|
|
51
58
|
"lint:fix": "eslint --fix . --ext .{js,jsx,ts,tsx}",
|
|
52
59
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
60
|
+
"prettier:check": "prettier --check . --config ./.prettierrc",
|
|
61
|
+
"prettier:fix": "prettier --write . --config ./.prettierrc",
|
|
53
62
|
"changeset": "changeset add --type patch",
|
|
54
63
|
"changeset:patch": "changeset add --type patch",
|
|
55
64
|
"changeset:feature": "changeset add --type minor",
|