@shohojdhara/atomix 0.4.1 → 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.
- package/dist/atomix.css +9341 -9249
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +4 -4
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +12 -19
- package/dist/charts.js +555 -358
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +16 -23
- package/dist/core.js +418 -262
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +11 -18
- package/dist/forms.js +411 -257
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +14 -21
- package/dist/heavy.js +408 -254
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +33 -40
- package/dist/index.esm.js +663 -453
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +667 -460
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +1 -1
- package/scripts/atomix-cli.js +34 -1
- package/src/components/AtomixGlass/AtomixGlass.tsx +82 -54
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +17 -18
- package/src/components/AtomixGlass/README.md +5 -5
- package/src/components/AtomixGlass/stories/Customization.stories.tsx +2 -2
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +42 -42
- package/src/components/AtomixGlass/stories/Modes.stories.tsx +5 -5
- package/src/components/AtomixGlass/stories/Overview.stories.tsx +3 -3
- package/src/components/AtomixGlass/stories/Performance.stories.tsx +2 -2
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +45 -45
- package/src/components/AtomixGlass/stories/Shaders.stories.tsx +3 -3
- package/src/components/Badge/Badge.stories.tsx +1 -1
- package/src/components/Badge/Badge.tsx +1 -1
- package/src/components/Breadcrumb/Breadcrumb.tsx +90 -76
- package/src/components/Breadcrumb/index.ts +2 -2
- package/src/components/Button/Button.stories.tsx +1 -1
- package/src/components/Button/README.md +2 -2
- package/src/components/Callout/Callout.test.tsx +3 -3
- package/src/components/Callout/Callout.tsx +2 -2
- package/src/components/Callout/README.md +2 -2
- package/src/components/Chart/Chart.stories.tsx +1 -1
- package/src/components/Chart/Chart.tsx +5 -5
- package/src/components/Chart/TreemapChart.tsx +37 -29
- package/src/components/DatePicker/readme.md +3 -3
- package/src/components/Dropdown/Dropdown.stories.tsx +1 -1
- package/src/components/EdgePanel/EdgePanel.stories.tsx +7 -7
- package/src/components/Form/Checkbox.stories.tsx +1 -1
- package/src/components/Form/Checkbox.tsx +1 -1
- package/src/components/Form/Input.stories.tsx +1 -1
- package/src/components/Form/Input.tsx +1 -1
- package/src/components/Form/Radio.stories.tsx +1 -1
- package/src/components/Form/Radio.tsx +1 -1
- package/src/components/Form/Select.stories.tsx +1 -1
- package/src/components/Form/Select.tsx +1 -1
- package/src/components/Form/Textarea.stories.tsx +1 -1
- package/src/components/Form/Textarea.tsx +1 -1
- package/src/components/Hero/Hero.stories.tsx +2 -2
- package/src/components/Hero/Hero.tsx +2 -2
- package/src/components/Messages/Messages.stories.tsx +1 -1
- package/src/components/Messages/Messages.tsx +2 -2
- package/src/components/Modal/Modal.stories.tsx +1 -1
- package/src/components/Navigation/Nav/Nav.stories.tsx +2 -2
- package/src/components/Navigation/Nav/Nav.tsx +1 -1
- package/src/components/Navigation/Navbar/Navbar.stories.tsx +3 -3
- package/src/components/Navigation/Navbar/Navbar.tsx +1 -1
- package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +2 -2
- package/src/components/Navigation/SideMenu/SideMenu.tsx +1 -1
- package/src/components/Pagination/Pagination.stories.tsx +1 -1
- package/src/components/Pagination/Pagination.tsx +1 -1
- package/src/components/Popover/Popover.stories.tsx +1 -1
- package/src/components/Popover/Popover.tsx +1 -1
- package/src/components/Progress/Progress.tsx +1 -1
- package/src/components/Rating/Rating.stories.tsx +1 -1
- package/src/components/Rating/Rating.test.tsx +73 -0
- package/src/components/Rating/Rating.tsx +25 -37
- package/src/components/Spinner/Spinner.tsx +1 -1
- package/src/components/Steps/Steps.stories.tsx +1 -1
- package/src/components/Steps/Steps.tsx +2 -2
- package/src/components/Tabs/Tabs.stories.tsx +1 -1
- package/src/components/Tabs/Tabs.tsx +1 -1
- package/src/components/Todo/Todo.tsx +0 -1
- package/src/components/Toggle/Toggle.stories.tsx +1 -1
- package/src/components/Toggle/Toggle.tsx +1 -1
- package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
- package/src/components/VideoPlayer/VideoPlayer.stories.tsx +2 -2
- package/src/lib/composables/__tests__/useAtomixGlassPerf.test.tsx +88 -0
- package/src/lib/composables/__tests__/useChart.test.ts +50 -0
- package/src/lib/composables/__tests__/useChart.test.tsx +139 -0
- package/src/lib/composables/__tests__/useHeroBackgroundSlider.test.tsx +59 -0
- package/src/lib/composables/__tests__/useSliderAutoplay.test.tsx +68 -0
- package/src/lib/composables/atomix-glass/useGlassBackgroundDetection.ts +329 -0
- package/src/lib/composables/atomix-glass/useGlassCornerRadius.ts +82 -0
- package/src/lib/composables/atomix-glass/useGlassMouseTracking.ts +153 -0
- package/src/lib/composables/atomix-glass/useGlassOverLight.ts +198 -0
- package/src/lib/composables/atomix-glass/useGlassSize.ts +117 -0
- package/src/lib/composables/atomix-glass/useGlassState.ts +112 -0
- package/src/lib/composables/atomix-glass/useGlassTransforms.ts +160 -0
- package/src/lib/composables/glass-styles.ts +302 -0
- package/src/lib/composables/index.ts +0 -4
- package/src/lib/composables/useAtomixGlass.ts +331 -522
- package/src/lib/composables/useAtomixGlassStyles.ts +307 -0
- package/src/lib/composables/useBarChart.ts +1 -1
- package/src/lib/composables/useBreadcrumb.ts +6 -6
- package/src/lib/composables/useChart.ts +104 -21
- package/src/lib/composables/useHeroBackgroundSlider.ts +16 -7
- package/src/lib/composables/useSlider.ts +66 -34
- package/src/lib/theme/devtools/CLI.ts +1 -1
- package/src/lib/theme/utils/__tests__/themeUtils.test.ts +213 -0
- package/src/lib/types/components.ts +13 -21
- package/src/lib/utils/__tests__/dom.test.ts +100 -0
- package/src/lib/utils/__tests__/fontPreloader.test.ts +102 -0
- package/src/styles/02-tools/_tools.breakpoints.scss +1 -1
- package/src/styles/02-tools/_tools.utility-api.scss +6 -6
- package/src/styles/06-components/_components.accordion.scss +0 -2
- package/src/styles/06-components/_components.chart.scss +0 -1
- package/src/styles/06-components/_components.dropdown.scss +0 -1
- package/src/styles/06-components/_components.edge-panel.scss +0 -2
- package/src/styles/06-components/_components.photoviewer.scss +0 -1
- package/src/styles/06-components/_components.river.scss +0 -1
- package/src/styles/06-components/_components.slider.scss +0 -3
- package/src/styles/99-utilities/_utilities.glass-fixes.scss +0 -1
- package/src/styles/99-utilities/_utilities.text.scss +1 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { OverLightConfig, OverLightObjectConfig } from '../../types/components';
|
|
3
|
+
|
|
4
|
+
// Module-level shared background detection cache using WeakMap
|
|
5
|
+
// Automatically cleaned up when elements are removed from DOM
|
|
6
|
+
interface BackgroundDetectionCacheEntry {
|
|
7
|
+
result: boolean;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
config: OverLightConfig;
|
|
10
|
+
threshold: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const backgroundDetectionCache = new WeakMap<HTMLElement, BackgroundDetectionCacheEntry>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compare two OverLightConfig values for equality
|
|
17
|
+
* Handles primitives (boolean, 'auto') and objects with deep comparison
|
|
18
|
+
*/
|
|
19
|
+
export const compareOverLightConfig = (
|
|
20
|
+
config1: OverLightConfig,
|
|
21
|
+
config2: OverLightConfig
|
|
22
|
+
): boolean => {
|
|
23
|
+
// Primitive comparison for boolean and 'auto'
|
|
24
|
+
if (typeof config1 !== 'object' || config1 === null) {
|
|
25
|
+
return config1 === config2;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Both must be objects at this point
|
|
29
|
+
if (typeof config2 !== 'object' || config2 === null) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const obj1 = config1 as OverLightObjectConfig;
|
|
34
|
+
const obj2 = config2 as OverLightObjectConfig;
|
|
35
|
+
|
|
36
|
+
// Deep comparison of object properties
|
|
37
|
+
// Compare all defined properties (threshold, opacity, contrast, brightness, saturationBoost)
|
|
38
|
+
const props: (keyof OverLightObjectConfig)[] = [
|
|
39
|
+
'threshold',
|
|
40
|
+
'opacity',
|
|
41
|
+
'contrast',
|
|
42
|
+
'brightness',
|
|
43
|
+
'saturationBoost',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
for (const prop of props) {
|
|
47
|
+
const val1 = obj1[prop];
|
|
48
|
+
const val2 = obj2[prop];
|
|
49
|
+
|
|
50
|
+
// If both are undefined, they're equal for this property
|
|
51
|
+
if (val1 === undefined && val2 === undefined) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If one is undefined and the other isn't, they're different
|
|
56
|
+
if (val1 === undefined || val2 === undefined) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Compare numeric values (handle NaN and floating point precision)
|
|
61
|
+
if (typeof val1 === 'number' && typeof val2 === 'number') {
|
|
62
|
+
// Use Number.isNaN for proper NaN comparison
|
|
63
|
+
if (Number.isNaN(val1) && Number.isNaN(val2)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (Number.isNaN(val1) || Number.isNaN(val2)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
// Compare with small epsilon for floating point numbers
|
|
70
|
+
if (Math.abs(val1 - val2) > Number.EPSILON) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
} else if (val1 !== val2) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get cached background detection result
|
|
83
|
+
*/
|
|
84
|
+
const getCachedBackgroundDetection = (
|
|
85
|
+
parentElement: HTMLElement | null,
|
|
86
|
+
overLightConfig: OverLightConfig
|
|
87
|
+
): boolean | null => {
|
|
88
|
+
if (!parentElement) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const cached = backgroundDetectionCache.get(parentElement);
|
|
93
|
+
if (cached && compareOverLightConfig(cached.config, overLightConfig)) {
|
|
94
|
+
// Update timestamp for LRU-like behavior (though WeakMap doesn't support LRU)
|
|
95
|
+
cached.timestamp = Date.now();
|
|
96
|
+
return cached.result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Set cached background detection result
|
|
104
|
+
*/
|
|
105
|
+
const setCachedBackgroundDetection = (
|
|
106
|
+
parentElement: HTMLElement | null,
|
|
107
|
+
overLightConfig: OverLightConfig,
|
|
108
|
+
result: boolean,
|
|
109
|
+
threshold: number
|
|
110
|
+
): void => {
|
|
111
|
+
if (!parentElement) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
backgroundDetectionCache.set(parentElement, {
|
|
116
|
+
result,
|
|
117
|
+
timestamp: Date.now(),
|
|
118
|
+
config: overLightConfig,
|
|
119
|
+
threshold,
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
interface UseGlassBackgroundDetectionProps {
|
|
124
|
+
glassRef: React.RefObject<HTMLDivElement>;
|
|
125
|
+
overLight: OverLightConfig;
|
|
126
|
+
debugOverLight?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function useGlassBackgroundDetection({
|
|
130
|
+
glassRef,
|
|
131
|
+
overLight,
|
|
132
|
+
debugOverLight = false,
|
|
133
|
+
}: UseGlassBackgroundDetectionProps) {
|
|
134
|
+
const [detectedOverLight, setDetectedOverLight] = useState(false);
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
// Only run auto-detection for 'auto' mode or object config (which uses auto-detection)
|
|
138
|
+
const shouldDetect =
|
|
139
|
+
overLight === 'auto' || (typeof overLight === 'object' && overLight !== null);
|
|
140
|
+
|
|
141
|
+
if (shouldDetect && glassRef.current) {
|
|
142
|
+
const element = glassRef.current;
|
|
143
|
+
const parentElement = element.parentElement;
|
|
144
|
+
|
|
145
|
+
// Check shared cache: skip detection if parent unchanged and config unchanged
|
|
146
|
+
const cachedResult = getCachedBackgroundDetection(parentElement, overLight);
|
|
147
|
+
if (cachedResult !== null) {
|
|
148
|
+
setDetectedOverLight(cachedResult);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const timeoutId = setTimeout(() => {
|
|
153
|
+
try {
|
|
154
|
+
if (!element) {
|
|
155
|
+
setDetectedOverLight(false);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Validate window context
|
|
160
|
+
if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
|
|
161
|
+
setDetectedOverLight(false);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let totalLuminance = 0;
|
|
166
|
+
let validSamples = 0;
|
|
167
|
+
let hasValidBackground = false;
|
|
168
|
+
|
|
169
|
+
let currentElement = element.parentElement;
|
|
170
|
+
let depth = 0;
|
|
171
|
+
const maxDepth = 20;
|
|
172
|
+
const maxSamples = 10;
|
|
173
|
+
|
|
174
|
+
// Limit traversal depth to prevent infinite loops and performance issues
|
|
175
|
+
while (currentElement && validSamples < maxSamples && depth < maxDepth) {
|
|
176
|
+
try {
|
|
177
|
+
const computedStyle = window.getComputedStyle(currentElement);
|
|
178
|
+
if (!computedStyle) {
|
|
179
|
+
currentElement = currentElement.parentElement;
|
|
180
|
+
depth++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const bgColor = computedStyle.backgroundColor;
|
|
185
|
+
const bgImage = computedStyle.backgroundImage;
|
|
186
|
+
|
|
187
|
+
// Check for solid color backgrounds
|
|
188
|
+
if (
|
|
189
|
+
bgColor &&
|
|
190
|
+
bgColor !== 'rgba(0, 0, 0, 0)' &&
|
|
191
|
+
bgColor !== 'transparent' &&
|
|
192
|
+
bgColor !== 'initial' &&
|
|
193
|
+
bgColor !== 'none'
|
|
194
|
+
) {
|
|
195
|
+
const rgb = bgColor.match(/\d+/g);
|
|
196
|
+
if (rgb && rgb.length >= 3) {
|
|
197
|
+
const r = Number(rgb[0]);
|
|
198
|
+
const g = Number(rgb[1]);
|
|
199
|
+
const b = Number(rgb[2]);
|
|
200
|
+
|
|
201
|
+
// Validate RGB values are valid numbers
|
|
202
|
+
if (
|
|
203
|
+
!isNaN(r) &&
|
|
204
|
+
!isNaN(g) &&
|
|
205
|
+
!isNaN(b) &&
|
|
206
|
+
isFinite(r) &&
|
|
207
|
+
isFinite(g) &&
|
|
208
|
+
isFinite(b) &&
|
|
209
|
+
r >= 0 &&
|
|
210
|
+
r <= 255 &&
|
|
211
|
+
g >= 0 &&
|
|
212
|
+
g <= 255 &&
|
|
213
|
+
b >= 0 &&
|
|
214
|
+
b <= 255
|
|
215
|
+
) {
|
|
216
|
+
// Only consider if it's not pure black or very dark
|
|
217
|
+
if (r > 10 || g > 10 || b > 10) {
|
|
218
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
219
|
+
if (!isNaN(luminance) && isFinite(luminance)) {
|
|
220
|
+
totalLuminance += luminance;
|
|
221
|
+
validSamples++;
|
|
222
|
+
hasValidBackground = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check for image backgrounds
|
|
230
|
+
if (bgImage && bgImage !== 'none' && bgImage !== 'initial') {
|
|
231
|
+
// For image backgrounds, assume medium luminance
|
|
232
|
+
totalLuminance += 0.5;
|
|
233
|
+
validSamples++;
|
|
234
|
+
hasValidBackground = true;
|
|
235
|
+
}
|
|
236
|
+
} catch (styleError) {
|
|
237
|
+
// Silently continue if getting computed style fails for this element
|
|
238
|
+
if (typeof process === 'undefined' || process.env?.NODE_ENV === 'development') {
|
|
239
|
+
console.debug('AtomixGlass: Error getting computed style for element:', styleError);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Move to parent element for next iteration
|
|
244
|
+
if (currentElement) {
|
|
245
|
+
currentElement = currentElement.parentElement;
|
|
246
|
+
depth++;
|
|
247
|
+
} else {
|
|
248
|
+
break; // Exit loop if currentElement becomes null
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// More conservative detection with better error handling
|
|
253
|
+
if (hasValidBackground && validSamples > 0) {
|
|
254
|
+
const avgLuminance = totalLuminance / validSamples;
|
|
255
|
+
if (!isNaN(avgLuminance) && isFinite(avgLuminance)) {
|
|
256
|
+
let threshold = 0.7; // Conservative threshold for overlight
|
|
257
|
+
|
|
258
|
+
// If overLight is an object, use its threshold property with validation
|
|
259
|
+
if (typeof overLight === 'object' && overLight !== null) {
|
|
260
|
+
const objConfig = overLight as OverLightObjectConfig;
|
|
261
|
+
if (objConfig.threshold !== undefined) {
|
|
262
|
+
const configThreshold =
|
|
263
|
+
typeof objConfig.threshold === 'number' &&
|
|
264
|
+
!isNaN(objConfig.threshold) &&
|
|
265
|
+
isFinite(objConfig.threshold)
|
|
266
|
+
? objConfig.threshold
|
|
267
|
+
: 0.7;
|
|
268
|
+
threshold = Math.min(0.9, Math.max(0.1, configThreshold));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const isOverLightDetected = avgLuminance > threshold;
|
|
273
|
+
|
|
274
|
+
// Cache the result in shared cache
|
|
275
|
+
setCachedBackgroundDetection(
|
|
276
|
+
element.parentElement,
|
|
277
|
+
overLight,
|
|
278
|
+
isOverLightDetected,
|
|
279
|
+
threshold
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
setDetectedOverLight(isOverLightDetected);
|
|
283
|
+
} else {
|
|
284
|
+
// Invalid luminance calculation, default to false
|
|
285
|
+
const result = false;
|
|
286
|
+
const threshold =
|
|
287
|
+
typeof overLight === 'object' && overLight !== null
|
|
288
|
+
? (overLight as OverLightObjectConfig).threshold || 0.7
|
|
289
|
+
: 0.7;
|
|
290
|
+
setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
|
|
291
|
+
setDetectedOverLight(result);
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
// Default to false if no valid background found
|
|
295
|
+
const result = false;
|
|
296
|
+
const threshold =
|
|
297
|
+
typeof overLight === 'object' && overLight !== null
|
|
298
|
+
? (overLight as OverLightObjectConfig).threshold || 0.7
|
|
299
|
+
: 0.7;
|
|
300
|
+
setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
|
|
301
|
+
setDetectedOverLight(result);
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
// Enhanced error logging with context
|
|
305
|
+
if (typeof process === 'undefined' || process.env?.NODE_ENV === 'development') {
|
|
306
|
+
console.warn('AtomixGlass: Error detecting background brightness:', error);
|
|
307
|
+
}
|
|
308
|
+
const result = false;
|
|
309
|
+
if (element && element.parentElement) {
|
|
310
|
+
const threshold =
|
|
311
|
+
typeof overLight === 'object' && overLight !== null
|
|
312
|
+
? (overLight as OverLightObjectConfig).threshold || 0.7
|
|
313
|
+
: 0.7;
|
|
314
|
+
setCachedBackgroundDetection(element.parentElement, overLight, result, threshold);
|
|
315
|
+
}
|
|
316
|
+
setDetectedOverLight(result);
|
|
317
|
+
}
|
|
318
|
+
}, 150);
|
|
319
|
+
|
|
320
|
+
return () => clearTimeout(timeoutId);
|
|
321
|
+
} else if (typeof overLight === 'boolean') {
|
|
322
|
+
// For boolean values, disable auto-detection
|
|
323
|
+
setDetectedOverLight(false);
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}, [overLight, glassRef, debugOverLight]);
|
|
327
|
+
|
|
328
|
+
return { detectedOverLight };
|
|
329
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { ATOMIX_GLASS } from '../../constants/components';
|
|
3
|
+
import {
|
|
4
|
+
extractBorderRadiusFromChildren,
|
|
5
|
+
extractBorderRadiusFromDOMElement,
|
|
6
|
+
} from '../../../components/AtomixGlass/glass-utils';
|
|
7
|
+
|
|
8
|
+
const { CONSTANTS } = ATOMIX_GLASS;
|
|
9
|
+
|
|
10
|
+
interface UseGlassCornerRadiusProps {
|
|
11
|
+
contentRef: React.RefObject<HTMLDivElement>;
|
|
12
|
+
borderRadius?: number;
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
debugBorderRadius?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useGlassCornerRadius({
|
|
18
|
+
contentRef,
|
|
19
|
+
borderRadius,
|
|
20
|
+
children,
|
|
21
|
+
debugBorderRadius = false,
|
|
22
|
+
}: UseGlassCornerRadiusProps) {
|
|
23
|
+
const [dynamicBorderRadius, setDynamicCornerRadius] = useState<number>(
|
|
24
|
+
CONSTANTS.DEFAULT_CORNER_RADIUS
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const effectiveBorderRadius = useMemo(() => {
|
|
28
|
+
if (borderRadius !== undefined) {
|
|
29
|
+
const result = Math.max(0, borderRadius);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const result = Math.max(0, dynamicBorderRadius);
|
|
34
|
+
return result;
|
|
35
|
+
}, [borderRadius, dynamicBorderRadius]);
|
|
36
|
+
|
|
37
|
+
// Extract border-radius from children
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const extractRadius = () => {
|
|
40
|
+
try {
|
|
41
|
+
let extractedRadius: number | null = null;
|
|
42
|
+
|
|
43
|
+
if (contentRef.current) {
|
|
44
|
+
const firstChild = contentRef.current.firstElementChild as HTMLElement;
|
|
45
|
+
if (firstChild) {
|
|
46
|
+
const domRadius = extractBorderRadiusFromDOMElement(firstChild);
|
|
47
|
+
if (domRadius !== null && domRadius > 0) {
|
|
48
|
+
extractedRadius = domRadius;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (extractedRadius === null) {
|
|
54
|
+
const childRadius = extractBorderRadiusFromChildren(children);
|
|
55
|
+
if (childRadius > 0 && childRadius !== CONSTANTS.DEFAULT_CORNER_RADIUS) {
|
|
56
|
+
extractedRadius = childRadius;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (extractedRadius !== null && extractedRadius > 0) {
|
|
61
|
+
setDynamicCornerRadius(extractedRadius);
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (
|
|
65
|
+
(typeof process === 'undefined' || process.env?.NODE_ENV !== 'production') &&
|
|
66
|
+
debugBorderRadius
|
|
67
|
+
) {
|
|
68
|
+
console.error('[AtomixGlass] Error extracting corner radius:', error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
extractRadius();
|
|
74
|
+
const timeoutId = setTimeout(extractRadius, 100);
|
|
75
|
+
return () => clearTimeout(timeoutId);
|
|
76
|
+
}, [children, debugBorderRadius, contentRef]);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
dynamicBorderRadius,
|
|
80
|
+
effectiveBorderRadius,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { globalMouseTracker } from '../shared-mouse-tracker';
|
|
3
|
+
import { calculateElementCenter } from '../../../components/AtomixGlass/glass-utils';
|
|
4
|
+
import type { MousePosition } from '../../types/components';
|
|
5
|
+
|
|
6
|
+
interface UseGlassMouseTrackingProps {
|
|
7
|
+
glassRef: React.RefObject<HTMLDivElement>;
|
|
8
|
+
mouseContainer?: React.RefObject<HTMLElement | null> | null;
|
|
9
|
+
externalGlobalMousePosition?: MousePosition;
|
|
10
|
+
externalMouseOffset?: MousePosition;
|
|
11
|
+
effectiveWithoutEffects?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useGlassMouseTracking({
|
|
15
|
+
glassRef,
|
|
16
|
+
mouseContainer,
|
|
17
|
+
externalGlobalMousePosition,
|
|
18
|
+
externalMouseOffset,
|
|
19
|
+
effectiveWithoutEffects = false,
|
|
20
|
+
}: UseGlassMouseTrackingProps) {
|
|
21
|
+
const [internalGlobalMousePosition, setInternalGlobalMousePosition] = useState<MousePosition>({
|
|
22
|
+
x: 0,
|
|
23
|
+
y: 0,
|
|
24
|
+
});
|
|
25
|
+
const [internalMouseOffset, setInternalMouseOffset] = useState<MousePosition>({ x: 0, y: 0 });
|
|
26
|
+
|
|
27
|
+
// Mouse tracking using shared global tracker
|
|
28
|
+
// Cache bounding rect to avoid repeated getBoundingClientRect calls
|
|
29
|
+
const cachedRectRef = useRef<DOMRect | null>(null);
|
|
30
|
+
const updateRectRef = useRef<number | null>(null);
|
|
31
|
+
|
|
32
|
+
const globalMousePosition = useMemo(
|
|
33
|
+
() => externalGlobalMousePosition || internalGlobalMousePosition,
|
|
34
|
+
[externalGlobalMousePosition, internalGlobalMousePosition]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const mouseOffset = useMemo(
|
|
38
|
+
() => externalMouseOffset || internalMouseOffset,
|
|
39
|
+
[externalMouseOffset, internalMouseOffset]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Handle mouse position updates from shared tracker
|
|
43
|
+
const handleGlobalMousePosition = useCallback(
|
|
44
|
+
(globalPos: MousePosition) => {
|
|
45
|
+
if (externalGlobalMousePosition && externalMouseOffset) {
|
|
46
|
+
// External mouse position provided, skip internal tracking
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (effectiveWithoutEffects) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const container = mouseContainer?.current || glassRef.current;
|
|
55
|
+
if (!container) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Use cached rect if available, otherwise get new one
|
|
60
|
+
let rect = cachedRectRef.current;
|
|
61
|
+
if (!rect || rect.width === 0 || rect.height === 0) {
|
|
62
|
+
rect = container.getBoundingClientRect();
|
|
63
|
+
cachedRectRef.current = rect;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const center = calculateElementCenter(rect);
|
|
71
|
+
|
|
72
|
+
// Calculate offset relative to this container
|
|
73
|
+
const newOffset = {
|
|
74
|
+
x: ((globalPos.x - center.x) / rect.width) * 100,
|
|
75
|
+
y: ((globalPos.y - center.y) / rect.height) * 100,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// React 18 automatically batches these updates
|
|
79
|
+
setInternalMouseOffset(newOffset);
|
|
80
|
+
setInternalGlobalMousePosition(globalPos);
|
|
81
|
+
},
|
|
82
|
+
[
|
|
83
|
+
mouseContainer,
|
|
84
|
+
glassRef,
|
|
85
|
+
externalGlobalMousePosition,
|
|
86
|
+
externalMouseOffset,
|
|
87
|
+
effectiveWithoutEffects,
|
|
88
|
+
]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Subscribe to shared mouse tracker
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (externalGlobalMousePosition && externalMouseOffset) {
|
|
94
|
+
// External mouse position provided, don't subscribe
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (effectiveWithoutEffects) {
|
|
99
|
+
// Effects disabled, don't subscribe
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Subscribe to shared tracker
|
|
104
|
+
const unsubscribe = globalMouseTracker.subscribe(handleGlobalMousePosition);
|
|
105
|
+
|
|
106
|
+
// Update cached rect when container size changes
|
|
107
|
+
const updateRect = () => {
|
|
108
|
+
if (updateRectRef.current !== null) {
|
|
109
|
+
cancelAnimationFrame(updateRectRef.current);
|
|
110
|
+
}
|
|
111
|
+
updateRectRef.current = requestAnimationFrame(() => {
|
|
112
|
+
const container = mouseContainer?.current || glassRef.current;
|
|
113
|
+
if (container) {
|
|
114
|
+
cachedRectRef.current = container.getBoundingClientRect();
|
|
115
|
+
}
|
|
116
|
+
updateRectRef.current = null;
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Use ResizeObserver to update cached rect when container size changes
|
|
121
|
+
const container = mouseContainer?.current || glassRef.current;
|
|
122
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
123
|
+
|
|
124
|
+
if (container && typeof ResizeObserver !== 'undefined') {
|
|
125
|
+
resizeObserver = new ResizeObserver(updateRect);
|
|
126
|
+
resizeObserver.observe(container);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
unsubscribe();
|
|
131
|
+
if (updateRectRef.current !== null) {
|
|
132
|
+
cancelAnimationFrame(updateRectRef.current);
|
|
133
|
+
updateRectRef.current = null;
|
|
134
|
+
}
|
|
135
|
+
if (resizeObserver) {
|
|
136
|
+
resizeObserver.disconnect();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}, [
|
|
140
|
+
handleGlobalMousePosition,
|
|
141
|
+
mouseContainer,
|
|
142
|
+
glassRef,
|
|
143
|
+
externalGlobalMousePosition,
|
|
144
|
+
externalMouseOffset,
|
|
145
|
+
effectiveWithoutEffects,
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
globalMousePosition,
|
|
150
|
+
mouseOffset,
|
|
151
|
+
cachedRectRef,
|
|
152
|
+
};
|
|
153
|
+
}
|