@shohojdhara/atomix 0.3.0 → 0.3.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/CHANGELOG.md +0 -1
- package/README.md +3 -5
- package/dist/atomix.css +753 -643
- package/dist/atomix.min.css +3 -5
- package/dist/index.d.ts +3075 -247
- package/dist/index.esm.js +20412 -16601
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +20379 -16605
- 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 -11
- package/src/components/AtomixGlass/AtomixGlass.test.tsx +21 -32
- package/src/components/AtomixGlass/AtomixGlass.tsx +55 -42
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +205 -57
- package/src/components/AtomixGlass/GlassFilter.tsx +22 -8
- package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +221 -0
- package/src/components/AtomixGlass/atomixGLass.old.tsx +0 -3
- package/src/components/AtomixGlass/shader-utils.ts +8 -0
- package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +319 -100
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +601 -105
- package/src/components/AtomixGlass/stories/Modes.stories.tsx +30 -12
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +173 -38
- package/src/components/AtomixGlass/stories/ShaderVariants.stories.tsx +18 -18
- package/src/components/AtomixGlass/stories/shared-components.tsx +27 -5
- package/src/components/Button/Button.tsx +62 -17
- package/src/components/Callout/Callout.test.tsx +8 -14
- package/src/components/Card/Card.tsx +103 -1
- package/src/components/Card/index.ts +3 -2
- package/src/components/Icon/index.ts +1 -1
- package/src/components/Modal/Modal.stories.tsx +29 -38
- package/src/components/Modal/Modal.tsx +4 -4
- package/src/components/Navigation/SideMenu/SideMenu.tsx +49 -41
- package/src/components/Navigation/SideMenu/SideMenuItem.tsx +63 -24
- package/src/components/Popover/Popover.tsx +1 -1
- package/src/components/VideoPlayer/VideoPlayer.stories.tsx +977 -400
- package/src/components/VideoPlayer/VideoPlayer.tsx +1 -6
- package/src/lib/composables/shared-mouse-tracker.ts +133 -0
- package/src/lib/composables/useAtomixGlass.ts +333 -145
- package/src/lib/index.ts +1 -4
- package/src/lib/theme/composeTheme.ts +375 -0
- package/src/lib/theme/config/index.ts +21 -0
- package/src/lib/theme/config/loader.ts +276 -0
- package/src/lib/theme/config/types.ts +98 -0
- package/src/lib/theme/config/validator.ts +326 -0
- package/src/lib/theme/constants.ts +183 -0
- package/src/lib/theme/core/ThemeCache.ts +283 -0
- package/src/lib/theme/core/ThemeEngine.test.ts +146 -0
- package/src/lib/theme/core/ThemeEngine.ts +657 -0
- package/src/lib/theme/core/ThemeRegistry.ts +284 -0
- package/src/lib/theme/core/ThemeValidator.ts +530 -0
- package/src/lib/theme/core/index.ts +24 -0
- package/src/lib/theme/createTheme.ts +521 -0
- package/src/lib/theme/devtools/CLI.ts +279 -0
- package/src/lib/theme/devtools/Inspector.tsx +594 -0
- package/src/lib/theme/devtools/Preview.tsx +392 -0
- package/src/lib/theme/devtools/index.ts +21 -0
- package/src/lib/theme/errors.test.ts +207 -0
- package/src/lib/theme/errors.ts +233 -0
- package/src/lib/theme/generateCSSVariables.ts +797 -0
- package/src/lib/theme/generators/CSSGenerator.ts +311 -0
- package/src/lib/theme/generators/ConfigGenerator.ts +287 -0
- package/src/lib/theme/generators/TypeGenerator.ts +228 -0
- package/src/lib/theme/generators/index.ts +21 -0
- package/src/lib/theme/i18n/index.ts +9 -0
- package/src/lib/theme/i18n/rtl.ts +325 -0
- package/src/lib/theme/index.ts +221 -10
- package/src/lib/theme/monitoring/ThemeAnalytics.ts +409 -0
- package/src/lib/theme/monitoring/index.ts +17 -0
- package/src/lib/theme/overrides/ComponentOverrides.ts +243 -0
- package/src/lib/theme/overrides/index.ts +15 -0
- package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +233 -0
- package/src/lib/theme/runtime/ThemeManager.test.ts +176 -0
- package/src/lib/theme/runtime/ThemeManager.ts +442 -0
- package/src/lib/theme/runtime/ThemeProvider.tsx +318 -0
- package/src/lib/theme/runtime/index.ts +17 -0
- package/src/lib/theme/runtime/useTheme.ts +52 -0
- package/src/lib/theme/studio/ThemeStudio.tsx +312 -0
- package/src/lib/theme/studio/index.ts +8 -0
- package/src/lib/theme/themeUtils.ts +333 -0
- package/src/lib/theme/types.ts +340 -9
- package/src/lib/theme/utils.ts +23 -22
- package/src/lib/theme/whitelabel/WhiteLabelManager.ts +364 -0
- package/src/lib/theme/whitelabel/index.ts +13 -0
- package/src/lib/types/components.ts +148 -59
- package/src/styles/01-settings/_index.scss +2 -2
- package/src/styles/01-settings/_settings.badge.scss +3 -3
- package/src/styles/01-settings/_settings.border-radius.scss +1 -1
- package/src/styles/01-settings/_settings.callout.scss +1 -1
- package/src/styles/01-settings/_settings.card.scss +1 -1
- package/src/styles/01-settings/{_settings.maps.scss → _settings.design-tokens.scss} +163 -49
- package/src/styles/01-settings/_settings.input.scss +1 -1
- package/src/styles/01-settings/_settings.modal.scss +1 -1
- package/src/styles/01-settings/_settings.navbar.scss +1 -1
- package/src/styles/01-settings/_settings.spacing.scss +14 -13
- package/src/styles/01-settings/_settings.upload.scss +1 -1
- package/src/styles/03-generic/_generic.root.scss +131 -50
- package/src/styles/05-objects/_objects.block.scss +1 -1
- package/src/styles/06-components/_components.atomix-glass.scss +20 -22
- package/src/styles/06-components/_components.badge.scss +2 -2
- package/src/styles/06-components/_components.button.scss +1 -1
- package/src/styles/06-components/_components.callout.scss +1 -1
- package/src/styles/06-components/_components.card.scss +74 -2
- package/src/styles/06-components/_components.chart.scss +3 -3
- package/src/styles/06-components/_components.dropdown.scss +6 -0
- package/src/styles/06-components/_components.footer.scss +1 -1
- package/src/styles/06-components/_components.list-group.scss +1 -1
- package/src/styles/06-components/_components.list.scss +1 -1
- package/src/styles/06-components/_components.menu.scss +1 -1
- package/src/styles/06-components/_components.messages.scss +1 -1
- package/src/styles/06-components/_components.modal.scss +7 -2
- package/src/styles/06-components/_components.navbar.scss +1 -1
- package/src/styles/06-components/_components.popover.scss +10 -0
- package/src/styles/06-components/_components.product-review.scss +1 -1
- package/src/styles/06-components/_components.progress.scss +1 -1
- package/src/styles/06-components/_components.rating.scss +1 -1
- package/src/styles/06-components/_components.spinner.scss +1 -1
- package/src/styles/99-utilities/_utilities.background.scss +1 -1
- package/src/styles/99-utilities/_utilities.border.scss +28 -59
- package/src/styles/99-utilities/_utilities.gradient.scss +12 -0
- package/src/styles/99-utilities/_utilities.link.scss +1 -1
- package/src/styles/99-utilities/_utilities.position.scss +8 -15
- package/src/styles/99-utilities/_utilities.scss +2 -0
- package/src/styles/99-utilities/_utilities.spacing.scss +76 -121
- package/src/styles/99-utilities/_utilities.text.scss +31 -50
- package/dist/themes/applemix.css +0 -15411
- package/dist/themes/applemix.min.css +0 -72
- package/dist/themes/boomdevs.css +0 -15001
- package/dist/themes/boomdevs.min.css +0 -405
- package/dist/themes/esrar.css +0 -17195
- package/dist/themes/esrar.min.css +0 -189
- package/dist/themes/flashtrade.css +0 -16408
- package/dist/themes/flashtrade.min.css +0 -192
- package/dist/themes/mashroom.css +0 -29900
- package/dist/themes/mashroom.min.css +0 -403
- package/dist/themes/shaj-default.css +0 -16024
- package/dist/themes/shaj-default.min.css +0 -500
- package/src/lib/theme/ThemeManager.stories.tsx +0 -472
- package/src/lib/theme/ThemeManager.test.ts +0 -186
- package/src/lib/theme/ThemeManager.ts +0 -501
- package/src/lib/theme/ThemeProvider.tsx +0 -227
- package/src/lib/theme/useTheme.test.tsx +0 -66
- package/src/lib/theme/useTheme.ts +0 -80
- package/src/lib/theme/utils.test.ts +0 -140
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shohojdhara/atomix",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Atomix Design System - A modern component library for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -72,22 +72,12 @@
|
|
|
72
72
|
"require": "./src/styles/99-utilities/_index.scss",
|
|
73
73
|
"default": "./src/styles/99-utilities/_index.scss"
|
|
74
74
|
},
|
|
75
|
-
"./themes/*": {
|
|
76
|
-
"import": "./dist/themes/*",
|
|
77
|
-
"require": "./dist/themes/*",
|
|
78
|
-
"default": "./dist/themes/*"
|
|
79
|
-
},
|
|
80
75
|
"./theme": {
|
|
81
76
|
"types": "./dist/lib/theme/index.d.ts",
|
|
82
77
|
"import": "./dist/lib/theme/index.js",
|
|
83
78
|
"require": "./dist/lib/theme/index.js",
|
|
84
79
|
"default": "./dist/lib/theme/index.js"
|
|
85
80
|
},
|
|
86
|
-
"./themes/config": {
|
|
87
|
-
"import": "./src/themes/themes.config.js",
|
|
88
|
-
"require": "./src/themes/themes.config.js",
|
|
89
|
-
"default": "./src/themes/themes.config.js"
|
|
90
|
-
},
|
|
91
81
|
"./package.json": "./package.json"
|
|
92
82
|
},
|
|
93
83
|
"dependencies": {
|
|
@@ -10,7 +10,7 @@ vi.mock('./shader-utils', () => ({
|
|
|
10
10
|
updateShader() {
|
|
11
11
|
return 'data:image/png;base64,mockBase64String';
|
|
12
12
|
}
|
|
13
|
-
destroy() {}
|
|
13
|
+
destroy() { }
|
|
14
14
|
},
|
|
15
15
|
fragmentShaders: {
|
|
16
16
|
liquidGlass: vi.fn(),
|
|
@@ -40,14 +40,14 @@ describe('AtomixGlass Component', () => {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test('renders with showHoverEffects enabled', () => {
|
|
43
|
-
render(
|
|
43
|
+
const { container } = render(
|
|
44
44
|
<AtomixGlass>
|
|
45
45
|
<div>Test Content</div>
|
|
46
46
|
</AtomixGlass>
|
|
47
47
|
);
|
|
48
48
|
|
|
49
|
-
// Check that
|
|
50
|
-
expect(
|
|
49
|
+
// Check that the glass component renders without errors
|
|
50
|
+
expect(container.querySelector('.c-atomix-glass')).toBeInTheDocument();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
test('applies clickable class when onClick is provided', () => {
|
|
@@ -58,9 +58,8 @@ describe('AtomixGlass Component', () => {
|
|
|
58
58
|
</AtomixGlass>
|
|
59
59
|
);
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
61
|
+
// The component should have role="button" when onClick is provided
|
|
62
|
+
expect(container.querySelector('.c-atomix-glass')).toHaveAttribute('role', 'button');
|
|
64
63
|
});
|
|
65
64
|
|
|
66
65
|
test('calls onClick when clicked', async () => {
|
|
@@ -168,7 +167,7 @@ describe('AtomixGlass Component', () => {
|
|
|
168
167
|
);
|
|
169
168
|
|
|
170
169
|
const glassContainer = container.querySelector('.c-atomix-glass__container');
|
|
171
|
-
expect(glassContainer).toHaveStyle('background-color:
|
|
170
|
+
expect(glassContainer).toHaveStyle('background-color: rgb(255, 0, 0)');
|
|
172
171
|
});
|
|
173
172
|
|
|
174
173
|
test('uses standard mode by default', () => {
|
|
@@ -183,38 +182,28 @@ describe('AtomixGlass Component', () => {
|
|
|
183
182
|
});
|
|
184
183
|
|
|
185
184
|
test('handles mouse events correctly', async () => {
|
|
186
|
-
const
|
|
187
|
-
const handleMouseLeave = vi.fn();
|
|
188
|
-
const handleMouseDown = vi.fn();
|
|
189
|
-
const handleMouseUp = vi.fn();
|
|
185
|
+
const handleClick = vi.fn();
|
|
190
186
|
|
|
191
|
-
render(
|
|
192
|
-
<AtomixGlass
|
|
193
|
-
onClick={() => {
|
|
194
|
-
handleMouseEnter();
|
|
195
|
-
handleMouseLeave();
|
|
196
|
-
handleMouseDown();
|
|
197
|
-
handleMouseUp();
|
|
198
|
-
}}
|
|
199
|
-
>
|
|
187
|
+
const { container } = render(
|
|
188
|
+
<AtomixGlass onClick={handleClick}>
|
|
200
189
|
<div>Content</div>
|
|
201
190
|
</AtomixGlass>
|
|
202
191
|
);
|
|
203
192
|
|
|
204
|
-
const
|
|
205
|
-
if (!
|
|
206
|
-
|
|
207
|
-
await userEvent.hover(glassContent);
|
|
208
|
-
expect(handleMouseEnter).toHaveBeenCalledTimes(1);
|
|
193
|
+
const glassContainer = container.querySelector('.c-atomix-glass__container');
|
|
194
|
+
if (!glassContainer) throw new Error('Glass container not found');
|
|
209
195
|
|
|
210
|
-
|
|
211
|
-
|
|
196
|
+
// Test mouse enter/leave by hovering
|
|
197
|
+
await userEvent.hover(glassContainer);
|
|
198
|
+
// Just verify it doesn't throw
|
|
199
|
+
expect(glassContainer).toBeInTheDocument();
|
|
212
200
|
|
|
213
|
-
await userEvent.
|
|
214
|
-
expect(
|
|
201
|
+
await userEvent.unhover(glassContainer);
|
|
202
|
+
expect(glassContainer).toBeInTheDocument();
|
|
215
203
|
|
|
216
|
-
|
|
217
|
-
|
|
204
|
+
// Test click
|
|
205
|
+
await userEvent.click(glassContainer);
|
|
206
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
218
207
|
});
|
|
219
208
|
});
|
|
220
209
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useMemo, useRef } from 'react';
|
|
1
|
+
import React, { useMemo, useRef, useEffect } from 'react';
|
|
2
2
|
import type { AtomixGlassProps, GlassSize } from '../../lib/types/components';
|
|
3
3
|
import { ATOMIX_GLASS } from '../../lib/constants/components';
|
|
4
4
|
import { AtomixGlassContainer } from './AtomixGlassContainer';
|
|
@@ -109,6 +109,14 @@ export function AtomixGlass({
|
|
|
109
109
|
}: AtomixGlassProps) {
|
|
110
110
|
const glassRef = useRef<HTMLDivElement>(null);
|
|
111
111
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
112
|
+
|
|
113
|
+
// Cache CSS custom property values to avoid reading on every render
|
|
114
|
+
const opacityCacheRef = useRef<{
|
|
115
|
+
opacity50: number;
|
|
116
|
+
opacity40: number;
|
|
117
|
+
opacity80: number;
|
|
118
|
+
opacity0: number;
|
|
119
|
+
} | null>(null);
|
|
112
120
|
|
|
113
121
|
// Use composable hook for all state and logic
|
|
114
122
|
const {
|
|
@@ -151,24 +159,48 @@ export function AtomixGlass({
|
|
|
151
159
|
const isOverLight = overLightConfig.isOverLight;
|
|
152
160
|
const shouldRenderOverLightLayers = enableOverLightLayers && isOverLight;
|
|
153
161
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
(
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
// Read CSS custom properties once on mount and cache them
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (typeof window !== 'undefined' && glassRef.current && !opacityCacheRef.current) {
|
|
165
|
+
try {
|
|
166
|
+
const computedStyle = window.getComputedStyle(glassRef.current);
|
|
167
|
+
const opacity50Value = computedStyle.getPropertyValue('--atomix-opacity-50').trim();
|
|
168
|
+
const opacity40Value = computedStyle.getPropertyValue('--atomix-opacity-40').trim();
|
|
169
|
+
const opacity80Value = computedStyle.getPropertyValue('--atomix-opacity-80').trim();
|
|
170
|
+
const opacity0Value = computedStyle.getPropertyValue('--atomix-opacity-0').trim();
|
|
171
|
+
|
|
172
|
+
// Parse opacity values, handling 0 correctly (use NaN check instead of falsy check)
|
|
173
|
+
const parseOpacity = (value: string, defaultValue: number): number => {
|
|
174
|
+
if (!value) return defaultValue;
|
|
175
|
+
const parsed = parseFloat(value);
|
|
176
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
opacityCacheRef.current = {
|
|
180
|
+
opacity50: parseOpacity(opacity50Value, 0.5),
|
|
181
|
+
opacity40: parseOpacity(opacity40Value, 0.4),
|
|
182
|
+
opacity80: parseOpacity(opacity80Value, 0.8),
|
|
183
|
+
opacity0: parseOpacity(opacity0Value, 0),
|
|
184
|
+
};
|
|
185
|
+
} catch (error) {
|
|
186
|
+
// Fallback to defaults if reading fails
|
|
187
|
+
opacityCacheRef.current = {
|
|
188
|
+
opacity50: 0.5,
|
|
189
|
+
opacity40: 0.4,
|
|
190
|
+
opacity80: 0.8,
|
|
191
|
+
opacity0: 0,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}, []);
|
|
196
|
+
|
|
159
197
|
|
|
160
198
|
// Calculate base style with transforms (only dynamic values)
|
|
161
|
-
// Performance: willChange is set only when transforms are active and effects are enabled
|
|
162
199
|
const baseStyle = useMemo(
|
|
163
200
|
() => ({
|
|
164
201
|
...style,
|
|
165
202
|
...(elasticity !== 0 && !effectiveDisableEffects && {
|
|
166
203
|
transform: transformStyle,
|
|
167
|
-
willChange: 'transform',
|
|
168
|
-
}),
|
|
169
|
-
// Reset willChange when effects are disabled to allow browser optimization
|
|
170
|
-
...(effectiveDisableEffects && {
|
|
171
|
-
willChange: 'auto',
|
|
172
204
|
}),
|
|
173
205
|
}),
|
|
174
206
|
[style, transformStyle, effectiveDisableEffects, elasticity]
|
|
@@ -235,10 +267,12 @@ export function AtomixGlass({
|
|
|
235
267
|
const mouseOffsetX = mouseOffset.x;
|
|
236
268
|
const mouseOffsetY = mouseOffset.y;
|
|
237
269
|
|
|
270
|
+
// Extract constants outside useMemo to avoid recreating on every render
|
|
271
|
+
const GRADIENT = ATOMIX_GLASS.CONSTANTS.GRADIENT;
|
|
272
|
+
|
|
238
273
|
const gradientCalculations = useMemo(() => {
|
|
239
274
|
const mx = mouseOffsetX;
|
|
240
275
|
const my = mouseOffsetY;
|
|
241
|
-
const { GRADIENT } = ATOMIX_GLASS.CONSTANTS;
|
|
242
276
|
|
|
243
277
|
// Calculate gradient angles and stops (optimized)
|
|
244
278
|
const borderGradientAngle =
|
|
@@ -296,38 +330,19 @@ export function AtomixGlass({
|
|
|
296
330
|
baseX,
|
|
297
331
|
baseY,
|
|
298
332
|
};
|
|
299
|
-
}, [mouseOffsetX, mouseOffsetY, isOverLight]);
|
|
333
|
+
}, [mouseOffsetX, mouseOffsetY, isOverLight]); // GRADIENT is constant, no need to include
|
|
300
334
|
|
|
301
335
|
// Memoize opacity values separately - using design token values where applicable
|
|
302
336
|
// Optimize: extract overLightConfig.opacity to avoid depending on whole object
|
|
303
337
|
const overLightOpacity = overLightConfig.opacity;
|
|
304
338
|
|
|
305
|
-
//
|
|
339
|
+
// Use cached opacity values from CSS custom properties (read once on mount)
|
|
306
340
|
const opacityValues = useMemo(() => {
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
let opacity0 = 0;
|
|
313
|
-
|
|
314
|
-
// Try to read from CSS custom properties if available (SSR-safe)
|
|
315
|
-
if (typeof window !== 'undefined' && glassRef.current) {
|
|
316
|
-
try {
|
|
317
|
-
const computedStyle = window.getComputedStyle(glassRef.current);
|
|
318
|
-
const opacity50Value = computedStyle.getPropertyValue('--atomix-opacity-50').trim();
|
|
319
|
-
const opacity40Value = computedStyle.getPropertyValue('--atomix-opacity-40').trim();
|
|
320
|
-
const opacity80Value = computedStyle.getPropertyValue('--atomix-opacity-80').trim();
|
|
321
|
-
const opacity0Value = computedStyle.getPropertyValue('--atomix-opacity-0').trim();
|
|
322
|
-
|
|
323
|
-
if (opacity50Value) opacity50 = parseFloat(opacity50Value) || 0.5;
|
|
324
|
-
if (opacity40Value) opacity40 = parseFloat(opacity40Value) || 0.4;
|
|
325
|
-
if (opacity80Value) opacity80 = parseFloat(opacity80Value) || 0.8;
|
|
326
|
-
if (opacity0Value) opacity0 = parseFloat(opacity0Value) || 0;
|
|
327
|
-
} catch (error) {
|
|
328
|
-
// Fallback to defaults if reading fails
|
|
329
|
-
}
|
|
330
|
-
}
|
|
341
|
+
// Use cached values if available, otherwise fallback to defaults
|
|
342
|
+
const opacity50 = opacityCacheRef.current?.opacity50 ?? 0.5;
|
|
343
|
+
const opacity40 = opacityCacheRef.current?.opacity40 ?? 0.4;
|
|
344
|
+
const opacity80 = opacityCacheRef.current?.opacity80 ?? 0.8;
|
|
345
|
+
const opacity0 = opacityCacheRef.current?.opacity0 ?? 0;
|
|
331
346
|
|
|
332
347
|
const BASE_OVER_LIGHT_OPACITY = opacity40; // Uses design token
|
|
333
348
|
const OVER_OPACITY_MULTIPLIER = 1.1; // Dynamic multiplier for overlay
|
|
@@ -339,7 +354,7 @@ export function AtomixGlass({
|
|
|
339
354
|
base: isOverLight ? overLightOpacity || BASE_OVER_LIGHT_OPACITY : opacity0,
|
|
340
355
|
over: isOverLight ? (overLightOpacity || BASE_OVER_LIGHT_OPACITY) * OVER_OPACITY_MULTIPLIER : opacity0,
|
|
341
356
|
};
|
|
342
|
-
}, [isHovered, isActive, isOverLight, overLightOpacity
|
|
357
|
+
}, [isHovered, isActive, isOverLight, overLightOpacity]);
|
|
343
358
|
|
|
344
359
|
// Generate CSS variables for layers (only dynamic values)
|
|
345
360
|
// Optimize: extract specific properties from objects to minimize dependencies
|
|
@@ -389,7 +404,6 @@ export function AtomixGlass({
|
|
|
389
404
|
// Standard CSS custom properties for dynamic values
|
|
390
405
|
'--atomix-glass-radius': `${effectiveCornerRadius}px`,
|
|
391
406
|
'--atomix-glass-transform': baseStyleTransform || 'none',
|
|
392
|
-
'--atomix-glass-transition': effectiveReducedMotion ? 'none' : transitionDuration,
|
|
393
407
|
'--atomix-glass-position': positionStylesPosition,
|
|
394
408
|
'--atomix-glass-top': positionStylesTop !== 'fixed' ? `${positionStylesTop}px` : '0',
|
|
395
409
|
'--atomix-glass-left': positionStylesLeft !== 'fixed' ? `${positionStylesLeft}px` : '0',
|
|
@@ -439,7 +453,6 @@ export function AtomixGlass({
|
|
|
439
453
|
// Other values
|
|
440
454
|
effectiveCornerRadius,
|
|
441
455
|
effectiveReducedMotion,
|
|
442
|
-
transitionDuration,
|
|
443
456
|
// Gradient calculations - extracted properties
|
|
444
457
|
gradientIsOverLight,
|
|
445
458
|
gradientMx,
|
|
@@ -2,7 +2,6 @@ import React, { forwardRef, useId, useRef, useState, useEffect, useMemo } from '
|
|
|
2
2
|
import type { CSSProperties } from 'react';
|
|
3
3
|
import type { DisplacementMode, MousePosition, GlassSize } from '../../lib/types/components';
|
|
4
4
|
import type { FragmentShaderType } from './shader-utils';
|
|
5
|
-
import { ShaderDisplacementGenerator, fragmentShaders } from './shader-utils';
|
|
6
5
|
import { GlassFilter } from './GlassFilter';
|
|
7
6
|
import {
|
|
8
7
|
calculateElementCenter,
|
|
@@ -15,6 +14,52 @@ import { ATOMIX_GLASS } from '../../lib/constants/components';
|
|
|
15
14
|
|
|
16
15
|
const { CONSTANTS } = ATOMIX_GLASS;
|
|
17
16
|
|
|
17
|
+
// Module-level shared shader cache with LRU eviction
|
|
18
|
+
const MAX_CACHE_SIZE = 15;
|
|
19
|
+
interface ShaderCacheEntry {
|
|
20
|
+
url: string;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
}
|
|
23
|
+
const sharedShaderCache = new Map<string, ShaderCacheEntry>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get cached shader URL, updating access timestamp
|
|
27
|
+
*/
|
|
28
|
+
const getCachedShader = (key: string): string | null => {
|
|
29
|
+
const entry = sharedShaderCache.get(key);
|
|
30
|
+
if (entry) {
|
|
31
|
+
// Update access timestamp for LRU
|
|
32
|
+
entry.timestamp = Date.now();
|
|
33
|
+
return entry.url;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Set cached shader URL with LRU eviction
|
|
40
|
+
*/
|
|
41
|
+
const setCachedShader = (key: string, url: string): void => {
|
|
42
|
+
// Evict oldest entries if at capacity
|
|
43
|
+
if (sharedShaderCache.size >= MAX_CACHE_SIZE) {
|
|
44
|
+
const entries = Array.from(sharedShaderCache.entries());
|
|
45
|
+
// Sort by timestamp (oldest first)
|
|
46
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
47
|
+
// Remove oldest entry
|
|
48
|
+
const oldestEntry = entries[0];
|
|
49
|
+
if (oldestEntry) {
|
|
50
|
+
sharedShaderCache.delete(oldestEntry[0]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
sharedShaderCache.set(key, { url, timestamp: Date.now() });
|
|
54
|
+
|
|
55
|
+
// Development mode: log cache size
|
|
56
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
57
|
+
if (sharedShaderCache.size >= MAX_CACHE_SIZE * 0.8) {
|
|
58
|
+
console.log(`AtomixGlass: Shader cache size: ${sharedShaderCache.size}/${MAX_CACHE_SIZE}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
18
63
|
interface AtomixGlassContainerProps {
|
|
19
64
|
className?: string;
|
|
20
65
|
style?: React.CSSProperties;
|
|
@@ -85,32 +130,108 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
85
130
|
},
|
|
86
131
|
ref
|
|
87
132
|
) => {
|
|
133
|
+
// Use React's useId() for SSR compatibility
|
|
134
|
+
// Note: In Next.js, IDs may differ between server and client
|
|
135
|
+
// We'll suppress hydration warnings on elements that use this ID
|
|
88
136
|
const filterId = useId();
|
|
137
|
+
|
|
89
138
|
const [shaderMapUrl, setShaderMapUrl] = useState<string>('');
|
|
90
|
-
const shaderGeneratorRef = useRef<
|
|
139
|
+
const shaderGeneratorRef = useRef<any>(null);
|
|
140
|
+
const shaderUtilsRef = useRef<{
|
|
141
|
+
ShaderDisplacementGenerator: any;
|
|
142
|
+
fragmentShaders: any;
|
|
143
|
+
} | null>(null);
|
|
144
|
+
|
|
145
|
+
// Use shared module-level cache (no per-instance cache needed)
|
|
146
|
+
const shaderDebounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
147
|
+
|
|
148
|
+
// Lazy load shader utilities only when shader mode is needed
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (mode === 'shader') {
|
|
151
|
+
// Dynamic import shader utilities
|
|
152
|
+
import('./shader-utils').then((shaderUtils) => {
|
|
153
|
+
shaderUtilsRef.current = {
|
|
154
|
+
ShaderDisplacementGenerator: shaderUtils.ShaderDisplacementGenerator,
|
|
155
|
+
fragmentShaders: shaderUtils.fragmentShaders,
|
|
156
|
+
};
|
|
157
|
+
}).catch((error) => {
|
|
158
|
+
console.warn('AtomixGlassContainer: Error loading shader utilities', error);
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
// Clear shader utils when not in shader mode to free memory
|
|
162
|
+
shaderUtilsRef.current = null;
|
|
163
|
+
}
|
|
164
|
+
}, [mode]);
|
|
91
165
|
|
|
92
|
-
// Generate
|
|
166
|
+
// Generate shader map with debouncing and caching
|
|
93
167
|
useEffect(() => {
|
|
94
168
|
// Enhanced validation for shader mode
|
|
95
|
-
if (mode === 'shader' && glassSize && validateGlassSize(glassSize)) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const url = shaderGeneratorRef.current.updateShader();
|
|
105
|
-
setShaderMapUrl(url);
|
|
106
|
-
} catch (error) {
|
|
107
|
-
console.warn('AtomixGlassContainer: Error generating shader map', error);
|
|
108
|
-
setShaderMapUrl(''); // Fallback to empty string
|
|
169
|
+
if (mode === 'shader' && glassSize && validateGlassSize(glassSize) && shaderUtilsRef.current) {
|
|
170
|
+
// Create cache key from size and variant
|
|
171
|
+
const cacheKey = `${glassSize.width}x${glassSize.height}-${shaderVariant}`;
|
|
172
|
+
|
|
173
|
+
// Check shared cache first
|
|
174
|
+
const cachedUrl = getCachedShader(cacheKey);
|
|
175
|
+
if (cachedUrl) {
|
|
176
|
+
setShaderMapUrl(cachedUrl);
|
|
177
|
+
return;
|
|
109
178
|
}
|
|
179
|
+
|
|
180
|
+
// Clear any pending debounce
|
|
181
|
+
if (shaderDebounceTimeoutRef.current) {
|
|
182
|
+
clearTimeout(shaderDebounceTimeoutRef.current);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Debounce shader generation to avoid blocking on rapid size changes
|
|
186
|
+
const generateShader = () => {
|
|
187
|
+
if (!shaderUtilsRef.current) {
|
|
188
|
+
// Shader utils not loaded yet, retry after a short delay
|
|
189
|
+
shaderDebounceTimeoutRef.current = setTimeout(generateShader, 100);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const { ShaderDisplacementGenerator, fragmentShaders } = shaderUtilsRef.current;
|
|
195
|
+
shaderGeneratorRef.current?.destroy();
|
|
196
|
+
const selectedShader = fragmentShaders[shaderVariant] || fragmentShaders.liquidGlass;
|
|
197
|
+
shaderGeneratorRef.current = new ShaderDisplacementGenerator({
|
|
198
|
+
width: glassSize.width,
|
|
199
|
+
height: glassSize.height,
|
|
200
|
+
fragment: selectedShader,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Use requestIdleCallback if available for non-blocking generation
|
|
204
|
+
const generate = () => {
|
|
205
|
+
const url = shaderGeneratorRef.current?.updateShader() || '';
|
|
206
|
+
setCachedShader(cacheKey, url);
|
|
207
|
+
setShaderMapUrl(url);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (typeof requestIdleCallback !== 'undefined') {
|
|
211
|
+
requestIdleCallback(generate, { timeout: 1000 });
|
|
212
|
+
} else {
|
|
213
|
+
// Fallback to setTimeout for browsers without requestIdleCallback
|
|
214
|
+
setTimeout(generate, 0);
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.warn('AtomixGlassContainer: Error generating shader map', error);
|
|
218
|
+
setShaderMapUrl(''); // Fallback to empty string
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Debounce with 300ms delay
|
|
223
|
+
shaderDebounceTimeoutRef.current = setTimeout(generateShader, 300);
|
|
224
|
+
} else {
|
|
225
|
+
// Not in shader mode, clear URL
|
|
226
|
+
setShaderMapUrl('');
|
|
110
227
|
}
|
|
111
228
|
|
|
112
229
|
// Cleanup function with error handling
|
|
113
230
|
return () => {
|
|
231
|
+
if (shaderDebounceTimeoutRef.current) {
|
|
232
|
+
clearTimeout(shaderDebounceTimeoutRef.current);
|
|
233
|
+
shaderDebounceTimeoutRef.current = null;
|
|
234
|
+
}
|
|
114
235
|
try {
|
|
115
236
|
shaderGeneratorRef.current?.destroy();
|
|
116
237
|
} catch (error) {
|
|
@@ -121,23 +242,7 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
121
242
|
};
|
|
122
243
|
}, [mode, glassSize, shaderVariant]);
|
|
123
244
|
|
|
124
|
-
|
|
125
|
-
if (!ref || typeof ref === 'function') return undefined;
|
|
126
|
-
|
|
127
|
-
const element = (ref as React.RefObject<HTMLDivElement>).current;
|
|
128
|
-
if (!element) return undefined;
|
|
129
|
-
|
|
130
|
-
const timeoutId = setTimeout(() => {
|
|
131
|
-
try {
|
|
132
|
-
// Force reflow to ensure proper sizing
|
|
133
|
-
element.offsetHeight;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
console.warn('AtomixGlassContainer: Error during reflow', error);
|
|
136
|
-
}
|
|
137
|
-
}, 0);
|
|
138
|
-
|
|
139
|
-
return () => clearTimeout(timeoutId);
|
|
140
|
-
}, [cornerRadius, glassSize?.width, glassSize?.height, ref]);
|
|
245
|
+
// Removed forced reflow to avoid layout thrash and potential feedback sizing loops
|
|
141
246
|
|
|
142
247
|
const [rectCache, setRectCache] = useState<DOMRect | null>(null);
|
|
143
248
|
|
|
@@ -156,12 +261,22 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
156
261
|
return undefined;
|
|
157
262
|
}, [ref, glassSize]);
|
|
158
263
|
|
|
264
|
+
// Pre-calculate static multipliers outside useMemo
|
|
265
|
+
const EDGE_BLUR_MULTIPLIER = 1.25;
|
|
266
|
+
const CENTER_BLUR_MULTIPLIER = 1.1;
|
|
267
|
+
const FLOW_BLUR_MULTIPLIER = 1.2;
|
|
268
|
+
const MOUSE_INFLUENCE_BLUR_FACTOR = 0.4;
|
|
269
|
+
const EDGE_INTENSITY_MULTIPLIER = 1.5;
|
|
270
|
+
const EDGE_INTENSITY_MOUSE_FACTOR = 0.3;
|
|
271
|
+
const CENTER_INTENSITY_DISTANCE_FACTOR = 0.3;
|
|
272
|
+
const CENTER_INTENSITY_MOUSE_FACTOR = 0.2;
|
|
273
|
+
|
|
159
274
|
const liquidBlur = useMemo(() => {
|
|
160
275
|
const defaultBlur = {
|
|
161
276
|
baseBlur: blurAmount,
|
|
162
|
-
edgeBlur: blurAmount *
|
|
163
|
-
centerBlur: blurAmount *
|
|
164
|
-
flowBlur: blurAmount *
|
|
277
|
+
edgeBlur: blurAmount * EDGE_BLUR_MULTIPLIER,
|
|
278
|
+
centerBlur: blurAmount * CENTER_BLUR_MULTIPLIER,
|
|
279
|
+
flowBlur: blurAmount * FLOW_BLUR_MULTIPLIER,
|
|
165
280
|
};
|
|
166
281
|
|
|
167
282
|
// Enhanced validation for liquid blur
|
|
@@ -178,6 +293,7 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
178
293
|
}
|
|
179
294
|
|
|
180
295
|
try {
|
|
296
|
+
// Cache center and distance calculations
|
|
181
297
|
const center = calculateElementCenter(rectCache);
|
|
182
298
|
const distance = calculateDistance(globalMousePosition, center);
|
|
183
299
|
const maxDistance =
|
|
@@ -185,10 +301,10 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
185
301
|
const normalizedDistance = Math.min(distance / maxDistance, 1);
|
|
186
302
|
const mouseInfluence = calculateMouseInfluence(mouseOffset);
|
|
187
303
|
|
|
188
|
-
const baseBlur = blurAmount + mouseInfluence * blurAmount *
|
|
189
|
-
const edgeIntensity = normalizedDistance *
|
|
304
|
+
const baseBlur = blurAmount + mouseInfluence * blurAmount * MOUSE_INFLUENCE_BLUR_FACTOR;
|
|
305
|
+
const edgeIntensity = normalizedDistance * EDGE_INTENSITY_MULTIPLIER + mouseInfluence * EDGE_INTENSITY_MOUSE_FACTOR;
|
|
190
306
|
const edgeBlur = baseBlur * (0.8 + edgeIntensity * 0.6);
|
|
191
|
-
const centerIntensity = (1 - normalizedDistance) *
|
|
307
|
+
const centerIntensity = (1 - normalizedDistance) * CENTER_INTENSITY_DISTANCE_FACTOR + mouseInfluence * CENTER_INTENSITY_MOUSE_FACTOR;
|
|
192
308
|
const centerBlur = baseBlur * (0.3 + centerIntensity * 0.4);
|
|
193
309
|
const deltaX = globalMousePosition.x - center.x;
|
|
194
310
|
const deltaY = globalMousePosition.y - center.y;
|
|
@@ -244,23 +360,54 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
244
360
|
? liquidBlur.flowBlur
|
|
245
361
|
: 0;
|
|
246
362
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
363
|
+
// Adaptive strategy: prefer single-pass blur for large areas or when effects are reduced
|
|
364
|
+
const area = rectCache ? rectCache.width * rectCache.height : 0;
|
|
365
|
+
const areaIsLarge = area > 180000; // ~600x300 threshold; tune as needed
|
|
366
|
+
const devicePrefersPerformance = effectiveReducedMotion || effectiveDisableEffects;
|
|
367
|
+
const useMultiPass = enableLiquidBlur && !devicePrefersPerformance && !areaIsLarge;
|
|
368
|
+
|
|
369
|
+
if (useMultiPass) {
|
|
370
|
+
const blurLayers = [
|
|
371
|
+
`blur(${validatedBaseBlur}px)`,
|
|
372
|
+
`blur(${validatedEdgeBlur}px)`,
|
|
373
|
+
`blur(${validatedCenterBlur}px)`,
|
|
374
|
+
`blur(${validatedFlowBlur}px)`,
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
backdropFilter: `${blurLayers.join(' ')} saturate(${Math.min(dynamicSaturation, 200)}%) contrast(1.05) brightness(1.05)`,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Single-pass fallback: stronger radius to match perceived blur of multi-pass
|
|
383
|
+
const effectiveBlur = clampBlur(
|
|
384
|
+
Math.max(
|
|
385
|
+
validatedBaseBlur,
|
|
386
|
+
validatedEdgeBlur * 0.8,
|
|
387
|
+
validatedCenterBlur * 1.1,
|
|
388
|
+
validatedFlowBlur * 0.9
|
|
389
|
+
)
|
|
390
|
+
);
|
|
253
391
|
|
|
254
392
|
return {
|
|
255
|
-
backdropFilter:
|
|
393
|
+
backdropFilter: `blur(${effectiveBlur}px) saturate(${Math.min(dynamicSaturation, 200)}%) contrast(1.05) brightness(1.05)`,
|
|
256
394
|
};
|
|
257
395
|
} catch (error) {
|
|
258
396
|
console.warn('AtomixGlassContainer: Error calculating backdrop style', error);
|
|
259
397
|
return {
|
|
260
|
-
backdropFilter: `blur(${blurAmount}px) saturate(${saturation}%)`,
|
|
398
|
+
backdropFilter: `blur(${blurAmount}px) saturate(${saturation}%) contrast(1.05) brightness(1.05)`,
|
|
261
399
|
};
|
|
262
400
|
}
|
|
263
|
-
}, [
|
|
401
|
+
}, [
|
|
402
|
+
filterId,
|
|
403
|
+
liquidBlur,
|
|
404
|
+
saturation,
|
|
405
|
+
blurAmount,
|
|
406
|
+
rectCache,
|
|
407
|
+
effectiveReducedMotion,
|
|
408
|
+
effectiveDisableEffects,
|
|
409
|
+
enableLiquidBlur,
|
|
410
|
+
]);
|
|
264
411
|
|
|
265
412
|
const containerVars = useMemo(() => {
|
|
266
413
|
try {
|
|
@@ -332,7 +479,6 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
332
479
|
className={ATOMIX_GLASS.INNER_CLASS}
|
|
333
480
|
style={{
|
|
334
481
|
padding: `var(--atomix-glass-container-padding)`,
|
|
335
|
-
borderRadius: `var(--atomix-glass-container-radius)`,
|
|
336
482
|
boxShadow: `var(--atomix-glass-container-box-shadow)`,
|
|
337
483
|
}}
|
|
338
484
|
onMouseEnter={onMouseEnter}
|
|
@@ -342,6 +488,7 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
342
488
|
>
|
|
343
489
|
<div className={ATOMIX_GLASS.FILTER_CLASS}>
|
|
344
490
|
<GlassFilter
|
|
491
|
+
blurAmount={blurAmount}
|
|
345
492
|
mode={mode}
|
|
346
493
|
id={filterId}
|
|
347
494
|
displacementScale={
|
|
@@ -358,8 +505,9 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
358
505
|
/>
|
|
359
506
|
{/* Enhanced Apple Liquid Glass Inner Shadow Layer */}
|
|
360
507
|
|
|
361
|
-
<
|
|
508
|
+
<div
|
|
362
509
|
className={ATOMIX_GLASS.FILTER_OVERLAY_CLASS}
|
|
510
|
+
suppressHydrationWarning
|
|
363
511
|
style={{
|
|
364
512
|
filter: `url(#${filterId})`,
|
|
365
513
|
backdropFilter: `var(--atomix-glass-container-backdrop)`,
|
|
@@ -369,12 +517,12 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
369
517
|
|
|
370
518
|
<div
|
|
371
519
|
className={ATOMIX_GLASS.FILTER_SHADOW_CLASS}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
520
|
+
style={{
|
|
521
|
+
boxShadow: `var(--atomix-glass-container-shadow)`,
|
|
522
|
+
opacity: `var(--atomix-glass-container-shadow-opacity)`,
|
|
523
|
+
background: `var(--atomix-glass-container-bg)`,
|
|
524
|
+
borderRadius: `var(--atomix-glass-container-radius)`,
|
|
525
|
+
}}
|
|
378
526
|
/>
|
|
379
527
|
</div>
|
|
380
528
|
|