@shohojdhara/atomix 0.6.4 → 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 +117 -38
  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 +66 -28
  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
@@ -4,11 +4,11 @@ exports[`AtomixGlass Visual Regression > matches snapshot with default props 1`]
4
4
  <div>
5
5
  <div
6
6
  class="c-atomix-glass"
7
- style="--atomix-glass-radius: 16px; --atomix-glass-transform: translate(0px, 0px) scale(1); --atomix-glass-container-position: absolute; --atomix-glass-position: absolute; --atomix-glass-top: 0px; --atomix-glass-left: 0px; --atomix-glass-right: auto; --atomix-glass-bottom: auto; --atomix-glass-width: 100%; --atomix-glass-height: 100%; --atomix-glass-border-width: var(--atomix-spacing-0-5, 0.125rem); --atomix-glass-blend-mode: overlay; --atomix-glass-border-gradient-1: linear-gradient(135deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.08399999999999999) 33%, rgba(255, 255, 255, 0.27999999999999997) 66%, rgba(255, 255, 255, 0) 100%); --atomix-glass-border-gradient-2: linear-gradient(135deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.22399999999999998) 33%, rgba(255, 255, 255, 0.42) 66%, rgba(255, 255, 255, 0) 100%); --atomix-glass-hover-1-opacity: 0; --atomix-glass-hover-1-gradient: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 50%); --atomix-glass-hover-2-opacity: 0; --atomix-glass-hover-2-gradient: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 80%); --atomix-glass-hover-3-opacity: 0; --atomix-glass-hover-3-gradient: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); --atomix-glass-base-opacity: 0; --atomix-glass-base-gradient: rgba(255, 255, 255, 0.1); --atomix-glass-overlay-opacity: 0; --atomix-glass-overlay-gradient: rgba(255, 255, 255, 0.05); --atomix-glass-overlay-highlight-opacity: 0; --atomix-glass-overlay-highlight-bg: radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.4) 0%, transparent 60%); --atomix-glass-hover-1-opacity: 0; --atomix-glass-hover-2-opacity: 0; --atomix-glass-hover-3-opacity: 0; --atomix-glass-base-opacity: 0; --atomix-glass-overlay-opacity: 0; --atomix-glass-overlay-highlight-opacity: 0;"
7
+ style="--atomix-glass-radius: 16px; --atomix-glass-transform: translate(0px, 0px) scaleX(1) scaleY(1); --atomix-glass-container-position: absolute; --atomix-glass-position: absolute; --atomix-glass-top: 0px; --atomix-glass-left: 0px; --atomix-glass-right: auto; --atomix-glass-bottom: auto; --atomix-glass-width: 100%; --atomix-glass-height: 100%; --atomix-glass-container-width: 100%; --atomix-glass-container-height: 100%; --atomix-glass-border-width: 0.5px; --atomix-glass-blend-mode: overlay; --atomix-glass-child-parallax: translate(0px, 0px); --atomix-glass-contrast: 1.02; --atomix-glass-brightness: 1.02; --atomix-glass-border-gradient-1: linear-gradient(136.5deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.027999999999999997) 33%, rgba(255, 255, 255, 0.09324) 66%, rgba(255, 255, 255, 0) 100%); --atomix-glass-border-gradient-2: linear-gradient(133.5deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.07448) 33%, rgba(255, 255, 255, 0.13999999999999999) 66%, rgba(255, 255, 255, 0) 100%); --atomix-glass-hover-1-gradient: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0) 50%); --atomix-glass-hover-2-gradient: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0) 80%); --atomix-glass-hover-3-gradient: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0) 100%); --atomix-glass-base-gradient: linear-gradient(180deg, rgba(0, 0, 0, 0.42) 0%, rgba(0, 0, 0, 0.22) 55%, rgba(0, 0, 0, 0.12) 100%); --atomix-glass-overlay-gradient: radial-gradient(120% 80% at 50% 0%, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0) 55%); --atomix-glass-hover-1-opacity: 0; --atomix-glass-hover-2-opacity: 0; --atomix-glass-hover-3-opacity: 0; --atomix-glass-base-opacity: 0.14; --atomix-glass-overlay-opacity: 0.1; --atomix-glass-overlay-highlight-opacity: 0.04900000000000001;"
8
8
  >
9
9
  <div
10
- class="c-atomix-glass__container "
11
- style="--atomix-glass-container-radius: 16px; --atomix-glass-container-backdrop: blur(0.1px) saturate(140%) contrast(1.4) brightness(0.9); --atomix-glass-container-shadow: 0 0 20px rgba(0, 0, 0, 0.15) inset, 0 4px 8px rgba(0, 0, 0, 0.08) inset; --atomix-glass-container-shadow-opacity: 1; --atomix-glass-container-bg: none; --atomix-glass-container-text-shadow: 0px 2px 12px rgba(0, 0, 0, 0.4); --atomix-glass-container-box-shadow: 0px 12px 40px rgba(0, 0, 0, 0.25); --atomix-glass-container-padding: 0;"
10
+ class="c-atomix-glass__container"
11
+ style="--atomix-glass-container-radius: 16px; --atomix-glass-container-backdrop: blur(20.16px) saturate(250%) contrast(1.02) brightness(1.02); --atomix-glass-container-shadow: inset 0 0.5px 0 rgba(255, 255, 255, 0.32), inset 0 1px 2px rgba(255, 255, 255, 0.06), 0 8px 32px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.16); --atomix-glass-container-shadow-opacity: 1; --atomix-glass-container-bg: none; --atomix-glass-container-text-shadow: 0px 2px 12px rgba(0, 0, 0, 0.4); --atomix-glass-container-box-shadow: 0 8px 32px rgba(0, 0, 0, 0.32), 0 2px 8px rgba(0, 0, 0, 0.18);"
12
12
  >
13
13
  <div
14
14
  class="c-atomix-glass__inner"
@@ -33,7 +33,7 @@ exports[`AtomixGlass Visual Regression > matches snapshot with default props 1`]
33
33
  stop-opacity="0"
34
34
  />
35
35
  <stop
36
- offset="76%"
36
+ offset="78.88%"
37
37
  stop-color="black"
38
38
  stop-opacity="0"
39
39
  />
@@ -75,7 +75,7 @@ exports[`AtomixGlass Visual Regression > matches snapshot with default props 1`]
75
75
  result="EDGE_MASK"
76
76
  >
77
77
  <fefunca
78
- tableValues="0 0.1 1"
78
+ tableValues="0 0.028000000000000004 1"
79
79
  type="discrete"
80
80
  />
81
81
  </fecomponenttransfer>
@@ -89,7 +89,7 @@ exports[`AtomixGlass Visual Regression > matches snapshot with default props 1`]
89
89
  in="SourceGraphic"
90
90
  in2="DISPLACEMENT_MAP"
91
91
  result="RED_DISPLACED"
92
- scale="-70"
92
+ scale="-28"
93
93
  xChannelSelector="R"
94
94
  yChannelSelector="B"
95
95
  />
@@ -98,15 +98,15 @@ exports[`AtomixGlass Visual Regression > matches snapshot with default props 1`]
98
98
  result="RED_CHANNEL"
99
99
  type="matrix"
100
100
  values="1 0 0 0 0
101
- 0 0 0 0 0
102
- 0 0 0 0 0
103
- 0 0 0 1 0"
101
+ 0 0 0 0 0
102
+ 0 0 0 0 0
103
+ 0 0 0 1 0"
104
104
  />
105
105
  <fedisplacementmap
106
106
  in="SourceGraphic"
107
107
  in2="DISPLACEMENT_MAP"
108
108
  result="GREEN_DISPLACED"
109
- scale="-72.8"
109
+ scale="-28.3136"
110
110
  xChannelSelector="R"
111
111
  yChannelSelector="B"
112
112
  />
@@ -115,15 +115,15 @@ exports[`AtomixGlass Visual Regression > matches snapshot with default props 1`]
115
115
  result="GREEN_CHANNEL"
116
116
  type="matrix"
117
117
  values="0 0 0 0 0
118
- 0 1 0 0 0
119
- 0 0 0 0 0
120
- 0 0 0 1 0"
118
+ 0 1 0 0 0
119
+ 0 0 0 0 0
120
+ 0 0 0 1 0"
121
121
  />
122
122
  <fedisplacementmap
123
123
  in="SourceGraphic"
124
124
  in2="DISPLACEMENT_MAP"
125
125
  result="BLUE_DISPLACED"
126
- scale="-74.2"
126
+ scale="-28.470399999999998"
127
127
  xChannelSelector="R"
128
128
  yChannelSelector="B"
129
129
  />
@@ -132,9 +132,9 @@ exports[`AtomixGlass Visual Regression > matches snapshot with default props 1`]
132
132
  result="BLUE_CHANNEL"
133
133
  type="matrix"
134
134
  values="0 0 0 0 0
135
- 0 0 0 0 0
136
- 0 0 1 0 0
137
- 0 0 0 1 0"
135
+ 0 0 0 0 0
136
+ 0 0 1 0 0
137
+ 0 0 0 1 0"
138
138
  />
139
139
  <feblend
140
140
  in="GREEN_CHANNEL"
@@ -151,7 +151,7 @@ exports[`AtomixGlass Visual Regression > matches snapshot with default props 1`]
151
151
  <fegaussianblur
152
152
  in="RGB_COMBINED"
153
153
  result="ABERRATED_BLURRED"
154
- stdDeviation="0"
154
+ stdDeviation="0.5644800000000001"
155
155
  />
156
156
  <fecomposite
157
157
  in="ABERRATED_BLURRED"
@@ -192,6 +192,7 @@ exports[`AtomixGlass Visual Regression > matches snapshot with default props 1`]
192
192
  </div>
193
193
  <div
194
194
  class="c-atomix-glass__content"
195
+ style="transform: var(--atomix-glass-child-parallax, none);"
195
196
  >
196
197
  <div>
197
198
  Default Glass
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ATOMIX_GLASS } from '../../lib/constants/components';
3
+ import {
4
+ buildGlassBorderCssVars,
5
+ formatGlassBorderWidth,
6
+ normalizeBorderConfig,
7
+ } from './glass-border-styles';
8
+
9
+ describe('normalizeBorderConfig', () => {
10
+ it('defaults to enabled when border and withBorder are omitted', () => {
11
+ expect(normalizeBorderConfig()).toEqual({
12
+ enabled: true,
13
+ width: '0.5px',
14
+ opacityMultiplier: 1,
15
+ animated: true,
16
+ });
17
+ });
18
+
19
+ it('respects legacy withBorder=false', () => {
20
+ expect(normalizeBorderConfig(undefined, false).enabled).toBe(false);
21
+ });
22
+
23
+ it('border object overrides withBorder', () => {
24
+ expect(
25
+ normalizeBorderConfig({ enabled: true, width: 1, opacity: 0.5, animated: false }, false)
26
+ ).toEqual({
27
+ enabled: true,
28
+ width: '1px',
29
+ opacityMultiplier: 0.5,
30
+ animated: false,
31
+ });
32
+ });
33
+ });
34
+
35
+ describe('formatGlassBorderWidth', () => {
36
+ it('formats numbers as px', () => {
37
+ expect(formatGlassBorderWidth(2)).toBe('2px');
38
+ });
39
+ });
40
+
41
+ describe('buildGlassBorderCssVars', () => {
42
+ it('returns static gradients at zero mouse offset', () => {
43
+ const vars = buildGlassBorderCssVars({
44
+ mouseOffset: { x: 0, y: 0 },
45
+ mouseVelocity: { x: 0, y: 0 },
46
+ elasticVelocity: { x: 0, y: 0 },
47
+ borderOpacity: ATOMIX_GLASS.BORDER.DARK.opacity,
48
+ opacityMultiplier: 1,
49
+ tensionFactor: 0,
50
+ });
51
+
52
+ expect(vars[ATOMIX_GLASS.BORDER.GRADIENT_CSS_VARS.GRADIENT_1]).toContain('linear-gradient');
53
+ expect(vars[ATOMIX_GLASS.BORDER.GRADIENT_CSS_VARS.GRADIENT_2]).toContain('linear-gradient');
54
+ expect(vars[ATOMIX_GLASS.BORDER.GRADIENT_CSS_VARS.GRADIENT_1]).toContain(
55
+ `${ATOMIX_GLASS.BORDER.GRADIENT.BASE_ANGLE + ATOMIX_GLASS.BORDER.GRADIENT.CHROMATIC_OFFSET}deg`
56
+ );
57
+ });
58
+ });
@@ -0,0 +1,136 @@
1
+ import { ATOMIX_GLASS } from '../../lib/constants/components';
2
+ import { smoothstep } from './glass-utils';
3
+ import type { GlassBorderConfig } from '../../lib/types/components';
4
+ import type { MousePosition } from '../../lib/types/components';
5
+
6
+ const { BORDER, CONSTANTS } = ATOMIX_GLASS;
7
+ const BORDER_GRADIENT = BORDER.GRADIENT;
8
+ const WHITE = CONSTANTS.PALETTE.WHITE;
9
+
10
+ /** Resolved border configuration after normalizing props. */
11
+ export interface ResolvedGlassBorderConfig {
12
+ enabled: boolean;
13
+ width: string;
14
+ opacityMultiplier: number;
15
+ animated: boolean;
16
+ }
17
+
18
+ /**
19
+ * Formats border width for CSS custom properties.
20
+ */
21
+ export function formatGlassBorderWidth(value: string | number | undefined): string {
22
+ if (value === undefined) {
23
+ return BORDER.DEFAULT_WIDTH;
24
+ }
25
+ return typeof value === 'number' ? `${value}px` : value;
26
+ }
27
+
28
+ /**
29
+ * Resolves `border` and legacy `withBorder` into a single configuration object.
30
+ */
31
+ export function normalizeBorderConfig(
32
+ border?: boolean | GlassBorderConfig,
33
+ withBorder?: boolean
34
+ ): ResolvedGlassBorderConfig {
35
+ const legacyDefault = withBorder ?? true;
36
+
37
+ if (border === undefined) {
38
+ return {
39
+ enabled: legacyDefault,
40
+ width: BORDER.DEFAULT_WIDTH,
41
+ opacityMultiplier: 1,
42
+ animated: true,
43
+ };
44
+ }
45
+
46
+ if (typeof border === 'boolean') {
47
+ return {
48
+ enabled: border,
49
+ width: BORDER.DEFAULT_WIDTH,
50
+ opacityMultiplier: 1,
51
+ animated: true,
52
+ };
53
+ }
54
+
55
+ return {
56
+ enabled: border.enabled ?? legacyDefault,
57
+ width: formatGlassBorderWidth(border.width),
58
+ opacityMultiplier: border.opacity ?? 1,
59
+ animated: border.animated !== false,
60
+ };
61
+ }
62
+
63
+ export interface BuildGlassBorderCssVarsParams {
64
+ mouseOffset: MousePosition;
65
+ mouseVelocity: MousePosition;
66
+ elasticVelocity: MousePosition;
67
+ borderOpacity: number;
68
+ opacityMultiplier?: number;
69
+ tensionFactor?: number;
70
+ }
71
+
72
+ /**
73
+ * Builds animated chromatic rim CSS variables for border layers 1 and 2.
74
+ * When empty, SCSS static conic/linear fallbacks apply.
75
+ */
76
+ export function buildGlassBorderCssVars(
77
+ params: BuildGlassBorderCssVarsParams
78
+ ): Record<string, string> {
79
+ const {
80
+ mouseOffset,
81
+ mouseVelocity,
82
+ elasticVelocity,
83
+ borderOpacity,
84
+ opacityMultiplier = 1,
85
+ tensionFactor = 0,
86
+ } = params;
87
+
88
+ const mx = mouseOffset.x;
89
+ const my = mouseOffset.y;
90
+ const absMx = Math.abs(mx);
91
+
92
+ const velocityRotation =
93
+ (mouseVelocity.x + elasticVelocity.x) * BORDER_GRADIENT.VELOCITY_ANGLE_MULTIPLIER;
94
+ const borderGradientAngle =
95
+ BORDER_GRADIENT.BASE_ANGLE + mx * BORDER_GRADIENT.ANGLE_MULTIPLIER + velocityRotation;
96
+
97
+ const chromaticOffset = BORDER_GRADIENT.CHROMATIC_OFFSET;
98
+ const angleR = borderGradientAngle - chromaticOffset;
99
+ const angleB = borderGradientAngle + chromaticOffset;
100
+
101
+ const borderStop1 = Math.max(
102
+ BORDER_GRADIENT.STOP_1.MIN,
103
+ BORDER_GRADIENT.STOP_1.BASE + my * BORDER_GRADIENT.STOP_1.MULTIPLIER
104
+ );
105
+ const borderStop2 = Math.min(
106
+ BORDER_GRADIENT.STOP_2.MAX,
107
+ BORDER_GRADIENT.STOP_2.BASE + my * BORDER_GRADIENT.STOP_2.MULTIPLIER
108
+ );
109
+
110
+ const tensionGlow = 1 + tensionFactor * 0.5;
111
+ const opacities = BORDER_GRADIENT.OPACITY;
112
+ const borderOpacities = [
113
+ (opacities.BASE_1 + absMx * opacities.MULTIPLIER_LOW) * tensionGlow,
114
+ (opacities.BASE_2 + absMx * opacities.MULTIPLIER_HIGH) * tensionGlow,
115
+ (opacities.BASE_3 + absMx * opacities.MULTIPLIER_LOW) * tensionGlow,
116
+ (opacities.BASE_4 + absMx * opacities.MULTIPLIER_HIGH) * tensionGlow,
117
+ ];
118
+
119
+ const configBorderOpacity = borderOpacity * opacityMultiplier;
120
+
121
+ const gradient1 = `linear-gradient(${angleB}deg, rgba(${WHITE}, 0) 0%, rgba(${WHITE}, ${(borderOpacities[0] ?? 1) * configBorderOpacity}) ${borderStop1}%, rgba(${WHITE}, ${(borderOpacities[1] ?? 1) * configBorderOpacity}) ${borderStop2}%, rgba(${WHITE}, 0) 100%)`;
122
+ const gradient2 = `linear-gradient(${angleR}deg, rgba(${WHITE}, 0) 0%, rgba(${WHITE}, ${(borderOpacities[2] ?? 1) * configBorderOpacity}) ${borderStop1}%, rgba(${WHITE}, ${(borderOpacities[3] ?? 1) * configBorderOpacity}) ${borderStop2}%, rgba(${WHITE}, 0) 100%)`;
123
+
124
+ return {
125
+ [BORDER.GRADIENT_CSS_VARS.GRADIENT_1]: gradient1,
126
+ [BORDER.GRADIENT_CSS_VARS.GRADIENT_2]: gradient2,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Computes tension factor from elastic translation magnitude (0–1).
132
+ */
133
+ export function computeBorderTensionFactor(elasticTranslation: MousePosition): number {
134
+ const magnitude = Math.hypot(elasticTranslation.x, elasticTranslation.y);
135
+ return smoothstep(magnitude / 80);
136
+ }