@shohojdhara/atomix 0.4.0 → 0.4.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.
Files changed (150) hide show
  1. package/dist/atomix.css +0 -14
  2. package/dist/atomix.css.map +1 -1
  3. package/dist/atomix.min.css +4 -4
  4. package/dist/atomix.min.css.map +1 -1
  5. package/dist/charts.d.ts +12 -19
  6. package/dist/charts.js +555 -359
  7. package/dist/charts.js.map +1 -1
  8. package/dist/core.d.ts +98 -28
  9. package/dist/core.js +1082 -733
  10. package/dist/core.js.map +1 -1
  11. package/dist/forms.d.ts +26 -21
  12. package/dist/forms.js +937 -350
  13. package/dist/forms.js.map +1 -1
  14. package/dist/heavy.d.ts +14 -21
  15. package/dist/heavy.js +409 -256
  16. package/dist/heavy.js.map +1 -1
  17. package/dist/index.d.ts +518 -284
  18. package/dist/index.esm.js +1993 -1237
  19. package/dist/index.esm.js.map +1 -1
  20. package/dist/index.js +1994 -1237
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.min.js +1 -1
  23. package/dist/index.min.js.map +1 -1
  24. package/package.json +2 -2
  25. package/scripts/atomix-cli.js +43 -1
  26. package/scripts/cli/__tests__/utils.test.js +6 -2
  27. package/scripts/cli/migration-tools.js +2 -2
  28. package/scripts/cli/theme-bridge.js +7 -9
  29. package/scripts/cli/utils.js +2 -1
  30. package/src/components/Accordion/Accordion.stories.tsx +40 -0
  31. package/src/components/Accordion/Accordion.tsx +174 -56
  32. package/src/components/Accordion/AccordionCompound.test.tsx +70 -0
  33. package/src/components/AtomixGlass/AtomixGlass.tsx +82 -54
  34. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +17 -18
  35. package/src/components/AtomixGlass/README.md +5 -5
  36. package/src/components/AtomixGlass/stories/Customization.stories.tsx +2 -2
  37. package/src/components/AtomixGlass/stories/Examples.stories.tsx +42 -42
  38. package/src/components/AtomixGlass/stories/Modes.stories.tsx +5 -5
  39. package/src/components/AtomixGlass/stories/Overview.stories.tsx +3 -3
  40. package/src/components/AtomixGlass/stories/Performance.stories.tsx +2 -2
  41. package/src/components/AtomixGlass/stories/Playground.stories.tsx +45 -45
  42. package/src/components/AtomixGlass/stories/Shaders.stories.tsx +3 -3
  43. package/src/components/Badge/Badge.stories.tsx +1 -1
  44. package/src/components/Badge/Badge.tsx +1 -1
  45. package/src/components/Breadcrumb/Breadcrumb.tsx +185 -65
  46. package/src/components/Breadcrumb/BreadcrumbCompound.test.tsx +84 -0
  47. package/src/components/Breadcrumb/index.ts +2 -2
  48. package/src/components/Button/Button.stories.tsx +1 -1
  49. package/src/components/Button/README.md +2 -2
  50. package/src/components/Callout/Callout.stories.tsx +166 -1011
  51. package/src/components/Callout/Callout.test.tsx +3 -3
  52. package/src/components/Callout/Callout.tsx +196 -84
  53. package/src/components/Callout/CalloutCompound.test.tsx +72 -0
  54. package/src/components/Callout/README.md +2 -2
  55. package/src/components/Chart/Chart.stories.tsx +1 -1
  56. package/src/components/Chart/Chart.tsx +5 -5
  57. package/src/components/Chart/TreemapChart.tsx +37 -29
  58. package/src/components/DatePicker/readme.md +3 -3
  59. package/src/components/Dropdown/Dropdown.stories.tsx +1 -1
  60. package/src/components/Dropdown/Dropdown.tsx +133 -20
  61. package/src/components/Dropdown/DropdownCompound.test.tsx +64 -0
  62. package/src/components/EdgePanel/EdgePanel.stories.tsx +7 -7
  63. package/src/components/EdgePanel/EdgePanel.tsx +164 -112
  64. package/src/components/EdgePanel/EdgePanelCompound.test.tsx +53 -0
  65. package/src/components/Form/Checkbox.stories.tsx +1 -1
  66. package/src/components/Form/Checkbox.tsx +1 -1
  67. package/src/components/Form/Input.stories.tsx +1 -1
  68. package/src/components/Form/Input.tsx +1 -1
  69. package/src/components/Form/Radio.stories.tsx +1 -1
  70. package/src/components/Form/Radio.tsx +1 -1
  71. package/src/components/Form/Select.stories.tsx +24 -1
  72. package/src/components/Form/Select.test.tsx +99 -0
  73. package/src/components/Form/Select.tsx +145 -94
  74. package/src/components/Form/SelectOption.tsx +88 -0
  75. package/src/components/Form/Textarea.stories.tsx +1 -1
  76. package/src/components/Form/Textarea.tsx +1 -1
  77. package/src/components/Hero/Hero.stories.tsx +39 -2
  78. package/src/components/Hero/Hero.test.tsx +142 -0
  79. package/src/components/Hero/Hero.tsx +143 -4
  80. package/src/components/List/List.test.tsx +62 -0
  81. package/src/components/List/List.tsx +16 -5
  82. package/src/components/List/ListItem.tsx +20 -0
  83. package/src/components/Messages/Messages.stories.tsx +1 -1
  84. package/src/components/Messages/Messages.tsx +2 -2
  85. package/src/components/Modal/Modal.stories.tsx +66 -2
  86. package/src/components/Modal/Modal.tsx +115 -35
  87. package/src/components/Modal/ModalCompound.test.tsx +94 -0
  88. package/src/components/Navigation/Nav/Nav.stories.tsx +2 -2
  89. package/src/components/Navigation/Nav/Nav.tsx +1 -1
  90. package/src/components/Navigation/Navbar/Navbar.stories.tsx +3 -3
  91. package/src/components/Navigation/Navbar/Navbar.tsx +1 -1
  92. package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +2 -2
  93. package/src/components/Navigation/SideMenu/SideMenu.tsx +1 -1
  94. package/src/components/Pagination/Pagination.stories.tsx +1 -1
  95. package/src/components/Pagination/Pagination.tsx +1 -1
  96. package/src/components/Popover/Popover.stories.tsx +1 -1
  97. package/src/components/Popover/Popover.tsx +1 -1
  98. package/src/components/Progress/Progress.tsx +1 -1
  99. package/src/components/Rating/Rating.stories.tsx +1 -1
  100. package/src/components/Rating/Rating.test.tsx +73 -0
  101. package/src/components/Rating/Rating.tsx +25 -37
  102. package/src/components/Spinner/Spinner.tsx +1 -1
  103. package/src/components/Steps/Steps.stories.tsx +1 -1
  104. package/src/components/Steps/Steps.tsx +125 -22
  105. package/src/components/Steps/StepsCompound.test.tsx +81 -0
  106. package/src/components/Tabs/Tabs.stories.tsx +1 -1
  107. package/src/components/Tabs/Tabs.tsx +198 -45
  108. package/src/components/Tabs/TabsCompound.test.tsx +64 -0
  109. package/src/components/Todo/Todo.tsx +0 -1
  110. package/src/components/Toggle/Toggle.stories.tsx +1 -1
  111. package/src/components/Toggle/Toggle.tsx +1 -1
  112. package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
  113. package/src/components/VideoPlayer/VideoPlayer.stories.tsx +2 -2
  114. package/src/lib/composables/__tests__/useAtomixGlassPerf.test.tsx +88 -0
  115. package/src/lib/composables/__tests__/useChart.test.ts +50 -0
  116. package/src/lib/composables/__tests__/useChart.test.tsx +139 -0
  117. package/src/lib/composables/__tests__/useHeroBackgroundSlider.test.tsx +59 -0
  118. package/src/lib/composables/__tests__/useSliderAutoplay.test.tsx +68 -0
  119. package/src/lib/composables/atomix-glass/useGlassBackgroundDetection.ts +329 -0
  120. package/src/lib/composables/atomix-glass/useGlassCornerRadius.ts +82 -0
  121. package/src/lib/composables/atomix-glass/useGlassMouseTracking.ts +153 -0
  122. package/src/lib/composables/atomix-glass/useGlassOverLight.ts +198 -0
  123. package/src/lib/composables/atomix-glass/useGlassSize.ts +117 -0
  124. package/src/lib/composables/atomix-glass/useGlassState.ts +112 -0
  125. package/src/lib/composables/atomix-glass/useGlassTransforms.ts +160 -0
  126. package/src/lib/composables/glass-styles.ts +302 -0
  127. package/src/lib/composables/index.ts +0 -8
  128. package/src/lib/composables/useAtomixGlass.ts +331 -537
  129. package/src/lib/composables/useAtomixGlassStyles.ts +307 -0
  130. package/src/lib/composables/useBarChart.ts +1 -1
  131. package/src/lib/composables/useBreadcrumb.ts +6 -6
  132. package/src/lib/composables/useChart.ts +104 -21
  133. package/src/lib/composables/useHeroBackgroundSlider.ts +16 -7
  134. package/src/lib/composables/useSlider.ts +66 -34
  135. package/src/lib/theme/devtools/CLI.ts +2 -10
  136. package/src/lib/theme/utils/__tests__/themeUtils.test.ts +213 -0
  137. package/src/lib/types/components.ts +21 -23
  138. package/src/lib/utils/__tests__/componentUtils.test.ts +57 -2
  139. package/src/lib/utils/__tests__/dom.test.ts +100 -0
  140. package/src/lib/utils/__tests__/fontPreloader.test.ts +102 -0
  141. package/src/lib/utils/__tests__/themeNaming.test.ts +117 -0
  142. package/src/lib/utils/themeNaming.ts +1 -1
  143. package/src/styles/06-components/_components.accordion.scss +0 -2
  144. package/src/styles/06-components/_components.chart.scss +0 -1
  145. package/src/styles/06-components/_components.dropdown.scss +0 -1
  146. package/src/styles/06-components/_components.edge-panel.scss +0 -2
  147. package/src/styles/06-components/_components.photoviewer.scss +0 -1
  148. package/src/styles/06-components/_components.river.scss +0 -1
  149. package/src/styles/06-components/_components.slider.scss +0 -3
  150. package/src/styles/99-utilities/_utilities.glass-fixes.scss +0 -1
@@ -0,0 +1,198 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { calculateMouseInfluence } from '../../../components/AtomixGlass/glass-utils';
3
+ import type {
4
+ MousePosition,
5
+ OverLightConfig,
6
+ OverLightObjectConfig,
7
+ } from '../../types/components';
8
+
9
+ interface UseGlassOverLightProps {
10
+ overLight: OverLightConfig;
11
+ detectedOverLight: boolean;
12
+ mouseOffset: MousePosition;
13
+ isHovered: boolean;
14
+ isActive: boolean;
15
+ debugOverLight?: boolean;
16
+ }
17
+
18
+ export function useGlassOverLight({
19
+ overLight,
20
+ detectedOverLight,
21
+ mouseOffset,
22
+ isHovered,
23
+ isActive,
24
+ debugOverLight = false,
25
+ }: UseGlassOverLightProps) {
26
+ /**
27
+ * Get effective overLight value based on configuration
28
+ * - boolean: returns the boolean value directly
29
+ * - 'auto': returns detectedOverLight (auto-detected from background)
30
+ * - object: returns detectedOverLight (auto-detected, but config object provides customization)
31
+ */
32
+ const getEffectiveOverLight = useCallback(() => {
33
+ if (typeof overLight === 'boolean') {
34
+ return overLight;
35
+ }
36
+ if (overLight === 'auto') {
37
+ return detectedOverLight;
38
+ }
39
+ if (typeof overLight === 'object' && overLight !== null) {
40
+ return detectedOverLight;
41
+ }
42
+ // Default to false for safety when overLight is undefined or invalid
43
+ return false;
44
+ }, [overLight, detectedOverLight]);
45
+
46
+ /**
47
+ * Validate and clamp a numeric config value
48
+ * @param value - The value to validate
49
+ * @param min - Minimum allowed value
50
+ * @param max - Maximum allowed value
51
+ * @param defaultValue - Default value if validation fails
52
+ * @returns Validated and clamped value
53
+ */
54
+ const validateConfigValue = useCallback(
55
+ (value: unknown, min: number, max: number, defaultValue: number): number => {
56
+ if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) {
57
+ return defaultValue;
58
+ }
59
+ return Math.min(max, Math.max(min, value));
60
+ },
61
+ []
62
+ );
63
+
64
+ const overLightConfig = useMemo(() => {
65
+ const isOverLight = getEffectiveOverLight();
66
+ const mouseInfluence = calculateMouseInfluence(mouseOffset);
67
+ const hoverIntensity = isHovered ? 1.4 : 1;
68
+ const activeIntensity = isActive ? 1.6 : 1;
69
+
70
+ // More robust overlight configuration with better defaults and clamping
71
+ const baseOpacity = isOverLight
72
+ ? Math.min(0.6, Math.max(0.2, 0.5 * hoverIntensity * activeIntensity))
73
+ : 0;
74
+
75
+ const baseConfig = {
76
+ isOverLight,
77
+ threshold: 0.7,
78
+ opacity: baseOpacity,
79
+ contrast: Math.min(1.6, Math.max(1.0, 1.4 + mouseInfluence * 0.1)),
80
+ brightness: Math.min(1.1, Math.max(0.8, 0.9 + mouseInfluence * 0.05)),
81
+ saturationBoost: 1.3, // Fixed value — dynamic saturation amplifies perceived displacement
82
+ shadowIntensity: Math.min(1.2, Math.max(0.5, 0.9 + mouseInfluence * 0.2)),
83
+ borderOpacity: Math.min(1.0, Math.max(0.3, 0.7 + mouseInfluence * 0.1)),
84
+ };
85
+
86
+ if (typeof overLight === 'object' && overLight !== null) {
87
+ const objConfig = overLight as OverLightObjectConfig;
88
+
89
+ // Validate and apply object config values with proper clamping
90
+ const validatedThreshold = validateConfigValue(
91
+ objConfig.threshold,
92
+ 0.1,
93
+ 1.0,
94
+ baseConfig.threshold
95
+ );
96
+ const validatedOpacity = validateConfigValue(objConfig.opacity, 0.1, 1.0, baseConfig.opacity);
97
+ const validatedContrast = validateConfigValue(
98
+ objConfig.contrast,
99
+ 0.5,
100
+ 2.5,
101
+ baseConfig.contrast
102
+ );
103
+ const validatedBrightness = validateConfigValue(
104
+ objConfig.brightness,
105
+ 0.5,
106
+ 2.0,
107
+ baseConfig.brightness
108
+ );
109
+ const validatedSaturationBoost = validateConfigValue(
110
+ objConfig.saturationBoost,
111
+ 0.5,
112
+ 3.0,
113
+ baseConfig.saturationBoost
114
+ );
115
+
116
+ const finalConfig = {
117
+ ...baseConfig,
118
+ threshold: validatedThreshold,
119
+ opacity: validatedOpacity * hoverIntensity * activeIntensity,
120
+ contrast: Math.min(1.6, validatedContrast + mouseInfluence * 0.1),
121
+ brightness: Math.min(1.1, validatedBrightness + mouseInfluence * 0.05),
122
+ saturationBoost: validatedSaturationBoost, // Use validated value directly, no mouse influence
123
+ };
124
+
125
+ // Debug logging
126
+ if (
127
+ (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') &&
128
+ debugOverLight
129
+ ) {
130
+ console.log('[AtomixGlass] OverLight Config:', {
131
+ isOverLight,
132
+ config: {
133
+ threshold: finalConfig.threshold.toFixed(3),
134
+ opacity: finalConfig.opacity.toFixed(3),
135
+ contrast: finalConfig.contrast.toFixed(3),
136
+ brightness: finalConfig.brightness.toFixed(3),
137
+ saturationBoost: finalConfig.saturationBoost.toFixed(3),
138
+ shadowIntensity: finalConfig.shadowIntensity.toFixed(3),
139
+ borderOpacity: finalConfig.borderOpacity.toFixed(3),
140
+ },
141
+ input: {
142
+ threshold: objConfig.threshold,
143
+ opacity: objConfig.opacity,
144
+ contrast: objConfig.contrast,
145
+ brightness: objConfig.brightness,
146
+ saturationBoost: objConfig.saturationBoost,
147
+ },
148
+ dynamic: {
149
+ mouseInfluence: mouseInfluence.toFixed(3),
150
+ hoverIntensity: hoverIntensity.toFixed(3),
151
+ activeIntensity: activeIntensity.toFixed(3),
152
+ },
153
+ timestamp: new Date().toISOString(),
154
+ });
155
+ }
156
+
157
+ return finalConfig;
158
+ }
159
+
160
+ // Debug logging for non-object configs
161
+ if (
162
+ (typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') &&
163
+ debugOverLight
164
+ ) {
165
+ console.log('[AtomixGlass] OverLight Config:', {
166
+ isOverLight,
167
+ configType: typeof overLight === 'boolean' ? (overLight ? 'true' : 'false') : overLight,
168
+ config: {
169
+ threshold: baseConfig.threshold.toFixed(3),
170
+ opacity: baseConfig.opacity.toFixed(3),
171
+ contrast: baseConfig.contrast.toFixed(3),
172
+ brightness: baseConfig.brightness.toFixed(3),
173
+ saturationBoost: baseConfig.saturationBoost.toFixed(3),
174
+ shadowIntensity: baseConfig.shadowIntensity.toFixed(3),
175
+ borderOpacity: baseConfig.borderOpacity.toFixed(3),
176
+ },
177
+ dynamic: {
178
+ mouseInfluence: mouseInfluence.toFixed(3),
179
+ hoverIntensity: hoverIntensity.toFixed(3),
180
+ activeIntensity: activeIntensity.toFixed(3),
181
+ },
182
+ timestamp: new Date().toISOString(),
183
+ });
184
+ }
185
+
186
+ return baseConfig;
187
+ }, [
188
+ overLight,
189
+ getEffectiveOverLight,
190
+ mouseOffset,
191
+ isHovered,
192
+ isActive,
193
+ validateConfigValue,
194
+ debugOverLight,
195
+ ]);
196
+
197
+ return { overLightConfig };
198
+ }
@@ -0,0 +1,117 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { ATOMIX_GLASS } from '../../constants/components';
3
+ import { validateGlassSize } from '../../../components/AtomixGlass/glass-utils';
4
+ import type { GlassSize } from '../../types/components';
5
+
6
+ const { CONSTANTS } = ATOMIX_GLASS;
7
+
8
+ interface UseGlassSizeProps {
9
+ glassRef: React.RefObject<HTMLDivElement>;
10
+ effectiveBorderRadius: number;
11
+ cachedRectRef?: React.MutableRefObject<DOMRect | null>;
12
+ }
13
+
14
+ export function useGlassSize({
15
+ glassRef,
16
+ effectiveBorderRadius,
17
+ cachedRectRef,
18
+ }: UseGlassSizeProps) {
19
+ const [glassSize, setGlassSize] = useState<GlassSize>({ width: 270, height: 69 });
20
+
21
+ useEffect(() => {
22
+ const isValidElement = (element: HTMLElement | null): element is HTMLElement =>
23
+ element !== null && element instanceof HTMLElement && element.isConnected;
24
+
25
+ const validateSize = (size: GlassSize): boolean =>
26
+ validateGlassSize(size) &&
27
+ size.width <= CONSTANTS.MAX_SIZE &&
28
+ size.height <= CONSTANTS.MAX_SIZE;
29
+
30
+ let rafId: number | null = null;
31
+ let lastSize = { width: 0, height: 0 };
32
+ let lastCornerRadius = effectiveBorderRadius;
33
+
34
+ const updateGlassSize = (forceUpdate = false): void => {
35
+ if (rafId !== null) cancelAnimationFrame(rafId);
36
+
37
+ rafId = requestAnimationFrame(() => {
38
+ if (!isValidElement(glassRef.current)) {
39
+ rafId = null;
40
+ return;
41
+ }
42
+
43
+ const rect = glassRef.current.getBoundingClientRect();
44
+ if (rect.width <= 0 || rect.height <= 0) {
45
+ rafId = null;
46
+ return;
47
+ }
48
+
49
+ // Measure actual rendered size without artificial offsets to avoid feedback loops
50
+ const newSize: GlassSize = {
51
+ width: Math.round(rect.width),
52
+ height: Math.round(rect.height),
53
+ };
54
+
55
+ const cornerRadiusChanged = lastCornerRadius !== effectiveBorderRadius;
56
+ const dimensionsChanged =
57
+ Math.abs(newSize.width - lastSize.width) > 1 ||
58
+ Math.abs(newSize.height - lastSize.height) > 1;
59
+
60
+ if ((forceUpdate || cornerRadiusChanged || dimensionsChanged) && validateSize(newSize)) {
61
+ lastSize = newSize;
62
+ lastCornerRadius = effectiveBorderRadius;
63
+ setGlassSize(newSize);
64
+ }
65
+
66
+ rafId = null;
67
+ });
68
+ };
69
+
70
+ let resizeTimeoutId: NodeJS.Timeout | null = null;
71
+ const debouncedResizeHandler = (): void => {
72
+ if (resizeTimeoutId) clearTimeout(resizeTimeoutId);
73
+ resizeTimeoutId = setTimeout(() => updateGlassSize(false), 16);
74
+ };
75
+
76
+ const initialTimeoutId = setTimeout(() => updateGlassSize(true), 0);
77
+
78
+ let resizeObserver: ResizeObserver | null = null;
79
+ let resizeDebounceTimeout: NodeJS.Timeout | null = null;
80
+
81
+ // ResizeObserver has 98%+ browser support, no need for fallback
82
+ if (typeof ResizeObserver !== 'undefined' && isValidElement(glassRef.current)) {
83
+ try {
84
+ resizeObserver = new ResizeObserver(entries => {
85
+ for (const entry of entries) {
86
+ if (entry.target === glassRef.current) {
87
+ // Update cached rect when size changes
88
+ if (glassRef.current && cachedRectRef) {
89
+ cachedRectRef.current = glassRef.current.getBoundingClientRect();
90
+ }
91
+ // Debounce resize updates to match RAF timing (16ms)
92
+ if (resizeDebounceTimeout) clearTimeout(resizeDebounceTimeout);
93
+ resizeDebounceTimeout = setTimeout(() => updateGlassSize(false), 16);
94
+ break;
95
+ }
96
+ }
97
+ });
98
+ resizeObserver.observe(glassRef.current);
99
+ } catch (error) {
100
+ console.warn('AtomixGlass: ResizeObserver not available, using window resize only', error);
101
+ }
102
+ }
103
+
104
+ window.addEventListener('resize', debouncedResizeHandler, { passive: true });
105
+
106
+ return () => {
107
+ clearTimeout(initialTimeoutId);
108
+ if (rafId !== null) cancelAnimationFrame(rafId);
109
+ if (resizeTimeoutId) clearTimeout(resizeTimeoutId);
110
+ if (resizeDebounceTimeout) clearTimeout(resizeDebounceTimeout);
111
+ window.removeEventListener('resize', debouncedResizeHandler);
112
+ resizeObserver?.disconnect();
113
+ };
114
+ }, [effectiveBorderRadius, glassRef, cachedRectRef]);
115
+
116
+ return { glassSize };
117
+ }
@@ -0,0 +1,112 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import type { AtomixGlassProps } from '../../types/components';
3
+
4
+ interface UseGlassStateProps {
5
+ reducedMotion?: boolean;
6
+ highContrast?: boolean;
7
+ withoutEffects?: boolean;
8
+ onClick?: () => void;
9
+ }
10
+
11
+ export function useGlassState({
12
+ reducedMotion = false,
13
+ highContrast = false,
14
+ withoutEffects = false,
15
+ onClick,
16
+ }: UseGlassStateProps) {
17
+ const [isHovered, setIsHovered] = useState(false);
18
+ const [isActive, setIsActive] = useState(false);
19
+ const [userPrefersReducedMotion, setUserPrefersReducedMotion] = useState(false);
20
+ const [userPrefersHighContrast, setUserPrefersHighContrast] = useState(false);
21
+
22
+ // Media query handlers
23
+ useEffect(() => {
24
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
25
+ return undefined;
26
+ }
27
+
28
+ try {
29
+ const mediaQueryReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
30
+ const mediaQueryHighContrast = window.matchMedia('(prefers-contrast: high)');
31
+
32
+ setUserPrefersReducedMotion(mediaQueryReducedMotion.matches);
33
+ setUserPrefersHighContrast(mediaQueryHighContrast.matches);
34
+
35
+ const handleReducedMotionChange = (e: MediaQueryListEvent) => {
36
+ setUserPrefersReducedMotion(e.matches);
37
+ };
38
+
39
+ const handleHighContrastChange = (e: MediaQueryListEvent) => {
40
+ setUserPrefersHighContrast(e.matches);
41
+ };
42
+
43
+ if (mediaQueryReducedMotion.addEventListener) {
44
+ mediaQueryReducedMotion.addEventListener('change', handleReducedMotionChange);
45
+ mediaQueryHighContrast.addEventListener('change', handleHighContrastChange);
46
+ } else if (mediaQueryReducedMotion.addListener) {
47
+ mediaQueryReducedMotion.addListener(handleReducedMotionChange);
48
+ mediaQueryHighContrast.addListener(handleHighContrastChange);
49
+ }
50
+
51
+ return () => {
52
+ try {
53
+ if (mediaQueryReducedMotion.removeEventListener) {
54
+ mediaQueryReducedMotion.removeEventListener('change', handleReducedMotionChange);
55
+ mediaQueryHighContrast.removeEventListener('change', handleHighContrastChange);
56
+ } else if (mediaQueryReducedMotion.removeListener) {
57
+ mediaQueryReducedMotion.removeListener(handleReducedMotionChange);
58
+ mediaQueryHighContrast.removeListener(handleHighContrastChange);
59
+ }
60
+ } catch (cleanupError) {
61
+ console.error('AtomixGlass: Error cleaning up media query listeners:', cleanupError);
62
+ }
63
+ };
64
+ } catch (error) {
65
+ console.error('AtomixGlass: Error setting up media queries:', error);
66
+ return undefined;
67
+ }
68
+ }, []);
69
+
70
+ const effectiveReducedMotion = useMemo(
71
+ () => reducedMotion || userPrefersReducedMotion,
72
+ [reducedMotion, userPrefersReducedMotion]
73
+ );
74
+
75
+ const effectiveHighContrast = useMemo(
76
+ () => highContrast || userPrefersHighContrast,
77
+ [highContrast, userPrefersHighContrast]
78
+ );
79
+
80
+ const effectiveWithoutEffects = useMemo(
81
+ () => withoutEffects || effectiveReducedMotion,
82
+ [withoutEffects, effectiveReducedMotion]
83
+ );
84
+
85
+ const handleMouseEnter = useCallback(() => setIsHovered(true), []);
86
+ const handleMouseLeave = useCallback(() => setIsHovered(false), []);
87
+ const handleMouseDown = useCallback(() => setIsActive(true), []);
88
+ const handleMouseUp = useCallback(() => setIsActive(false), []);
89
+
90
+ const handleKeyDown = useCallback(
91
+ (e: React.KeyboardEvent<HTMLDivElement>) => {
92
+ if (onClick && (e.key === 'Enter' || e.key === ' ')) {
93
+ e.preventDefault();
94
+ onClick();
95
+ }
96
+ },
97
+ [onClick]
98
+ );
99
+
100
+ return {
101
+ isHovered,
102
+ isActive,
103
+ effectiveReducedMotion,
104
+ effectiveHighContrast,
105
+ effectiveWithoutEffects,
106
+ handleMouseEnter,
107
+ handleMouseLeave,
108
+ handleMouseDown,
109
+ handleMouseUp,
110
+ handleKeyDown,
111
+ };
112
+ }
@@ -0,0 +1,160 @@
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { ATOMIX_GLASS } from '../../constants/components';
3
+ import {
4
+ calculateDistance,
5
+ calculateElementCenter,
6
+ validateGlassSize,
7
+ } from '../../../components/AtomixGlass/glass-utils';
8
+ import type { GlassSize, MousePosition, OverLightConfig } from '../../types/components';
9
+
10
+ const { CONSTANTS } = ATOMIX_GLASS;
11
+
12
+ interface UseGlassTransformsProps {
13
+ glassRef: React.RefObject<HTMLDivElement>;
14
+ globalMousePosition: MousePosition;
15
+ glassSize: GlassSize;
16
+ overLight: OverLightConfig;
17
+ detectedOverLight: boolean;
18
+ elasticity?: number;
19
+ effectiveWithoutEffects?: boolean;
20
+ isActive?: boolean;
21
+ onClick?: () => void;
22
+ }
23
+
24
+ export function useGlassTransforms({
25
+ glassRef,
26
+ globalMousePosition,
27
+ glassSize,
28
+ overLight,
29
+ detectedOverLight,
30
+ elasticity = 0.05,
31
+ effectiveWithoutEffects = false,
32
+ isActive = false,
33
+ onClick,
34
+ }: UseGlassTransformsProps) {
35
+ const calculateDirectionalScale = useCallback(() => {
36
+ // Disable directional scaling if overLight is active (to prevent zooming/distorting the premium glass effect)
37
+ const isOverLightActive =
38
+ overLight === true ||
39
+ (overLight === 'auto' && detectedOverLight) ||
40
+ (typeof overLight === 'object' && overLight !== null && detectedOverLight);
41
+
42
+ if (isOverLightActive) {
43
+ return 'scale(1)';
44
+ }
45
+
46
+ if (
47
+ !globalMousePosition.x ||
48
+ !globalMousePosition.y ||
49
+ !glassRef.current ||
50
+ !validateGlassSize(glassSize)
51
+ ) {
52
+ return 'scale(1)';
53
+ }
54
+
55
+ const rect = glassRef.current.getBoundingClientRect();
56
+ const center = calculateElementCenter(rect);
57
+ const deltaX = globalMousePosition.x - center.x;
58
+ const deltaY = globalMousePosition.y - center.y;
59
+
60
+ const edgeDistanceX = Math.max(0, Math.abs(deltaX) - glassSize.width / 2);
61
+ const edgeDistanceY = Math.max(0, Math.abs(deltaY) - glassSize.height / 2);
62
+ const edgeDistance = calculateDistance({ x: edgeDistanceX, y: edgeDistanceY }, { x: 0, y: 0 });
63
+
64
+ if (edgeDistance > CONSTANTS.ACTIVATION_ZONE) {
65
+ return 'scale(1)';
66
+ }
67
+
68
+ const fadeInFactor = 1 - edgeDistance / CONSTANTS.ACTIVATION_ZONE;
69
+ const centerDistance = calculateDistance(globalMousePosition, center);
70
+
71
+ if (centerDistance === 0) {
72
+ return 'scale(1)';
73
+ }
74
+
75
+ const normalizedX = deltaX / centerDistance;
76
+ const normalizedY = deltaY / centerDistance;
77
+ const stretchIntensity = Math.min(centerDistance / 300, 1) * elasticity * fadeInFactor;
78
+
79
+ const scaleX =
80
+ 1 +
81
+ Math.abs(normalizedX) * stretchIntensity * 0.3 -
82
+ Math.abs(normalizedY) * stretchIntensity * 0.15;
83
+ const scaleY =
84
+ 1 +
85
+ Math.abs(normalizedY) * stretchIntensity * 0.3 -
86
+ Math.abs(normalizedX) * stretchIntensity * 0.15;
87
+
88
+ return `scaleX(${Math.max(0.8, scaleX)}) scaleY(${Math.max(0.8, scaleY)})`;
89
+ }, [globalMousePosition, elasticity, glassSize, glassRef, overLight, detectedOverLight]);
90
+
91
+ const calculateFadeInFactor = useCallback(() => {
92
+ if (
93
+ !globalMousePosition.x ||
94
+ !globalMousePosition.y ||
95
+ !glassRef.current ||
96
+ !validateGlassSize(glassSize)
97
+ ) {
98
+ return 0;
99
+ }
100
+
101
+ const rect = glassRef.current.getBoundingClientRect();
102
+ const center = calculateElementCenter(rect);
103
+
104
+ const edgeDistanceX = Math.max(
105
+ 0,
106
+ Math.abs(globalMousePosition.x - center.x) - glassSize.width / 2
107
+ );
108
+ const edgeDistanceY = Math.max(
109
+ 0,
110
+ Math.abs(globalMousePosition.y - center.y) - glassSize.height / 2
111
+ );
112
+ const edgeDistance = calculateDistance({ x: edgeDistanceX, y: edgeDistanceY }, { x: 0, y: 0 });
113
+
114
+ return edgeDistance > CONSTANTS.ACTIVATION_ZONE
115
+ ? 0
116
+ : 1 - edgeDistance / CONSTANTS.ACTIVATION_ZONE;
117
+ }, [globalMousePosition, glassSize, glassRef]);
118
+
119
+ const calculateElasticTranslation = useCallback(() => {
120
+ if (!glassRef.current) {
121
+ return { x: 0, y: 0 };
122
+ }
123
+
124
+ const fadeInFactor = calculateFadeInFactor();
125
+ const rect = glassRef.current.getBoundingClientRect();
126
+ const center = calculateElementCenter(rect);
127
+
128
+ return {
129
+ x: (globalMousePosition.x - center.x) * elasticity * 0.1 * fadeInFactor,
130
+ y: (globalMousePosition.y - center.y) * elasticity * 0.1 * fadeInFactor,
131
+ };
132
+ }, [globalMousePosition, elasticity, calculateFadeInFactor, glassRef]);
133
+
134
+ const elasticTranslation = useMemo(() => {
135
+ if (effectiveWithoutEffects) {
136
+ return { x: 0, y: 0 };
137
+ }
138
+ return calculateElasticTranslation();
139
+ }, [calculateElasticTranslation, effectiveWithoutEffects]);
140
+
141
+ const directionalScale = useMemo(() => {
142
+ if (effectiveWithoutEffects) {
143
+ return 'scale(1)';
144
+ }
145
+ return calculateDirectionalScale();
146
+ }, [calculateDirectionalScale, effectiveWithoutEffects]);
147
+
148
+ const transformStyle = useMemo(() => {
149
+ if (effectiveWithoutEffects) {
150
+ return isActive && Boolean(onClick) ? 'scale(0.98)' : 'scale(1)';
151
+ }
152
+ return `translate(${elasticTranslation.x}px, ${elasticTranslation.y}px) ${isActive && Boolean(onClick) ? 'scale(0.96)' : directionalScale}`;
153
+ }, [elasticTranslation, isActive, onClick, directionalScale, effectiveWithoutEffects]);
154
+
155
+ return {
156
+ elasticTranslation,
157
+ directionalScale,
158
+ transformStyle,
159
+ };
160
+ }