@space-man/react-theme-animation 1.0.0 → 1.0.2

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.
@@ -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,3 +0,0 @@
1
- import React from 'react';
2
- import { ThemeSelectorProps } from '../types';
3
- export declare const ThemeSelector: React.FC<ThemeSelectorProps>;
@@ -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,3 +0,0 @@
1
- import React from 'react';
2
- import { ThemeSwitcherProps } from '../types';
3
- export declare const ThemeSwitcher: React.FC<ThemeSwitcherProps>;
@@ -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,2 +0,0 @@
1
- import { UseThemeAnimationProps, UseThemeAnimationReturn } from '../types';
2
- export declare const useThemeAnimation: (props?: UseThemeAnimationProps) => UseThemeAnimationReturn;
@@ -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';
@@ -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
- }
@@ -1,5 +0,0 @@
1
- export var ThemeAnimationType;
2
- (function (ThemeAnimationType) {
3
- ThemeAnimationType["CIRCLE"] = "circle";
4
- ThemeAnimationType["BLUR_CIRCLE"] = "blur-circle";
5
- })(ThemeAnimationType || (ThemeAnimationType = {}));
@@ -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;