@shohojdhara/atomix 0.6.3 → 0.6.5

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 (77) hide show
  1. package/dist/atomix.css +119 -40
  2. package/dist/atomix.css.map +1 -1
  3. package/dist/atomix.min.css +1 -1
  4. package/dist/atomix.min.css.map +1 -1
  5. package/dist/atomix.umd.js +1 -1
  6. package/dist/atomix.umd.js.map +1 -1
  7. package/dist/atomix.umd.min.js +1 -1
  8. package/dist/charts.d.ts +30 -1
  9. package/dist/charts.js +566 -597
  10. package/dist/charts.js.map +1 -1
  11. package/dist/core.d.ts +30 -1
  12. package/dist/core.js +600 -624
  13. package/dist/core.js.map +1 -1
  14. package/dist/forms.d.ts +30 -1
  15. package/dist/forms.js +1122 -1163
  16. package/dist/forms.js.map +1 -1
  17. package/dist/heavy.d.ts +31 -89
  18. package/dist/heavy.js +1015 -1045
  19. package/dist/heavy.js.map +1 -1
  20. package/dist/index.d.ts +378 -104
  21. package/dist/index.esm.js +10959 -10837
  22. package/dist/index.esm.js.map +1 -1
  23. package/dist/index.js +10935 -10812
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.min.js +1 -1
  26. package/dist/index.min.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/components/Accordion/Accordion.tsx +2 -5
  29. package/src/components/AtomixGlass/AtomixGlass.test.tsx +14 -16
  30. package/src/components/AtomixGlass/AtomixGlass.tsx +137 -355
  31. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +32 -249
  32. package/src/components/AtomixGlass/GlassFilter.tsx +62 -68
  33. package/src/components/AtomixGlass/README.md +2 -1
  34. package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +19 -18
  35. package/src/components/AtomixGlass/glass-border-styles.test.ts +58 -0
  36. package/src/components/AtomixGlass/glass-border-styles.ts +136 -0
  37. package/src/components/AtomixGlass/glass-utils.ts +411 -6
  38. package/src/components/AtomixGlass/stories/AnimationFeatures.stories.tsx +158 -537
  39. package/src/components/AtomixGlass/stories/Border.stories.tsx +149 -0
  40. package/src/components/AtomixGlass/stories/Examples.stories.tsx +229 -89
  41. package/src/components/AtomixGlass/stories/Playground.stories.tsx +29 -340
  42. package/src/components/AtomixGlass/stories/argTypes.ts +30 -13
  43. package/src/components/AtomixGlass/stories/premium-presets.ts +206 -0
  44. package/src/components/AtomixGlass/stories/shared-components.tsx +52 -8
  45. package/src/components/Badge/Badge.tsx +4 -4
  46. package/src/components/Button/Button.tsx +2 -6
  47. package/src/components/Callout/Callout.test.tsx +4 -3
  48. package/src/components/Callout/Callout.tsx +2 -5
  49. package/src/components/Dropdown/Dropdown.tsx +3 -7
  50. package/src/components/Form/Checkbox.tsx +2 -8
  51. package/src/components/Form/Input.tsx +2 -9
  52. package/src/components/Form/Radio.tsx +2 -9
  53. package/src/components/Form/Select.tsx +2 -7
  54. package/src/components/Form/Textarea.tsx +2 -9
  55. package/src/components/Messages/Messages.tsx +2 -8
  56. package/src/components/Modal/Modal.tsx +4 -5
  57. package/src/components/Navigation/Nav/Nav.tsx +2 -6
  58. package/src/components/Navigation/Navbar/Navbar.tsx +2 -9
  59. package/src/components/Navigation/SideMenu/SideMenu.tsx +2 -6
  60. package/src/components/Pagination/Pagination.tsx +2 -10
  61. package/src/components/Popover/Popover.tsx +2 -9
  62. package/src/components/Progress/Progress.tsx +2 -7
  63. package/src/components/Rating/Rating.tsx +2 -10
  64. package/src/components/Spinner/Spinner.tsx +2 -7
  65. package/src/components/Steps/Steps.tsx +2 -10
  66. package/src/components/Tabs/Tabs.tsx +2 -9
  67. package/src/components/Toggle/Toggle.tsx +2 -10
  68. package/src/components/Tooltip/Tooltip.tsx +2 -5
  69. package/src/lib/composables/useAtomixGlass.ts +41 -10
  70. package/src/lib/composables/useAtomixGlassStyles.ts +59 -75
  71. package/src/lib/composables/usePerformanceMonitor.ts +5 -0
  72. package/src/lib/constants/components.ts +358 -46
  73. package/src/lib/types/components.ts +33 -1
  74. package/src/styles/01-settings/_settings.atomix-glass.scss +69 -31
  75. package/src/styles/02-tools/_tools.glass.scss +45 -3
  76. package/src/styles/06-components/_components.atomix-glass.scss +114 -77
  77. package/src/components/AtomixGlass/deprecated/AtomixGlass.deprecated.tsx +0 -390
@@ -1,44 +1,32 @@
1
1
  import React, { forwardRef, useId, useRef, useState, useEffect, useMemo } from 'react';
2
- import type { CSSProperties } from 'react';
3
2
  import type {
4
3
  DisplacementMode,
5
4
  MousePosition,
6
5
  GlassSize,
7
6
  AtomixGlassProps,
8
7
  } from '../../lib/types/components';
8
+ import useForkRef from '../../lib/utils/useForkRef';
9
+ import { mergeClassNames } from '../../lib/utils/componentUtils';
9
10
  import type { FragmentShaderType, ShaderOptions, Vec2 } from './shader-utils';
10
11
  import { GlassFilter } from './GlassFilter';
11
12
  import {
12
- calculateMouseInfluence,
13
- clampBlur,
14
- validateGlassSize,
15
13
  getCachedShader,
14
+ getShaderAnimationTargetFps,
16
15
  setCachedShader,
16
+ toSafeNumber,
17
+ validateGlassSize,
17
18
  } from './glass-utils';
18
19
  import { ATOMIX_GLASS } from '../../lib/constants/components';
19
20
 
20
- const { CONSTANTS } = ATOMIX_GLASS;
21
-
22
- // ─── Blur multiplier constants (module-level, never change at runtime) ────────
23
- const EDGE_BLUR_MULTIPLIER = 1.25;
24
- const CENTER_BLUR_MULTIPLIER = 1.1;
25
- const FLOW_BLUR_MULTIPLIER = 1.2;
26
- const MOUSE_INFLUENCE_BLUR_FACTOR = 0.15;
27
- const EDGE_INTENSITY_MULTIPLIER = 1.5;
28
- const EDGE_INTENSITY_MOUSE_FACTOR = 0.15;
29
- const CENTER_INTENSITY_DISTANCE_FACTOR = 0.3;
30
- const CENTER_INTENSITY_MOUSE_FACTOR = 0.1;
31
- /** Maximum blur multiplier relative to base — prevents runaway blur. */
32
- const MAX_BLUR_RELATIVE = 2;
33
21
 
34
- // ─── Shader utility types ─────────────────────────────────────────────────────
35
22
 
23
+ /** Minimal interface for dynamically loaded shader displacement generators. */
36
24
  interface ShaderGenerator {
37
25
  updateShader(): string;
38
26
  destroy(): void;
39
27
  }
40
28
 
41
- /** Fragment shader function signature matches shader-utils.ts */
29
+ /** Fragment shader signature; must match `shader-utils.ts`. */
42
30
  type FragmentShaderFn = (uv: Vec2, mousePosition?: Vec2) => Vec2;
43
31
 
44
32
  interface ShaderUtilsModule {
@@ -69,7 +57,6 @@ interface AtomixGlassContainerProps
69
57
  onMouseEnter?: () => void;
70
58
  onMouseDown?: () => void;
71
59
  onMouseUp?: () => void;
72
- isHovered?: boolean;
73
60
  isActive?: boolean;
74
61
  overLight?: boolean;
75
62
  overLightConfig?: {
@@ -79,7 +66,7 @@ interface AtomixGlassContainerProps
79
66
  borderOpacity?: number;
80
67
  };
81
68
  borderRadius?: number;
82
- padding?: string;
69
+
83
70
  glassSize?: GlassSize;
84
71
  onClick?: () => void;
85
72
  mode?: DisplacementMode;
@@ -90,8 +77,6 @@ interface AtomixGlassContainerProps
90
77
  withLiquidBlur?: boolean;
91
78
  isFixedOrSticky?: boolean;
92
79
  elasticity?: number;
93
-
94
- // Phase 1: Animation System props
95
80
  shaderTime?: number;
96
81
 
97
82
  contentRef?: React.RefObject<HTMLDivElement | null>;
@@ -99,8 +84,10 @@ interface AtomixGlassContainerProps
99
84
  }
100
85
 
101
86
  /**
102
- * AtomixGlassContainer - Internal container component for glass effects
103
- * Handles the visual glass morphism layer with filters and backdrop effects
87
+ * Internal glass surface that owns backdrop-filter, SVG distortion, and content.
88
+ *
89
+ * Layout and stacking styles are applied via the `style` prop from the parent.
90
+ * The root wrapper supplies CSS custom properties only.
104
91
  */
105
92
  export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContainerProps>(
106
93
  (
@@ -121,7 +108,7 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
121
108
  overLight = false,
122
109
  overLightConfig = {},
123
110
  borderRadius = 0,
124
- padding = '0 0',
111
+
125
112
  glassSize = { width: 0, height: 0 },
126
113
  onClick,
127
114
  mode = 'standard',
@@ -130,8 +117,6 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
130
117
  shaderVariant = 'liquidGlass',
131
118
  withLiquidBlur = false,
132
119
  isFixedOrSticky = false,
133
-
134
- // Phase 1: Animation System props
135
120
  shaderTime,
136
121
  withTimeAnimation = false,
137
122
  animationSpeed = 1.0,
@@ -148,6 +133,7 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
148
133
  // React 18 useId — stable, unique, and SSR-safe (no module-level counter)
149
134
  const rawId = useId();
150
135
  const filterId = useMemo(() => `atomix-glass-filter-${rawId.replace(/:/g, '')}`, [rawId]);
136
+ const containerRef = useForkRef(ref, null);
151
137
 
152
138
  const [shaderMapUrl, setShaderMapUrl] = useState<string>('');
153
139
  const shaderGeneratorRef = useRef<ShaderGenerator | null>(null);
@@ -156,7 +142,6 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
156
142
  const shaderDebounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
157
143
  const shaderUpdateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
158
144
 
159
- // Phase 1: Animation frame ref for continuous shader updates
160
145
  const animationFrameRef = useRef<number | null>(null);
161
146
 
162
147
  // Lazy load shader utilities only when shader mode is needed
@@ -260,7 +245,6 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
260
245
  };
261
246
  }, [mode, glassSize, shaderVariant]);
262
247
 
263
- // Phase 1: Time-Based Animation Loop - Continuous shader regeneration
264
248
  useEffect(() => {
265
249
  // Only run animations in shader mode with time animation enabled
266
250
  if (
@@ -277,27 +261,14 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
277
261
  return;
278
262
  }
279
263
 
280
- const baseFps =
281
- distortionQuality === 'ultra'
282
- ? 60
283
- : distortionQuality === 'high'
284
- ? 30
285
- : distortionQuality === 'medium'
286
- ? 24
287
- : 20;
288
- const effectiveSpeed = Math.max(0.5, Math.min(2, animationSpeed || 1));
289
- const complexity = withMultiLayerDistortion
290
- ? Math.max(
291
- 1,
292
- (distortionOctaves || 3) / 3 +
293
- Math.max(0, (distortionLacunarity || 2) - 2) * 0.25 +
294
- Math.max(0, (distortionGain || 0.5) - 0.5)
295
- )
296
- : 1;
297
- const targetFps = Math.max(
298
- 12,
299
- Math.min(60, Math.round((baseFps * effectiveSpeed) / complexity))
300
- );
264
+ const targetFps = getShaderAnimationTargetFps({
265
+ distortionQuality,
266
+ animationSpeed,
267
+ withMultiLayerDistortion,
268
+ distortionOctaves,
269
+ distortionLacunarity,
270
+ distortionGain,
271
+ });
301
272
  const frameInterval = 1000 / targetFps;
302
273
  let lastUpdate = 0;
303
274
  let isCancelled = false;
@@ -350,212 +321,32 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
350
321
 
351
322
  // Removed forced reflow to avoid layout thrash and potential feedback sizing loops
352
323
 
353
- const [rectCache, setRectCache] = useState<DOMRect | null>(null);
354
-
355
- useEffect(() => {
356
- if (!ref || typeof ref === 'function') return undefined;
357
- const element = (ref as React.RefObject<HTMLDivElement>).current;
358
- if (!element) return undefined;
359
-
360
- try {
361
- setRectCache(element.getBoundingClientRect());
362
- } catch (error) {
363
- console.warn('AtomixGlassContainer: Error getting element bounds', error);
364
- setRectCache(null);
365
- }
366
-
367
- return undefined;
368
- }, [ref, glassSize]);
369
-
370
- const liquidBlur = useMemo(() => {
371
- const defaultBlur = {
372
- baseBlur: blurAmount,
373
- edgeBlur: blurAmount * EDGE_BLUR_MULTIPLIER,
374
- centerBlur: blurAmount * CENTER_BLUR_MULTIPLIER,
375
- flowBlur: blurAmount * FLOW_BLUR_MULTIPLIER,
376
- };
377
-
378
- // Enhanced validation for liquid blur
379
- if (
380
- !withLiquidBlur ||
381
- !rectCache ||
382
- !mouseOffset ||
383
- typeof mouseOffset.x !== 'number' ||
384
- typeof mouseOffset.y !== 'number' ||
385
- isNaN(mouseOffset.x) ||
386
- isNaN(mouseOffset.y)
387
- ) {
388
- return defaultBlur;
389
- }
390
-
391
- try {
392
- const mouseInfluence = calculateMouseInfluence(mouseOffset);
393
- const maxBlur = blurAmount * MAX_BLUR_RELATIVE;
394
-
395
- const baseBlur = Math.min(
396
- maxBlur,
397
- blurAmount + mouseInfluence * blurAmount * MOUSE_INFLUENCE_BLUR_FACTOR
398
- );
399
- const edgeIntensity = mouseInfluence * EDGE_INTENSITY_MOUSE_FACTOR;
400
- const edgeBlur = Math.min(maxBlur, baseBlur * (0.8 + edgeIntensity * 0.4));
401
- const centerIntensity = mouseInfluence * CENTER_INTENSITY_MOUSE_FACTOR;
402
- const centerBlur = Math.min(maxBlur, baseBlur * (0.3 + centerIntensity * 0.3));
403
- const flowBlur = Math.min(maxBlur, baseBlur * FLOW_BLUR_MULTIPLIER);
404
-
405
- // NOTE: hover/active multipliers intentionally omitted here —
406
- // they belong on opacity layers, not the blur filter itself.
407
- return {
408
- baseBlur: clampBlur(baseBlur),
409
- edgeBlur: clampBlur(edgeBlur),
410
- centerBlur: clampBlur(centerBlur),
411
- flowBlur: clampBlur(flowBlur),
412
- };
413
- } catch (error) {
414
- console.warn('AtomixGlassContainer: Error calculating liquid blur', error);
415
- return defaultBlur;
416
- }
417
- }, [withLiquidBlur, blurAmount, mouseOffset, rectCache]);
418
324
 
419
- const backdropStyle = useMemo(() => {
420
- try {
421
- const dynamicSaturation = saturation + (liquidBlur.baseBlur || 0) * 20;
422
-
423
- // Validate blur values before using them
424
- const validatedBaseBlur =
425
- typeof liquidBlur.baseBlur === 'number' && !isNaN(liquidBlur.baseBlur)
426
- ? liquidBlur.baseBlur
427
- : 0;
428
- const validatedEdgeBlur =
429
- typeof liquidBlur.edgeBlur === 'number' && !isNaN(liquidBlur.edgeBlur)
430
- ? liquidBlur.edgeBlur
431
- : 0;
432
- const validatedCenterBlur =
433
- typeof liquidBlur.centerBlur === 'number' && !isNaN(liquidBlur.centerBlur)
434
- ? liquidBlur.centerBlur
435
- : 0;
436
- const validatedFlowBlur =
437
- typeof liquidBlur.flowBlur === 'number' && !isNaN(liquidBlur.flowBlur)
438
- ? liquidBlur.flowBlur
439
- : 0;
440
-
441
- // Adaptive strategy: prefer single-pass blur for large areas or when effects are reduced
442
- const area = rectCache ? rectCache.width * rectCache.height : 0;
443
- const areaIsLarge = area > 180000; // ~600x300 threshold; tune as needed
444
- const devicePrefersPerformance = effectiveReducedMotion || effectiveWithoutEffects;
445
- const useMultiPass = withLiquidBlur && !devicePrefersPerformance && !areaIsLarge;
446
-
447
- if (useMultiPass) {
448
- // Use a single weighted-average blur instead of stacking multiple
449
- // blur() calls. CSS blur() is additive — stacking 4 passes
450
- // causes the perceived blur to compound far beyond any single value.
451
- const weightedBlur = clampBlur(
452
- validatedBaseBlur * 0.4 +
453
- validatedEdgeBlur * 0.25 +
454
- validatedCenterBlur * 0.15 +
455
- validatedFlowBlur * 0.2
456
- );
457
-
458
- return {
459
- backdropFilter: `blur(${weightedBlur}px) saturate(${Math.min(dynamicSaturation, 200)}%) contrast(${overLightConfig?.contrast || 1}) brightness(${overLightConfig?.brightness || 1})`,
460
- };
461
- }
462
325
 
463
- // Single-pass fallback: stronger radius to match perceived blur of multi-pass
464
- const effectiveBlur = clampBlur(
465
- Math.max(
466
- validatedBaseBlur,
467
- validatedEdgeBlur * 0.8,
468
- validatedCenterBlur * 1.1,
469
- validatedFlowBlur * 0.9
470
- )
471
- );
472
326
 
473
- return {
474
- backdropFilter: `blur(${effectiveBlur}px) saturate(${Math.min(dynamicSaturation, 200)}%) contrast(${overLightConfig?.contrast || 1.05}) brightness(${overLightConfig?.brightness || 1.05})`,
475
- };
476
- } catch (error) {
477
- console.warn('AtomixGlassContainer: Error calculating backdrop style', error);
478
- return {
479
- backdropFilter: `blur(${blurAmount}px) saturate(${saturation}%) contrast(1.05) brightness(1.05)`,
480
- };
481
- }
482
- }, [
483
- liquidBlur,
484
- saturation,
485
- blurAmount,
486
- rectCache,
487
- effectiveReducedMotion,
488
- effectiveWithoutEffects,
489
- withLiquidBlur,
490
- overLightConfig,
491
- ]);
492
327
 
493
328
  const containerVars = useMemo(() => {
494
329
  try {
495
- // Safe extraction of mouse offset values
496
- const mx =
497
- mouseOffset && typeof mouseOffset.x === 'number' && !isNaN(mouseOffset.x)
498
- ? mouseOffset.x
499
- : 0;
500
- const my =
501
- mouseOffset && typeof mouseOffset.y === 'number' && !isNaN(mouseOffset.y)
502
- ? mouseOffset.y
503
- : 0;
504
330
  return {
505
331
  '--atomix-glass-container-radius': `${typeof borderRadius === 'number' && !isNaN(borderRadius) ? borderRadius : 0}px`,
506
- '--atomix-glass-container-backdrop': backdropStyle?.backdropFilter || 'none',
507
- '--atomix-glass-container-shadow': overLight
508
- ? [
509
- `inset 0 1px 0 rgba(255, 255, 255, ${(0.4 + mx * 0.002) * (overLightConfig?.shadowIntensity || 1)})`,
510
- `inset 0 -1px 0 rgba(0, 0, 0, ${(0.2 + Math.abs(my) * 0.001) * (overLightConfig?.shadowIntensity || 1)})`,
511
- `inset 0 0 20px rgba(0, 0, 0, ${(0.08 + Math.abs(mx + my) * 0.001) * (overLightConfig?.shadowIntensity || 1)})`,
512
- `0 2px 12px rgba(0, 0, 0, ${(0.12 + Math.abs(my) * 0.002) * (overLightConfig?.shadowIntensity || 1)})`,
513
- ].join(', ')
514
- : '0 0 20px rgba(0, 0, 0, 0.15) inset, 0 4px 8px rgba(0, 0, 0, 0.08) inset',
515
- '--atomix-glass-container-shadow-opacity': effectiveWithoutEffects ? 0 : 1,
516
- // Background and shadow values use design token-aligned RGB values
517
- '--atomix-glass-container-bg': overLight
518
- ? `linear-gradient(${180 + mx * 0.5}deg, rgba(255, 255, 255, 0.1) 0%, transparent 20%, transparent 80%, rgba(0, 0, 0, 0.05) 100%)`
519
- : 'none',
520
- '--atomix-glass-container-text-shadow': overLight
521
- ? '0px 1px 2px rgba(255, 255, 255, 0.15)'
522
- : '0px 2px 12px rgba(0, 0, 0, 0.4)',
523
- '--atomix-glass-container-box-shadow': overLight
524
- ? '0px 16px 70px rgba(0, 0, 0, 0.75)'
525
- : '0px 12px 40px rgba(0, 0, 0, 0.25)',
526
332
  } as React.CSSProperties;
527
333
  } catch (error) {
528
334
  console.warn('AtomixGlassContainer: Error generating container variables', error);
529
335
  return {
530
- '--atomix-glass-container-padding': '0 0',
531
336
  '--atomix-glass-container-radius': '0px',
532
- '--atomix-glass-container-backdrop': 'none',
533
- '--atomix-glass-container-shadow': 'none',
534
- '--atomix-glass-container-shadow-opacity': 1,
535
- '--atomix-glass-container-bg': 'none',
536
- '--atomix-glass-container-text-shadow': 'none',
537
337
  } as React.CSSProperties;
538
338
  }
539
- }, [
540
- borderRadius,
541
- backdropStyle,
542
- mouseOffset,
543
- overLight,
544
- effectiveWithoutEffects,
545
- overLightConfig,
546
- ]);
339
+ }, [borderRadius]);
547
340
 
548
341
  return (
549
342
  <div
550
- ref={el => {
551
- // Handle forwarded ref
552
- if (typeof ref === 'function') {
553
- ref(el);
554
- } else if (ref) {
555
- (ref as React.MutableRefObject<HTMLDivElement | null>).current = el;
556
- }
557
- }}
558
- className={`${ATOMIX_GLASS.CONTAINER_CLASS} ${className} ${isActive ? ATOMIX_GLASS.CLASSES.ACTIVE : ''} ${overLight ? ATOMIX_GLASS.CLASSES.OVER_LIGHT : ''}`}
343
+ ref={containerRef}
344
+ className={mergeClassNames(
345
+ ATOMIX_GLASS.CONTAINER_CLASS,
346
+ className,
347
+ isActive && ATOMIX_GLASS.CLASSES.ACTIVE,
348
+ overLight && ATOMIX_GLASS.CLASSES.OVER_LIGHT
349
+ )}
559
350
  style={{ ...style, ...containerVars }}
560
351
  onClick={onClick}
561
352
  >
@@ -571,16 +362,8 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
571
362
  blurAmount={blurAmount}
572
363
  mode={mode}
573
364
  id={filterId}
574
- displacementScale={
575
- typeof displacementScale === 'number' && !isNaN(displacementScale)
576
- ? displacementScale
577
- : 0
578
- }
579
- aberrationIntensity={
580
- typeof aberrationIntensity === 'number' && !isNaN(aberrationIntensity)
581
- ? aberrationIntensity
582
- : 0
583
- }
365
+ displacementScale={toSafeNumber(displacementScale)}
366
+ aberrationIntensity={toSafeNumber(aberrationIntensity)}
584
367
  shaderMapUrl={shaderMapUrl}
585
368
  />
586
369
  {/* Enhanced Apple Liquid Glass Inner Shadow Layer */}
@@ -1,7 +1,6 @@
1
1
  import React, { memo } from 'react';
2
2
  import type { DisplacementMode } from '../../lib/types/components';
3
- import type { FragmentShaderType } from './shader-utils';
4
- import { getDisplacementMap } from './glass-utils';
3
+ import { getChromaticDisplacementScale, getDisplacementMap } from './glass-utils';
5
4
  import { displacementMap, polarDisplacementMap, prominentDisplacementMap } from './utils';
6
5
 
7
6
  interface GlassFilterProps {
@@ -13,9 +12,43 @@ interface GlassFilterProps {
13
12
  blurAmount: number;
14
13
  }
15
14
 
15
+ /** Per-channel SVG filter configuration for chromatic aberration. */
16
+ const CHROMATIC_CHANNELS = [
17
+ {
18
+ result: 'RED_DISPLACED',
19
+ channelResult: 'RED_CHANNEL',
20
+ aberrationFactor: 0,
21
+ colorMatrix:
22
+ '1 0 0 0 0\n0 0 0 0 0\n0 0 0 0 0\n0 0 0 1 0',
23
+ },
24
+ {
25
+ result: 'GREEN_DISPLACED',
26
+ channelResult: 'GREEN_CHANNEL',
27
+ aberrationFactor: 0.02,
28
+ colorMatrix:
29
+ '0 0 0 0 0\n0 1 0 0 0\n0 0 0 0 0\n0 0 0 1 0',
30
+ },
31
+ {
32
+ result: 'BLUE_DISPLACED',
33
+ channelResult: 'BLUE_CHANNEL',
34
+ aberrationFactor: 0.03,
35
+ colorMatrix:
36
+ '0 0 0 0 0\n0 0 0 0 0\n0 0 1 0 0\n0 0 0 1 0',
37
+ },
38
+ ] as const;
39
+
40
+ const FILTER_SVG_STYLE: React.CSSProperties = {
41
+ position: 'absolute',
42
+ width: '100%',
43
+ height: '100%',
44
+ inset: 0,
45
+ };
46
+
16
47
  /**
17
- * GlassFilter - SVG filter component for glass morphism effects
18
- * Creates chromatic aberration and edge distortion effects using SVG filters
48
+ * Renders an SVG filter definition for glass morphism distortion.
49
+ *
50
+ * Produces chromatic aberration at the edges via channel-separated displacement
51
+ * maps and recomposites the center region without distortion.
19
52
  */
20
53
  const GlassFilterComponent: React.FC<GlassFilterProps> = ({
21
54
  id,
@@ -25,15 +58,7 @@ const GlassFilterComponent: React.FC<GlassFilterProps> = ({
25
58
  shaderMapUrl,
26
59
  blurAmount,
27
60
  }) => (
28
- <svg
29
- style={{
30
- position: 'absolute',
31
- width: '100%',
32
- height: '100%',
33
- inset: 0,
34
- }}
35
- aria-hidden="true"
36
- >
61
+ <svg style={FILTER_SVG_STYLE} aria-hidden="true">
37
62
  <defs>
38
63
  <radialGradient id={`${id}-edge-mask`} cx="50%" cy="50%" r="50%">
39
64
  <stop offset="0%" stopColor="black" stopOpacity="0" />
@@ -77,59 +102,29 @@ const GlassFilterComponent: React.FC<GlassFilterProps> = ({
77
102
 
78
103
  <feOffset in="SourceGraphic" dx="0" dy="0" result="CENTER_ORIGINAL" />
79
104
 
80
- <feDisplacementMap
81
- in="SourceGraphic"
82
- in2="DISPLACEMENT_MAP"
83
- scale={displacementScale * (mode === 'shader' ? 1 : -1)}
84
- xChannelSelector="R"
85
- yChannelSelector="B"
86
- result="RED_DISPLACED"
87
- />
88
- <feColorMatrix
89
- in="RED_DISPLACED"
90
- type="matrix"
91
- values="1 0 0 0 0
92
- 0 0 0 0 0
93
- 0 0 0 0 0
94
- 0 0 0 1 0"
95
- result="RED_CHANNEL"
96
- />
97
-
98
- <feDisplacementMap
99
- in="SourceGraphic"
100
- in2="DISPLACEMENT_MAP"
101
- scale={displacementScale * ((mode === 'shader' ? 1 : -1) - aberrationIntensity * 0.02)}
102
- xChannelSelector="R"
103
- yChannelSelector="B"
104
- result="GREEN_DISPLACED"
105
- />
106
- <feColorMatrix
107
- in="GREEN_DISPLACED"
108
- type="matrix"
109
- values="0 0 0 0 0
110
- 0 1 0 0 0
111
- 0 0 0 0 0
112
- 0 0 0 1 0"
113
- result="GREEN_CHANNEL"
114
- />
115
-
116
- <feDisplacementMap
117
- in="SourceGraphic"
118
- in2="DISPLACEMENT_MAP"
119
- scale={displacementScale * ((mode === 'shader' ? 1 : -1) - aberrationIntensity * 0.03)}
120
- xChannelSelector="R"
121
- yChannelSelector="B"
122
- result="BLUE_DISPLACED"
123
- />
124
- <feColorMatrix
125
- in="BLUE_DISPLACED"
126
- type="matrix"
127
- values="0 0 0 0 0
128
- 0 0 0 0 0
129
- 0 0 1 0 0
130
- 0 0 0 1 0"
131
- result="BLUE_CHANNEL"
132
- />
105
+ {CHROMATIC_CHANNELS.map(channel => (
106
+ <React.Fragment key={channel.channelResult}>
107
+ <feDisplacementMap
108
+ in="SourceGraphic"
109
+ in2="DISPLACEMENT_MAP"
110
+ scale={getChromaticDisplacementScale(
111
+ mode,
112
+ displacementScale,
113
+ aberrationIntensity,
114
+ channel.aberrationFactor
115
+ )}
116
+ xChannelSelector="R"
117
+ yChannelSelector="B"
118
+ result={channel.result}
119
+ />
120
+ <feColorMatrix
121
+ in={channel.result}
122
+ type="matrix"
123
+ values={channel.colorMatrix}
124
+ result={channel.channelResult}
125
+ />
126
+ </React.Fragment>
127
+ ))}
133
128
 
134
129
  <feBlend in="GREEN_CHANNEL" in2="BLUE_CHANNEL" mode="screen" result="GB_COMBINED" />
135
130
  <feBlend in="RED_CHANNEL" in2="GB_COMBINED" mode="screen" result="RGB_COMBINED" />
@@ -160,9 +155,8 @@ const GlassFilterComponent: React.FC<GlassFilterProps> = ({
160
155
 
161
156
  GlassFilterComponent.displayName = 'GlassFilter';
162
157
 
163
- // Memoize component to prevent unnecessary re-renders
158
+ /** Shallow prop comparison to avoid redundant SVG filter regeneration. */
164
159
  export const GlassFilter = memo(GlassFilterComponent, (prevProps, nextProps) => {
165
- // Custom comparison: only re-render if props actually changed
166
160
  return (
167
161
  prevProps.id === nextProps.id &&
168
162
  prevProps.displacementScale === nextProps.displacementScale &&
@@ -58,7 +58,8 @@ function MyComponent() {
58
58
  | `debugOverLight` | boolean | false | Enable debug logging for overLight detection and configuration |
59
59
  | `mode` | 'standard' \| 'polar' \| 'prominent' \| 'shader' | 'standard' | The glass effect mode |
60
60
  | `onClick` | function | undefined | Click handler |
61
- | `withBorder` | boolean | true | Whether to show border effects |
61
+ | `border` | boolean \| GlassBorderConfig | true | Liquid glass rim (~0.5px). Structured: `{ enabled?, width?, opacity?, animated? }` |
62
+ | `withBorder` | boolean | true | **Deprecated** — use `border` (alias when `border` is omitted) |
62
63
  | `withLiquidBlur` | boolean | false | Whether to enable liquid blur effects |
63
64
  | `withoutEffects` | boolean | false | Whether to disable all visual effects |
64
65
  | `reducedMotion` | boolean | false | Force reduced motion preference |