@space-man/react-theme-animation 2.3.0 → 3.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.
@@ -1,5 +1,5 @@
1
- import { Theme, ColorTheme } from '../core/index.js';
2
- export { resolveThemeForServer } from '../core/index.js';
1
+ import { c as Theme, C as ColorTheme } from '../animations-BrxKLW8D.js';
2
+ export { o as resolveThemeForServer } from '../animations-BrxKLW8D.js';
3
3
  import 'react';
4
4
 
5
5
  declare const STORAGE_KEY = "theme";
@@ -8,13 +8,13 @@ declare const GLOBAL_CLASS_NAME = "dark";
8
8
  declare const COLOR_THEME_PREFIX = "theme-";
9
9
  type ServerResolvedTheme = 'light' | 'dark' | 'system';
10
10
  interface ServerThemeData {
11
- theme: ServerResolvedTheme;
12
- themePreference: Theme;
13
- colorTheme: ColorTheme;
11
+ readonly theme: ServerResolvedTheme;
12
+ readonly themePreference: Theme;
13
+ readonly colorTheme: ColorTheme;
14
14
  }
15
15
  declare function buildServerThemeData(themeCookie: string | undefined, colorThemeCookie: string | undefined, options?: {
16
- defaultTheme?: Theme;
17
- defaultColorTheme?: ColorTheme;
16
+ readonly defaultTheme?: Theme;
17
+ readonly defaultColorTheme?: ColorTheme;
18
18
  }): ServerThemeData;
19
19
 
20
20
  export { COLOR_STORAGE_KEY, COLOR_THEME_PREFIX, ColorTheme, GLOBAL_CLASS_NAME, STORAGE_KEY, type ServerResolvedTheme, type ServerThemeData, Theme, buildServerThemeData };
@@ -1,27 +1 @@
1
- // src/core/utils/animations.ts
2
- var resolveThemeForServer = (theme) => {
3
- if (theme === "system") {
4
- return "system";
5
- }
6
- return theme === "dark" ? "dark" : "light";
7
- };
8
-
9
- // src/tanstack/helpers.ts
10
- var STORAGE_KEY = "theme";
11
- var COLOR_STORAGE_KEY = "color-theme";
12
- var GLOBAL_CLASS_NAME = "dark";
13
- var COLOR_THEME_PREFIX = "theme-";
14
- function buildServerThemeData(themeCookie, colorThemeCookie, options = {}) {
15
- const { defaultTheme = "system", defaultColorTheme = "default" } = options;
16
- const themePreference = themeCookie ?? defaultTheme;
17
- const colorTheme = colorThemeCookie ?? defaultColorTheme;
18
- return {
19
- theme: resolveThemeForServer(themePreference),
20
- themePreference,
21
- colorTheme
22
- };
23
- }
24
-
25
- export { COLOR_STORAGE_KEY, COLOR_THEME_PREFIX, GLOBAL_CLASS_NAME, STORAGE_KEY, buildServerThemeData, resolveThemeForServer };
26
- //# sourceMappingURL=index.js.map
27
- //# sourceMappingURL=index.js.map
1
+ export{b as COLOR_STORAGE_KEY,d as COLOR_THEME_PREFIX,c as GLOBAL_CLASS_NAME,a as STORAGE_KEY,e as buildServerThemeData}from'../chunk-QYUMCBTQ.js';export{d as resolveThemeForServer}from'../chunk-I3T2QXRT.js';
package/package.json CHANGED
@@ -1,120 +1,110 @@
1
1
  {
2
2
  "name": "@space-man/react-theme-animation",
3
- "version": "2.3.0",
3
+ "version": "3.0.1",
4
4
  "description": "React theme switching with smooth view transition animations and multi-theme support",
5
- "type": "module",
6
- "sideEffects": false,
7
5
  "keywords": [
8
- "react",
9
- "theme",
6
+ "animation",
7
+ "dark",
10
8
  "dark mode",
11
- "light mode",
12
- "theme switching",
13
- "view transitions",
14
9
  "hooks",
15
- "animation",
10
+ "light",
11
+ "light mode",
12
+ "nextjs",
13
+ "react",
14
+ "reactjs",
15
+ "shadcn",
16
16
  "switch",
17
+ "tailwind",
18
+ "tailwindcss",
19
+ "theme",
20
+ "theme switching",
17
21
  "typescript",
18
- "dark",
19
- "light",
22
+ "view transitions",
20
23
  "view-transition",
21
24
  "view-transition-api",
22
- "tailwind",
23
- "tailwindcss",
24
- "shadcn",
25
- "reactjs",
26
- "nextjs",
27
25
  "vite"
28
26
  ],
29
- "publishConfig": {
30
- "registry": "https://registry.npmjs.org/",
31
- "access": "public",
32
- "provenance": true
33
- },
34
- "repository": {
35
- "type": "git",
36
- "url": "git+https://github.com/tonyedgal/spaceman-react-theme-animation.git"
37
- },
38
27
  "homepage": "https://github.com/tonyedgal/spaceman-react-theme-animation#readme",
39
28
  "bugs": {
40
29
  "url": "https://github.com/tonyedgal/spaceman-react-theme-animation/issues"
41
30
  },
42
31
  "license": "MIT",
43
32
  "author": "tonyedgal",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/tonyedgal/spaceman-react-theme-animation.git"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "LICENSE",
40
+ "README.md"
41
+ ],
42
+ "type": "module",
43
+ "sideEffects": false,
44
+ "main": "./dist/index.js",
45
+ "module": "./dist/index.js",
46
+ "types": "./dist/index.d.ts",
44
47
  "exports": {
45
48
  ".": {
46
49
  "import": {
47
50
  "types": "./dist/index.d.ts",
48
51
  "default": "./dist/index.js"
49
- },
50
- "require": {
51
- "types": "./dist/index.d.cts",
52
- "default": "./dist/index.cjs"
53
52
  }
54
53
  },
55
54
  "./core": {
56
55
  "import": {
57
56
  "types": "./dist/core/index.d.ts",
58
57
  "default": "./dist/core/index.js"
59
- },
60
- "require": {
61
- "types": "./dist/core/index.d.cts",
62
- "default": "./dist/core/index.cjs"
63
58
  }
64
59
  },
65
60
  "./react": {
66
61
  "import": {
67
62
  "types": "./dist/react/index.d.ts",
68
63
  "default": "./dist/react/index.js"
69
- },
70
- "require": {
71
- "types": "./dist/react/index.d.cts",
72
- "default": "./dist/react/index.cjs"
73
64
  }
74
65
  },
75
66
  "./tanstack": {
76
67
  "import": {
77
68
  "types": "./dist/tanstack/index.d.ts",
78
69
  "default": "./dist/tanstack/index.js"
79
- },
80
- "require": {
81
- "types": "./dist/tanstack/index.d.cts",
82
- "default": "./dist/tanstack/index.cjs"
83
70
  }
84
71
  },
85
72
  "./package.json": "./package.json"
86
73
  },
87
- "main": "./dist/index.cjs",
88
- "module": "./dist/index.js",
89
- "types": "./dist/index.d.ts",
90
- "files": [
91
- "dist",
92
- "LICENSE",
93
- "README.md"
94
- ],
95
- "peerDependencies": {
96
- "react": ">=17",
97
- "react-dom": ">=17",
98
- "motion": ">=11",
99
- "@radix-ui/react-select": ">=2"
100
- },
101
- "devDependencies": {
102
- "@radix-ui/react-select": "^2.2.6",
103
- "@types/react": "^19.2.0",
104
- "@types/react-dom": "^19.2.0",
105
- "motion": "^12.27.1",
106
- "react": "^19.2.0",
107
- "react-dom": "^19.2.0"
74
+ "publishConfig": {
75
+ "access": "public",
76
+ "provenance": true,
77
+ "registry": "https://registry.npmjs.org/"
108
78
  },
109
79
  "dependencies": {
110
80
  "clsx": "^2.1.1"
111
81
  },
82
+ "devDependencies": {
83
+ "@playwright/test": "1.63.0",
84
+ "@radix-ui/react-select": "^2.3.7",
85
+ "@types/node": "^26.6.4",
86
+ "@types/react": "^19.3.0",
87
+ "@types/react-dom": "^19.3.0",
88
+ "motion": "^14.0.0",
89
+ "react": "^19.3.0",
90
+ "react-dom": "^19.3.0",
91
+ "vite": "^8.3.2",
92
+ "zod": "^4.6.5"
93
+ },
94
+ "peerDependencies": {
95
+ "@radix-ui/react-select": ">=2",
96
+ "motion": ">=12",
97
+ "react": ">=18",
98
+ "react-dom": ">=18"
99
+ },
112
100
  "scripts": {
113
101
  "build": "tsup && node scripts/mark-react-client.mjs",
102
+ "clean": "rm -rf dist",
114
103
  "dev": "tsup --watch",
115
- "lint": "eslint .",
116
104
  "test": "node scripts/smoke-subpath-imports.mjs",
117
- "typecheck": "tsc --noEmit",
118
- "clean": "rm -rf dist"
105
+ "test:browser": "playwright test",
106
+ "test:types": "tsc --noEmit -p tests/tsconfig.json",
107
+ "test:unit": "node --test tests/*.test.mjs",
108
+ "typecheck": "tsc --noEmit"
119
109
  }
120
110
  }
@@ -1,74 +0,0 @@
1
- 'use strict';
2
-
3
- // src/core/types/index.ts
4
- var ThemeAnimationType = /* @__PURE__ */ ((ThemeAnimationType2) => {
5
- ThemeAnimationType2["CIRCLE"] = "circle";
6
- ThemeAnimationType2["BLUR_CIRCLE"] = "blur-circle";
7
- ThemeAnimationType2["SLIDE"] = "slide";
8
- return ThemeAnimationType2;
9
- })(ThemeAnimationType || {});
10
-
11
- // src/core/utils/animations.ts
12
- var isBrowser = typeof window !== "undefined";
13
- var injectBaseStyles = () => {
14
- if (isBrowser) {
15
- const styleId = "spaceman-theme-base-style";
16
- if (!document.getElementById(styleId)) {
17
- const style = document.createElement("style");
18
- style.id = styleId;
19
- const isHighResolution = window.innerWidth >= 3e3 || window.innerHeight >= 2e3;
20
- style.textContent = `
21
- ::view-transition-old(root),
22
- ::view-transition-new(root) {
23
- animation: none;
24
- mix-blend-mode: normal;
25
- ${isHighResolution ? "transform: translateZ(0);" : ""}
26
- }
27
-
28
- ${isHighResolution ? `
29
- ::view-transition-group(root),
30
- ::view-transition-image-pair(root),
31
- ::view-transition-old(root),
32
- ::view-transition-new(root) {
33
- backface-visibility: hidden;
34
- perspective: 1000px;
35
- transform: translate3d(0, 0, 0);
36
- }
37
- ` : ""}
38
- `;
39
- document.head.appendChild(style);
40
- }
41
- }
42
- };
43
- var getSystemTheme = () => {
44
- if (!isBrowser) return "light";
45
- return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
46
- };
47
- var resolveTheme = (theme) => {
48
- if (theme === "system") {
49
- return getSystemTheme();
50
- }
51
- return theme === "dark" ? "dark" : "light";
52
- };
53
- var resolveThemeForServer = (theme) => {
54
- if (theme === "system") {
55
- return "system";
56
- }
57
- return theme === "dark" ? "dark" : "light";
58
- };
59
- var supportsViewTransitions = () => {
60
- return isBrowser && "startViewTransition" in document;
61
- };
62
- var prefersReducedMotion = () => {
63
- return isBrowser && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
64
- };
65
-
66
- exports.ThemeAnimationType = ThemeAnimationType;
67
- exports.getSystemTheme = getSystemTheme;
68
- exports.injectBaseStyles = injectBaseStyles;
69
- exports.prefersReducedMotion = prefersReducedMotion;
70
- exports.resolveTheme = resolveTheme;
71
- exports.resolveThemeForServer = resolveThemeForServer;
72
- exports.supportsViewTransitions = supportsViewTransitions;
73
- //# sourceMappingURL=index.cjs.map
74
- //# sourceMappingURL=index.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/core/types/index.ts","../../src/core/utils/animations.ts"],"names":["ThemeAnimationType"],"mappings":";;;AAEO,IAAK,kBAAA,qBAAAA,mBAAAA,KAAL;AACL,EAAAA,oBAAA,QAAA,CAAA,GAAS,QAAA;AACT,EAAAA,oBAAA,aAAA,CAAA,GAAc,aAAA;AACd,EAAAA,oBAAA,OAAA,CAAA,GAAQ,OAAA;AAHE,EAAA,OAAAA,mBAAAA;AAAA,CAAA,EAAA,kBAAA,IAAA,EAAA;;;ACAZ,IAAM,SAAA,GAAY,OAAO,MAAA,KAAW,WAAA;AAE7B,IAAM,mBAAmB,MAAY;AAC1C,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,MAAM,OAAA,GAAU,2BAAA;AAChB,IAAA,IAAI,CAAC,QAAA,CAAS,cAAA,CAAe,OAAO,CAAA,EAAG;AACrC,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,aAAA,CAAc,OAAO,CAAA;AAC5C,MAAA,KAAA,CAAM,EAAA,GAAK,OAAA;AACX,MAAA,MAAM,gBAAA,GACJ,MAAA,CAAO,UAAA,IAAc,GAAA,IAAQ,OAAO,WAAA,IAAe,GAAA;AAErD,MAAA,KAAA,CAAM,WAAA,GAAc;AAAA;AAAA;AAAA;AAAA;AAAA,UAAA,EAKd,gBAAA,GAAmB,8BAA8B,EAAE;AAAA;AAAA;AAAA,QAAA,EAIrD,gBAAA,GACI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA,CAAA,GAUA,EACN;AAAA,MAAA,CAAA;AAEF,MAAA,QAAA,CAAS,IAAA,CAAK,YAAY,KAAK,CAAA;AAAA,IACjC;AAAA,EACF;AACF;AAYO,IAAM,iBAAiB,MAAwB;AACpD,EAAA,IAAI,CAAC,WAAW,OAAO,OAAA;AACvB,EAAA,OAAO,MAAA,CAAO,UAAA,CAAW,8BAA8B,CAAA,CAAE,UACrD,MAAA,GACA,OAAA;AACN;AAEO,IAAM,YAAA,GAAe,CAAC,KAAA,KAAoC;AAC/D,EAAA,IAAI,UAAU,QAAA,EAAU;AACtB,IAAA,OAAO,cAAA,EAAe;AAAA,EACxB;AACA,EAAA,OAAO,KAAA,KAAU,SAAS,MAAA,GAAS,OAAA;AACrC;AAEO,IAAM,qBAAA,GAAwB,CACnC,KAAA,KACgC;AAChC,EAAA,IAAI,UAAU,QAAA,EAAU;AACtB,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAO,KAAA,KAAU,SAAS,MAAA,GAAS,OAAA;AACrC;AAEO,IAAM,0BAA0B,MAAe;AACpD,EAAA,OAAO,aAAa,qBAAA,IAAyB,QAAA;AAC/C;AAEO,IAAM,uBAAuB,MAAe;AACjD,EAAA,OACE,SAAA,IAAa,MAAA,CAAO,UAAA,CAAW,kCAAkC,CAAA,CAAE,OAAA;AAEvE","file":"index.cjs","sourcesContent":["import { RefObject } from 'react'\n\nexport enum ThemeAnimationType {\n CIRCLE = 'circle',\n BLUR_CIRCLE = 'blur-circle',\n SLIDE = 'slide',\n}\n\nexport type SlideDirection =\n | 'left'\n | 'right'\n | 'top'\n | 'bottom'\n | 'top-left'\n | 'top-right'\n | 'bottom-left'\n | 'bottom-right'\n\nexport type Theme = 'light' | 'dark' | 'system'\nexport type ColorTheme = string\nexport type SystemThemeMode = 'css' | 'js'\n\nexport interface UseThemeAnimationProps {\n duration?: number\n easing?: string\n animationType?: ThemeAnimationType\n blurAmount?: number\n styleId?: string\n\n themes?: Theme[]\n colorThemes?: ColorTheme[]\n defaultTheme?: Theme\n defaultColorTheme?: ColorTheme\n\n globalClassName?: string\n colorThemePrefix?: string\n attribute?: 'class' | 'data-theme'\n value?: Record<string, string>\n enableColorScheme?: boolean\n\n storageKey?: string\n colorStorageKey?: string\n\n theme?: Theme\n colorTheme?: ColorTheme\n\n onThemeChange?: (theme: Theme) => void\n onColorThemeChange?: (colorTheme: ColorTheme) => void\n\n initialTheme?: Theme\n initialColorTheme?: ColorTheme\n systemThemeMode?: SystemThemeMode\n\n // Slide animation options (only valid when animationType is SLIDE)\n slideDirection?: SlideDirection\n slideFromX?: number // custom from translate X (%)\n slideFromY?: number // custom from translate Y (%)\n slideToX?: number // custom to translate X (%)\n slideToY?: number // custom to translate Y (%)\n}\n\nexport interface UseThemeAnimationReturn {\n ref: RefObject<HTMLButtonElement | null>\n\n theme: Theme\n colorTheme: ColorTheme\n resolvedTheme: 'light' | 'dark'\n systemTheme: 'light' | 'dark'\n\n setTheme: (theme: Theme) => void\n setColorTheme: (colorTheme: ColorTheme) => void\n\n switchTheme: (theme: Theme, animationOff?: boolean) => Promise<void>\n switchColorTheme: (colorTheme: string) => void\n\n toggleTheme: (animationOff?: boolean) => Promise<void>\n toggleLightTheme: (animationOff?: boolean) => Promise<void>\n toggleDarkTheme: (animationOff?: boolean) => Promise<void>\n toggleColorTheme: () => void\n\n createColorThemeToggle: (targetColorTheme: string) => () => void\n isColorThemeActive: (targetColorTheme: string) => boolean\n}\n\nexport interface ThemeSwitcherProps {\n themes?: Theme[]\n currentTheme?: Theme\n onThemeChange?: (theme: Theme) => void\n\n animationType?: ThemeAnimationType\n duration?: number\n\n className?: string\n size?: 'sm' | 'md' | 'lg'\n variant?: 'default' | 'outline' | 'ghost'\n\n icons?: {\n light?: React.ReactNode\n dark?: React.ReactNode\n system?: React.ReactNode\n }\n}\n\nexport interface ThemeSelectorProps {\n themes?: Theme[]\n colorThemes?: ColorTheme[]\n currentTheme?: Theme\n currentColorTheme?: ColorTheme\n\n onThemeChange?: (theme: Theme) => void\n onColorThemeChange?: (colorTheme: ColorTheme) => void\n\n animationType?: ThemeAnimationType\n duration?: number\n\n className?: string\n placeholder?: string\n\n themeLabel?: string\n colorThemeLabel?: string\n}\n","import { ThemeAnimationType } from '../types'\n\nconst isBrowser = typeof window !== 'undefined'\n\nexport const injectBaseStyles = (): void => {\n if (isBrowser) {\n const styleId = 'spaceman-theme-base-style'\n if (!document.getElementById(styleId)) {\n const style = document.createElement('style')\n style.id = styleId\n const isHighResolution =\n window.innerWidth >= 3000 || window.innerHeight >= 2000\n\n style.textContent = `\n ::view-transition-old(root),\n ::view-transition-new(root) {\n animation: none;\n mix-blend-mode: normal;\n ${isHighResolution ? 'transform: translateZ(0);' : ''}\n }\n \n ${\n isHighResolution\n ? `\n ::view-transition-group(root),\n ::view-transition-image-pair(root),\n ::view-transition-old(root),\n ::view-transition-new(root) {\n backface-visibility: hidden;\n perspective: 1000px;\n transform: translate3d(0, 0, 0);\n }\n `\n : ''\n }\n `\n document.head.appendChild(style)\n }\n }\n}\n\nexport const createBlurCircleMask = (blur: number): string => {\n const isHighResolution =\n isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000)\n\n const blurFilter = `<filter id=\"blur\"><feGaussianBlur stdDeviation=\"${blur}\" /></filter>`\n const circleRadius = isHighResolution ? 20 : 25\n\n return `url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"-50 -50 100 100\"><defs>${blurFilter}</defs><circle cx=\"0\" cy=\"0\" r=\"${circleRadius}\" fill=\"white\" filter=\"url(%23blur)\"/></svg>')`\n}\n\nexport const getSystemTheme = (): 'light' | 'dark' => {\n if (!isBrowser) return 'light'\n return window.matchMedia('(prefers-color-scheme: dark)').matches\n ? 'dark'\n : 'light'\n}\n\nexport const resolveTheme = (theme: string): 'light' | 'dark' => {\n if (theme === 'system') {\n return getSystemTheme()\n }\n return theme === 'dark' ? 'dark' : 'light'\n}\n\nexport const resolveThemeForServer = (\n theme: string\n): 'light' | 'dark' | 'system' => {\n if (theme === 'system') {\n return 'system'\n }\n return theme === 'dark' ? 'dark' : 'light'\n}\n\nexport const supportsViewTransitions = (): boolean => {\n return isBrowser && 'startViewTransition' in document\n}\n\nexport const prefersReducedMotion = (): boolean => {\n return (\n isBrowser && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n )\n}\n\nexport interface AnimationConfig {\n a?: number\n b?: number\n x: number\n y: number\n duration: number\n easing: string\n animationType: ThemeAnimationType\n blurAmount: number\n styleId: string\n}\n\nexport const createCircleAnimation = (config: AnimationConfig): void => {\n const { x, y, duration, easing } = config\n\n // Calculate the distance to each corner of the viewport\n const topLeft = Math.hypot(x, y)\n const topRight = Math.hypot(window.innerWidth - x, y)\n const bottomLeft = Math.hypot(x, window.innerHeight - y)\n const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y)\n\n // Find the maximum distance to ensure animation covers the entire viewport\n const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight)\n\n document.documentElement.animate(\n {\n clipPath: [\n `circle(0px at ${x}px ${y}px)`,\n `circle(${maxRadius}px at ${x}px ${y}px)`,\n ],\n },\n {\n duration,\n easing,\n pseudoElement: '::view-transition-new(root)',\n }\n )\n}\n\nexport const createSlideAnimation = (config: AnimationConfig): void => {\n const { a = -100, b = 0, x = 0, y = 0, duration, styleId } = config\n\n const styleElement = document.createElement('style')\n styleElement.id = styleId\n\n styleElement.textContent = `\n ::view-transition-old(root) {\n animation-delay: ${duration}ms;\n }\n \n ::view-transition-new(root) {\n animation: move-in ${duration}ms;\n animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);\n }\n \n @keyframes move-in {\n from {\n translate: ${a}% ${b}%;\n }\n to {\n translate: ${x}% ${y}%;\n }\n }\n `\n\n document.head.appendChild(styleElement)\n\n // Clean up after animation\n setTimeout(() => {\n const styleElement = document.getElementById(styleId)\n if (styleElement) {\n styleElement.remove()\n }\n }, duration)\n}\n\nexport const createBlurCircleAnimation = (config: AnimationConfig): void => {\n const { x, y, duration, easing, blurAmount, styleId } = config\n\n const existingStyle = document.getElementById(styleId)\n if (existingStyle) {\n existingStyle.remove()\n }\n\n const viewportSize = Math.max(window.innerWidth, window.innerHeight) + 200\n const isHighResolution =\n window.innerWidth >= 3000 || window.innerHeight >= 2000\n const scaleFactor = isHighResolution ? 2.5 : 4\n const optimalMaskSize = isHighResolution\n ? Math.min(viewportSize * scaleFactor, 5000)\n : viewportSize * scaleFactor\n\n const topLeft = Math.hypot(x, y)\n const topRight = Math.hypot(window.innerWidth - x, y)\n const bottomLeft = Math.hypot(x, window.innerHeight - y)\n const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y)\n const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight)\n\n const styleElement = document.createElement('style')\n styleElement.id = styleId\n\n const blurFactor = isHighResolution ? 1.5 : 1.2\n const finalMaskSize = Math.max(optimalMaskSize, maxRadius * 2.5)\n\n styleElement.textContent = `\n ::view-transition-group(root) {\n animation-duration: ${duration}ms;\n animation-timing-function: ${\n isHighResolution\n ? 'cubic-bezier(0.2, 0, 0.2, 1)'\n : 'linear(' +\n '0 0%, 0.2342 12.49%, 0.4374 24.99%,' +\n '0.6093 37.49%, 0.6835 43.74%,' +\n '0.7499 49.99%, 0.8086 56.25%,' +\n '0.8593 62.5%, 0.9023 68.75%, 0.9375 75%,' +\n '0.9648 81.25%, 0.9844 87.5%,' +\n '0.9961 93.75%, 1 100%' +\n ')'\n };\n will-change: transform;\n }\n \n ::view-transition-new(root) {\n mask: ${createBlurCircleMask(blurAmount * blurFactor)} 0 0 / 100% 100% no-repeat;\n mask-position: ${x}px ${y}px;\n animation: spaceman-mask-scale ${duration}ms ${easing};\n transform-origin: ${x}px ${y}px;\n will-change: mask-size, mask-position;\n }\n \n ::view-transition-old(root),\n .dark::view-transition-old(root) {\n animation: spaceman-mask-scale ${duration}ms ${easing};\n transform-origin: ${x}px ${y}px;\n z-index: -1;\n will-change: mask-size, mask-position;\n }\n \n @keyframes spaceman-mask-scale {\n 0% {\n mask-size: 0px;\n mask-position: ${x}px ${y}px;\n }\n 100% {\n mask-size: ${finalMaskSize}px;\n mask-position: ${x - finalMaskSize / 2}px ${y - finalMaskSize / 2}px;\n }\n }\n `\n\n document.head.appendChild(styleElement)\n\n setTimeout(() => {\n const styleElement = document.getElementById(styleId)\n if (styleElement) {\n styleElement.remove()\n }\n }, duration)\n}\n"]}
@@ -1,96 +0,0 @@
1
- import { RefObject } from 'react';
2
-
3
- declare enum ThemeAnimationType {
4
- CIRCLE = "circle",
5
- BLUR_CIRCLE = "blur-circle",
6
- SLIDE = "slide"
7
- }
8
- type SlideDirection = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
9
- type Theme = 'light' | 'dark' | 'system';
10
- type ColorTheme = string;
11
- type SystemThemeMode = 'css' | 'js';
12
- interface UseThemeAnimationProps {
13
- duration?: number;
14
- easing?: string;
15
- animationType?: ThemeAnimationType;
16
- blurAmount?: number;
17
- styleId?: string;
18
- themes?: Theme[];
19
- colorThemes?: ColorTheme[];
20
- defaultTheme?: Theme;
21
- defaultColorTheme?: ColorTheme;
22
- globalClassName?: string;
23
- colorThemePrefix?: string;
24
- attribute?: 'class' | 'data-theme';
25
- value?: Record<string, string>;
26
- enableColorScheme?: boolean;
27
- storageKey?: string;
28
- colorStorageKey?: string;
29
- theme?: Theme;
30
- colorTheme?: ColorTheme;
31
- onThemeChange?: (theme: Theme) => void;
32
- onColorThemeChange?: (colorTheme: ColorTheme) => void;
33
- initialTheme?: Theme;
34
- initialColorTheme?: ColorTheme;
35
- systemThemeMode?: SystemThemeMode;
36
- slideDirection?: SlideDirection;
37
- slideFromX?: number;
38
- slideFromY?: number;
39
- slideToX?: number;
40
- slideToY?: number;
41
- }
42
- interface UseThemeAnimationReturn {
43
- ref: RefObject<HTMLButtonElement | null>;
44
- theme: Theme;
45
- colorTheme: ColorTheme;
46
- resolvedTheme: 'light' | 'dark';
47
- systemTheme: 'light' | 'dark';
48
- setTheme: (theme: Theme) => void;
49
- setColorTheme: (colorTheme: ColorTheme) => void;
50
- switchTheme: (theme: Theme, animationOff?: boolean) => Promise<void>;
51
- switchColorTheme: (colorTheme: string) => void;
52
- toggleTheme: (animationOff?: boolean) => Promise<void>;
53
- toggleLightTheme: (animationOff?: boolean) => Promise<void>;
54
- toggleDarkTheme: (animationOff?: boolean) => Promise<void>;
55
- toggleColorTheme: () => void;
56
- createColorThemeToggle: (targetColorTheme: string) => () => void;
57
- isColorThemeActive: (targetColorTheme: string) => boolean;
58
- }
59
- interface ThemeSwitcherProps {
60
- themes?: Theme[];
61
- currentTheme?: Theme;
62
- onThemeChange?: (theme: Theme) => void;
63
- animationType?: ThemeAnimationType;
64
- duration?: number;
65
- className?: string;
66
- size?: 'sm' | 'md' | 'lg';
67
- variant?: 'default' | 'outline' | 'ghost';
68
- icons?: {
69
- light?: React.ReactNode;
70
- dark?: React.ReactNode;
71
- system?: React.ReactNode;
72
- };
73
- }
74
- interface ThemeSelectorProps {
75
- themes?: Theme[];
76
- colorThemes?: ColorTheme[];
77
- currentTheme?: Theme;
78
- currentColorTheme?: ColorTheme;
79
- onThemeChange?: (theme: Theme) => void;
80
- onColorThemeChange?: (colorTheme: ColorTheme) => void;
81
- animationType?: ThemeAnimationType;
82
- duration?: number;
83
- className?: string;
84
- placeholder?: string;
85
- themeLabel?: string;
86
- colorThemeLabel?: string;
87
- }
88
-
89
- declare const injectBaseStyles: () => void;
90
- declare const getSystemTheme: () => "light" | "dark";
91
- declare const resolveTheme: (theme: string) => "light" | "dark";
92
- declare const resolveThemeForServer: (theme: string) => "light" | "dark" | "system";
93
- declare const supportsViewTransitions: () => boolean;
94
- declare const prefersReducedMotion: () => boolean;
95
-
96
- export { type ColorTheme, type SlideDirection, type SystemThemeMode, type Theme, ThemeAnimationType, type ThemeSelectorProps, type ThemeSwitcherProps, type UseThemeAnimationProps, type UseThemeAnimationReturn, getSystemTheme, injectBaseStyles, prefersReducedMotion, resolveTheme, resolveThemeForServer, supportsViewTransitions };
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/core/types/index.ts","../../src/core/utils/animations.ts"],"names":["ThemeAnimationType"],"mappings":";AAEO,IAAK,kBAAA,qBAAAA,mBAAAA,KAAL;AACL,EAAAA,oBAAA,QAAA,CAAA,GAAS,QAAA;AACT,EAAAA,oBAAA,aAAA,CAAA,GAAc,aAAA;AACd,EAAAA,oBAAA,OAAA,CAAA,GAAQ,OAAA;AAHE,EAAA,OAAAA,mBAAAA;AAAA,CAAA,EAAA,kBAAA,IAAA,EAAA;;;ACAZ,IAAM,SAAA,GAAY,OAAO,MAAA,KAAW,WAAA;AAE7B,IAAM,mBAAmB,MAAY;AAC1C,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,MAAM,OAAA,GAAU,2BAAA;AAChB,IAAA,IAAI,CAAC,QAAA,CAAS,cAAA,CAAe,OAAO,CAAA,EAAG;AACrC,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,aAAA,CAAc,OAAO,CAAA;AAC5C,MAAA,KAAA,CAAM,EAAA,GAAK,OAAA;AACX,MAAA,MAAM,gBAAA,GACJ,MAAA,CAAO,UAAA,IAAc,GAAA,IAAQ,OAAO,WAAA,IAAe,GAAA;AAErD,MAAA,KAAA,CAAM,WAAA,GAAc;AAAA;AAAA;AAAA;AAAA;AAAA,UAAA,EAKd,gBAAA,GAAmB,8BAA8B,EAAE;AAAA;AAAA;AAAA,QAAA,EAIrD,gBAAA,GACI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA,CAAA,GAUA,EACN;AAAA,MAAA,CAAA;AAEF,MAAA,QAAA,CAAS,IAAA,CAAK,YAAY,KAAK,CAAA;AAAA,IACjC;AAAA,EACF;AACF;AAYO,IAAM,iBAAiB,MAAwB;AACpD,EAAA,IAAI,CAAC,WAAW,OAAO,OAAA;AACvB,EAAA,OAAO,MAAA,CAAO,UAAA,CAAW,8BAA8B,CAAA,CAAE,UACrD,MAAA,GACA,OAAA;AACN;AAEO,IAAM,YAAA,GAAe,CAAC,KAAA,KAAoC;AAC/D,EAAA,IAAI,UAAU,QAAA,EAAU;AACtB,IAAA,OAAO,cAAA,EAAe;AAAA,EACxB;AACA,EAAA,OAAO,KAAA,KAAU,SAAS,MAAA,GAAS,OAAA;AACrC;AAEO,IAAM,qBAAA,GAAwB,CACnC,KAAA,KACgC;AAChC,EAAA,IAAI,UAAU,QAAA,EAAU;AACtB,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAO,KAAA,KAAU,SAAS,MAAA,GAAS,OAAA;AACrC;AAEO,IAAM,0BAA0B,MAAe;AACpD,EAAA,OAAO,aAAa,qBAAA,IAAyB,QAAA;AAC/C;AAEO,IAAM,uBAAuB,MAAe;AACjD,EAAA,OACE,SAAA,IAAa,MAAA,CAAO,UAAA,CAAW,kCAAkC,CAAA,CAAE,OAAA;AAEvE","file":"index.js","sourcesContent":["import { RefObject } from 'react'\n\nexport enum ThemeAnimationType {\n CIRCLE = 'circle',\n BLUR_CIRCLE = 'blur-circle',\n SLIDE = 'slide',\n}\n\nexport type SlideDirection =\n | 'left'\n | 'right'\n | 'top'\n | 'bottom'\n | 'top-left'\n | 'top-right'\n | 'bottom-left'\n | 'bottom-right'\n\nexport type Theme = 'light' | 'dark' | 'system'\nexport type ColorTheme = string\nexport type SystemThemeMode = 'css' | 'js'\n\nexport interface UseThemeAnimationProps {\n duration?: number\n easing?: string\n animationType?: ThemeAnimationType\n blurAmount?: number\n styleId?: string\n\n themes?: Theme[]\n colorThemes?: ColorTheme[]\n defaultTheme?: Theme\n defaultColorTheme?: ColorTheme\n\n globalClassName?: string\n colorThemePrefix?: string\n attribute?: 'class' | 'data-theme'\n value?: Record<string, string>\n enableColorScheme?: boolean\n\n storageKey?: string\n colorStorageKey?: string\n\n theme?: Theme\n colorTheme?: ColorTheme\n\n onThemeChange?: (theme: Theme) => void\n onColorThemeChange?: (colorTheme: ColorTheme) => void\n\n initialTheme?: Theme\n initialColorTheme?: ColorTheme\n systemThemeMode?: SystemThemeMode\n\n // Slide animation options (only valid when animationType is SLIDE)\n slideDirection?: SlideDirection\n slideFromX?: number // custom from translate X (%)\n slideFromY?: number // custom from translate Y (%)\n slideToX?: number // custom to translate X (%)\n slideToY?: number // custom to translate Y (%)\n}\n\nexport interface UseThemeAnimationReturn {\n ref: RefObject<HTMLButtonElement | null>\n\n theme: Theme\n colorTheme: ColorTheme\n resolvedTheme: 'light' | 'dark'\n systemTheme: 'light' | 'dark'\n\n setTheme: (theme: Theme) => void\n setColorTheme: (colorTheme: ColorTheme) => void\n\n switchTheme: (theme: Theme, animationOff?: boolean) => Promise<void>\n switchColorTheme: (colorTheme: string) => void\n\n toggleTheme: (animationOff?: boolean) => Promise<void>\n toggleLightTheme: (animationOff?: boolean) => Promise<void>\n toggleDarkTheme: (animationOff?: boolean) => Promise<void>\n toggleColorTheme: () => void\n\n createColorThemeToggle: (targetColorTheme: string) => () => void\n isColorThemeActive: (targetColorTheme: string) => boolean\n}\n\nexport interface ThemeSwitcherProps {\n themes?: Theme[]\n currentTheme?: Theme\n onThemeChange?: (theme: Theme) => void\n\n animationType?: ThemeAnimationType\n duration?: number\n\n className?: string\n size?: 'sm' | 'md' | 'lg'\n variant?: 'default' | 'outline' | 'ghost'\n\n icons?: {\n light?: React.ReactNode\n dark?: React.ReactNode\n system?: React.ReactNode\n }\n}\n\nexport interface ThemeSelectorProps {\n themes?: Theme[]\n colorThemes?: ColorTheme[]\n currentTheme?: Theme\n currentColorTheme?: ColorTheme\n\n onThemeChange?: (theme: Theme) => void\n onColorThemeChange?: (colorTheme: ColorTheme) => void\n\n animationType?: ThemeAnimationType\n duration?: number\n\n className?: string\n placeholder?: string\n\n themeLabel?: string\n colorThemeLabel?: string\n}\n","import { ThemeAnimationType } from '../types'\n\nconst isBrowser = typeof window !== 'undefined'\n\nexport const injectBaseStyles = (): void => {\n if (isBrowser) {\n const styleId = 'spaceman-theme-base-style'\n if (!document.getElementById(styleId)) {\n const style = document.createElement('style')\n style.id = styleId\n const isHighResolution =\n window.innerWidth >= 3000 || window.innerHeight >= 2000\n\n style.textContent = `\n ::view-transition-old(root),\n ::view-transition-new(root) {\n animation: none;\n mix-blend-mode: normal;\n ${isHighResolution ? 'transform: translateZ(0);' : ''}\n }\n \n ${\n isHighResolution\n ? `\n ::view-transition-group(root),\n ::view-transition-image-pair(root),\n ::view-transition-old(root),\n ::view-transition-new(root) {\n backface-visibility: hidden;\n perspective: 1000px;\n transform: translate3d(0, 0, 0);\n }\n `\n : ''\n }\n `\n document.head.appendChild(style)\n }\n }\n}\n\nexport const createBlurCircleMask = (blur: number): string => {\n const isHighResolution =\n isBrowser && (window.innerWidth >= 3000 || window.innerHeight >= 2000)\n\n const blurFilter = `<filter id=\"blur\"><feGaussianBlur stdDeviation=\"${blur}\" /></filter>`\n const circleRadius = isHighResolution ? 20 : 25\n\n return `url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"-50 -50 100 100\"><defs>${blurFilter}</defs><circle cx=\"0\" cy=\"0\" r=\"${circleRadius}\" fill=\"white\" filter=\"url(%23blur)\"/></svg>')`\n}\n\nexport const getSystemTheme = (): 'light' | 'dark' => {\n if (!isBrowser) return 'light'\n return window.matchMedia('(prefers-color-scheme: dark)').matches\n ? 'dark'\n : 'light'\n}\n\nexport const resolveTheme = (theme: string): 'light' | 'dark' => {\n if (theme === 'system') {\n return getSystemTheme()\n }\n return theme === 'dark' ? 'dark' : 'light'\n}\n\nexport const resolveThemeForServer = (\n theme: string\n): 'light' | 'dark' | 'system' => {\n if (theme === 'system') {\n return 'system'\n }\n return theme === 'dark' ? 'dark' : 'light'\n}\n\nexport const supportsViewTransitions = (): boolean => {\n return isBrowser && 'startViewTransition' in document\n}\n\nexport const prefersReducedMotion = (): boolean => {\n return (\n isBrowser && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n )\n}\n\nexport interface AnimationConfig {\n a?: number\n b?: number\n x: number\n y: number\n duration: number\n easing: string\n animationType: ThemeAnimationType\n blurAmount: number\n styleId: string\n}\n\nexport const createCircleAnimation = (config: AnimationConfig): void => {\n const { x, y, duration, easing } = config\n\n // Calculate the distance to each corner of the viewport\n const topLeft = Math.hypot(x, y)\n const topRight = Math.hypot(window.innerWidth - x, y)\n const bottomLeft = Math.hypot(x, window.innerHeight - y)\n const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y)\n\n // Find the maximum distance to ensure animation covers the entire viewport\n const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight)\n\n document.documentElement.animate(\n {\n clipPath: [\n `circle(0px at ${x}px ${y}px)`,\n `circle(${maxRadius}px at ${x}px ${y}px)`,\n ],\n },\n {\n duration,\n easing,\n pseudoElement: '::view-transition-new(root)',\n }\n )\n}\n\nexport const createSlideAnimation = (config: AnimationConfig): void => {\n const { a = -100, b = 0, x = 0, y = 0, duration, styleId } = config\n\n const styleElement = document.createElement('style')\n styleElement.id = styleId\n\n styleElement.textContent = `\n ::view-transition-old(root) {\n animation-delay: ${duration}ms;\n }\n \n ::view-transition-new(root) {\n animation: move-in ${duration}ms;\n animation-timing-function: cubic-bezier(0.66, 0, 0.34, 1);\n }\n \n @keyframes move-in {\n from {\n translate: ${a}% ${b}%;\n }\n to {\n translate: ${x}% ${y}%;\n }\n }\n `\n\n document.head.appendChild(styleElement)\n\n // Clean up after animation\n setTimeout(() => {\n const styleElement = document.getElementById(styleId)\n if (styleElement) {\n styleElement.remove()\n }\n }, duration)\n}\n\nexport const createBlurCircleAnimation = (config: AnimationConfig): void => {\n const { x, y, duration, easing, blurAmount, styleId } = config\n\n const existingStyle = document.getElementById(styleId)\n if (existingStyle) {\n existingStyle.remove()\n }\n\n const viewportSize = Math.max(window.innerWidth, window.innerHeight) + 200\n const isHighResolution =\n window.innerWidth >= 3000 || window.innerHeight >= 2000\n const scaleFactor = isHighResolution ? 2.5 : 4\n const optimalMaskSize = isHighResolution\n ? Math.min(viewportSize * scaleFactor, 5000)\n : viewportSize * scaleFactor\n\n const topLeft = Math.hypot(x, y)\n const topRight = Math.hypot(window.innerWidth - x, y)\n const bottomLeft = Math.hypot(x, window.innerHeight - y)\n const bottomRight = Math.hypot(window.innerWidth - x, window.innerHeight - y)\n const maxRadius = Math.max(topLeft, topRight, bottomLeft, bottomRight)\n\n const styleElement = document.createElement('style')\n styleElement.id = styleId\n\n const blurFactor = isHighResolution ? 1.5 : 1.2\n const finalMaskSize = Math.max(optimalMaskSize, maxRadius * 2.5)\n\n styleElement.textContent = `\n ::view-transition-group(root) {\n animation-duration: ${duration}ms;\n animation-timing-function: ${\n isHighResolution\n ? 'cubic-bezier(0.2, 0, 0.2, 1)'\n : 'linear(' +\n '0 0%, 0.2342 12.49%, 0.4374 24.99%,' +\n '0.6093 37.49%, 0.6835 43.74%,' +\n '0.7499 49.99%, 0.8086 56.25%,' +\n '0.8593 62.5%, 0.9023 68.75%, 0.9375 75%,' +\n '0.9648 81.25%, 0.9844 87.5%,' +\n '0.9961 93.75%, 1 100%' +\n ')'\n };\n will-change: transform;\n }\n \n ::view-transition-new(root) {\n mask: ${createBlurCircleMask(blurAmount * blurFactor)} 0 0 / 100% 100% no-repeat;\n mask-position: ${x}px ${y}px;\n animation: spaceman-mask-scale ${duration}ms ${easing};\n transform-origin: ${x}px ${y}px;\n will-change: mask-size, mask-position;\n }\n \n ::view-transition-old(root),\n .dark::view-transition-old(root) {\n animation: spaceman-mask-scale ${duration}ms ${easing};\n transform-origin: ${x}px ${y}px;\n z-index: -1;\n will-change: mask-size, mask-position;\n }\n \n @keyframes spaceman-mask-scale {\n 0% {\n mask-size: 0px;\n mask-position: ${x}px ${y}px;\n }\n 100% {\n mask-size: ${finalMaskSize}px;\n mask-position: ${x - finalMaskSize / 2}px ${y - finalMaskSize / 2}px;\n }\n }\n `\n\n document.head.appendChild(styleElement)\n\n setTimeout(() => {\n const styleElement = document.getElementById(styleId)\n if (styleElement) {\n styleElement.remove()\n }\n }, duration)\n}\n"]}