@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.
Files changed (144) hide show
  1. package/CHANGELOG.md +0 -1
  2. package/README.md +3 -5
  3. package/dist/atomix.css +753 -643
  4. package/dist/atomix.min.css +3 -5
  5. package/dist/index.d.ts +3075 -247
  6. package/dist/index.esm.js +20412 -16601
  7. package/dist/index.esm.js.map +1 -1
  8. package/dist/index.js +20379 -16605
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.min.js +1 -1
  11. package/dist/index.min.js.map +1 -1
  12. package/package.json +1 -11
  13. package/src/components/AtomixGlass/AtomixGlass.test.tsx +21 -32
  14. package/src/components/AtomixGlass/AtomixGlass.tsx +55 -42
  15. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +205 -57
  16. package/src/components/AtomixGlass/GlassFilter.tsx +22 -8
  17. package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +221 -0
  18. package/src/components/AtomixGlass/atomixGLass.old.tsx +0 -3
  19. package/src/components/AtomixGlass/shader-utils.ts +8 -0
  20. package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +319 -100
  21. package/src/components/AtomixGlass/stories/Examples.stories.tsx +601 -105
  22. package/src/components/AtomixGlass/stories/Modes.stories.tsx +30 -12
  23. package/src/components/AtomixGlass/stories/Playground.stories.tsx +173 -38
  24. package/src/components/AtomixGlass/stories/ShaderVariants.stories.tsx +18 -18
  25. package/src/components/AtomixGlass/stories/shared-components.tsx +27 -5
  26. package/src/components/Button/Button.tsx +62 -17
  27. package/src/components/Callout/Callout.test.tsx +8 -14
  28. package/src/components/Card/Card.tsx +103 -1
  29. package/src/components/Card/index.ts +3 -2
  30. package/src/components/Icon/index.ts +1 -1
  31. package/src/components/Modal/Modal.stories.tsx +29 -38
  32. package/src/components/Modal/Modal.tsx +4 -4
  33. package/src/components/Navigation/SideMenu/SideMenu.tsx +49 -41
  34. package/src/components/Navigation/SideMenu/SideMenuItem.tsx +63 -24
  35. package/src/components/Popover/Popover.tsx +1 -1
  36. package/src/components/VideoPlayer/VideoPlayer.stories.tsx +977 -400
  37. package/src/components/VideoPlayer/VideoPlayer.tsx +1 -6
  38. package/src/lib/composables/shared-mouse-tracker.ts +133 -0
  39. package/src/lib/composables/useAtomixGlass.ts +333 -145
  40. package/src/lib/index.ts +1 -4
  41. package/src/lib/theme/composeTheme.ts +375 -0
  42. package/src/lib/theme/config/index.ts +21 -0
  43. package/src/lib/theme/config/loader.ts +276 -0
  44. package/src/lib/theme/config/types.ts +98 -0
  45. package/src/lib/theme/config/validator.ts +326 -0
  46. package/src/lib/theme/constants.ts +183 -0
  47. package/src/lib/theme/core/ThemeCache.ts +283 -0
  48. package/src/lib/theme/core/ThemeEngine.test.ts +146 -0
  49. package/src/lib/theme/core/ThemeEngine.ts +657 -0
  50. package/src/lib/theme/core/ThemeRegistry.ts +284 -0
  51. package/src/lib/theme/core/ThemeValidator.ts +530 -0
  52. package/src/lib/theme/core/index.ts +24 -0
  53. package/src/lib/theme/createTheme.ts +521 -0
  54. package/src/lib/theme/devtools/CLI.ts +279 -0
  55. package/src/lib/theme/devtools/Inspector.tsx +594 -0
  56. package/src/lib/theme/devtools/Preview.tsx +392 -0
  57. package/src/lib/theme/devtools/index.ts +21 -0
  58. package/src/lib/theme/errors.test.ts +207 -0
  59. package/src/lib/theme/errors.ts +233 -0
  60. package/src/lib/theme/generateCSSVariables.ts +797 -0
  61. package/src/lib/theme/generators/CSSGenerator.ts +311 -0
  62. package/src/lib/theme/generators/ConfigGenerator.ts +287 -0
  63. package/src/lib/theme/generators/TypeGenerator.ts +228 -0
  64. package/src/lib/theme/generators/index.ts +21 -0
  65. package/src/lib/theme/i18n/index.ts +9 -0
  66. package/src/lib/theme/i18n/rtl.ts +325 -0
  67. package/src/lib/theme/index.ts +221 -10
  68. package/src/lib/theme/monitoring/ThemeAnalytics.ts +409 -0
  69. package/src/lib/theme/monitoring/index.ts +17 -0
  70. package/src/lib/theme/overrides/ComponentOverrides.ts +243 -0
  71. package/src/lib/theme/overrides/index.ts +15 -0
  72. package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +233 -0
  73. package/src/lib/theme/runtime/ThemeManager.test.ts +176 -0
  74. package/src/lib/theme/runtime/ThemeManager.ts +442 -0
  75. package/src/lib/theme/runtime/ThemeProvider.tsx +318 -0
  76. package/src/lib/theme/runtime/index.ts +17 -0
  77. package/src/lib/theme/runtime/useTheme.ts +52 -0
  78. package/src/lib/theme/studio/ThemeStudio.tsx +312 -0
  79. package/src/lib/theme/studio/index.ts +8 -0
  80. package/src/lib/theme/themeUtils.ts +333 -0
  81. package/src/lib/theme/types.ts +340 -9
  82. package/src/lib/theme/utils.ts +23 -22
  83. package/src/lib/theme/whitelabel/WhiteLabelManager.ts +364 -0
  84. package/src/lib/theme/whitelabel/index.ts +13 -0
  85. package/src/lib/types/components.ts +148 -59
  86. package/src/styles/01-settings/_index.scss +2 -2
  87. package/src/styles/01-settings/_settings.badge.scss +3 -3
  88. package/src/styles/01-settings/_settings.border-radius.scss +1 -1
  89. package/src/styles/01-settings/_settings.callout.scss +1 -1
  90. package/src/styles/01-settings/_settings.card.scss +1 -1
  91. package/src/styles/01-settings/{_settings.maps.scss → _settings.design-tokens.scss} +163 -49
  92. package/src/styles/01-settings/_settings.input.scss +1 -1
  93. package/src/styles/01-settings/_settings.modal.scss +1 -1
  94. package/src/styles/01-settings/_settings.navbar.scss +1 -1
  95. package/src/styles/01-settings/_settings.spacing.scss +14 -13
  96. package/src/styles/01-settings/_settings.upload.scss +1 -1
  97. package/src/styles/03-generic/_generic.root.scss +131 -50
  98. package/src/styles/05-objects/_objects.block.scss +1 -1
  99. package/src/styles/06-components/_components.atomix-glass.scss +20 -22
  100. package/src/styles/06-components/_components.badge.scss +2 -2
  101. package/src/styles/06-components/_components.button.scss +1 -1
  102. package/src/styles/06-components/_components.callout.scss +1 -1
  103. package/src/styles/06-components/_components.card.scss +74 -2
  104. package/src/styles/06-components/_components.chart.scss +3 -3
  105. package/src/styles/06-components/_components.dropdown.scss +6 -0
  106. package/src/styles/06-components/_components.footer.scss +1 -1
  107. package/src/styles/06-components/_components.list-group.scss +1 -1
  108. package/src/styles/06-components/_components.list.scss +1 -1
  109. package/src/styles/06-components/_components.menu.scss +1 -1
  110. package/src/styles/06-components/_components.messages.scss +1 -1
  111. package/src/styles/06-components/_components.modal.scss +7 -2
  112. package/src/styles/06-components/_components.navbar.scss +1 -1
  113. package/src/styles/06-components/_components.popover.scss +10 -0
  114. package/src/styles/06-components/_components.product-review.scss +1 -1
  115. package/src/styles/06-components/_components.progress.scss +1 -1
  116. package/src/styles/06-components/_components.rating.scss +1 -1
  117. package/src/styles/06-components/_components.spinner.scss +1 -1
  118. package/src/styles/99-utilities/_utilities.background.scss +1 -1
  119. package/src/styles/99-utilities/_utilities.border.scss +28 -59
  120. package/src/styles/99-utilities/_utilities.gradient.scss +12 -0
  121. package/src/styles/99-utilities/_utilities.link.scss +1 -1
  122. package/src/styles/99-utilities/_utilities.position.scss +8 -15
  123. package/src/styles/99-utilities/_utilities.scss +2 -0
  124. package/src/styles/99-utilities/_utilities.spacing.scss +76 -121
  125. package/src/styles/99-utilities/_utilities.text.scss +31 -50
  126. package/dist/themes/applemix.css +0 -15411
  127. package/dist/themes/applemix.min.css +0 -72
  128. package/dist/themes/boomdevs.css +0 -15001
  129. package/dist/themes/boomdevs.min.css +0 -405
  130. package/dist/themes/esrar.css +0 -17195
  131. package/dist/themes/esrar.min.css +0 -189
  132. package/dist/themes/flashtrade.css +0 -16408
  133. package/dist/themes/flashtrade.min.css +0 -192
  134. package/dist/themes/mashroom.css +0 -29900
  135. package/dist/themes/mashroom.min.css +0 -403
  136. package/dist/themes/shaj-default.css +0 -16024
  137. package/dist/themes/shaj-default.min.css +0 -500
  138. package/src/lib/theme/ThemeManager.stories.tsx +0 -472
  139. package/src/lib/theme/ThemeManager.test.ts +0 -186
  140. package/src/lib/theme/ThemeManager.ts +0 -501
  141. package/src/lib/theme/ThemeProvider.tsx +0 -227
  142. package/src/lib/theme/useTheme.test.tsx +0 -66
  143. package/src/lib/theme/useTheme.ts +0 -80
  144. 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.0",
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 hover effects are enabled
50
- expect(screen.getByTestId('atomix-glass')).toHaveAttribute('data-hover-effects', 'true');
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
- expect(container.querySelector('.c-atomix-glass__container')).toHaveClass(
62
- 'c-atomix-glass__container--clickable'
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: red');
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 handleMouseEnter = vi.fn();
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 glassContent = screen.getByText('Content').parentElement;
205
- if (!glassContent) throw new Error('Glass content not found');
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
- await userEvent.unhover(glassContent);
211
- expect(handleMouseLeave).toHaveBeenCalledTimes(1);
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.pointer([{ keys: '[MouseLeft>]', target: glassContent }]);
214
- expect(handleMouseDown).toHaveBeenCalledTimes(1);
201
+ await userEvent.unhover(glassContainer);
202
+ expect(glassContainer).toBeInTheDocument();
215
203
 
216
- await userEvent.pointer([{ keys: '[/MouseLeft]', target: glassContent }]);
217
- expect(handleMouseUp).toHaveBeenCalledTimes(1);
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
- // Memoize transition duration using design token pattern
155
- const transitionDuration = useMemo(
156
- () => (effectiveReducedMotion ? 'none' : 'var(--atomix-transition-duration, 0.2s) ease-out'),
157
- [effectiveReducedMotion]
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
- // Read opacity design tokens from CSS custom properties
339
+ // Use cached opacity values from CSS custom properties (read once on mount)
306
340
  const opacityValues = useMemo(() => {
307
- // Get opacity values from CSS custom properties with fallbacks
308
- // These align with design tokens: --atomix-opacity-50, --atomix-opacity-40, etc.
309
- let opacity50 = 0.5;
310
- let opacity40 = 0.4;
311
- let opacity80 = 0.8;
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, glassRef]);
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<ShaderDisplacementGenerator | null>(null);
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 initial shader map when mode/size/variant changes
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
- try {
97
- shaderGeneratorRef.current?.destroy();
98
- const selectedShader = fragmentShaders[shaderVariant] || fragmentShaders.liquidGlass;
99
- shaderGeneratorRef.current = new ShaderDisplacementGenerator({
100
- width: glassSize.width,
101
- height: glassSize.height,
102
- fragment: selectedShader,
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
- useEffect(() => {
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 * 1.25,
163
- centerBlur: blurAmount * 1.1,
164
- flowBlur: blurAmount * 1.2,
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 * 0.4;
189
- const edgeIntensity = normalizedDistance * 1.5 + mouseInfluence * 0.3;
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) * 0.3 + mouseInfluence * 0.2;
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
- const blurLayers = [
248
- `blur(${validatedBaseBlur}px)`,
249
- `blur(${validatedEdgeBlur}px)`,
250
- `blur(${validatedCenterBlur}px)`,
251
- `blur(${validatedFlowBlur}px)`,
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: `${blurLayers.join(' ')} saturate(${Math.min(dynamicSaturation, 200)}%) `,
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
- }, [filterId, liquidBlur, saturation, blurAmount]);
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
- <span
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
- style={{
373
- boxShadow: `var(--atomix-glass-container-shadow)`,
374
- opacity: `var(--atomix-glass-container-shadow-opacity)`,
375
- background: `var(--atomix-glass-container-bg)`,
376
- borderRadius: `var(--atomix-glass-container-radius)`,
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