@shohojdhara/atomix 0.4.1 → 0.4.3

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 (131) hide show
  1. package/dist/atomix.css +9351 -9259
  2. package/dist/atomix.css.map +1 -1
  3. package/dist/atomix.min.css +4 -4
  4. package/dist/atomix.min.css.map +1 -1
  5. package/dist/charts.d.ts +12 -19
  6. package/dist/charts.js +555 -358
  7. package/dist/charts.js.map +1 -1
  8. package/dist/core.d.ts +21 -24
  9. package/dist/core.js +435 -265
  10. package/dist/core.js.map +1 -1
  11. package/dist/forms.d.ts +11 -18
  12. package/dist/forms.js +411 -257
  13. package/dist/forms.js.map +1 -1
  14. package/dist/heavy.d.ts +14 -21
  15. package/dist/heavy.js +409 -254
  16. package/dist/heavy.js.map +1 -1
  17. package/dist/index.d.ts +38 -41
  18. package/dist/index.esm.js +731 -487
  19. package/dist/index.esm.js.map +1 -1
  20. package/dist/index.js +733 -492
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.min.js +1 -1
  23. package/dist/index.min.js.map +1 -1
  24. package/package.json +1 -1
  25. package/scripts/atomix-cli.js +34 -1
  26. package/src/components/AtomixGlass/AtomixGlass.tsx +82 -54
  27. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +17 -18
  28. package/src/components/AtomixGlass/README.md +5 -5
  29. package/src/components/AtomixGlass/stories/Customization.stories.tsx +2 -2
  30. package/src/components/AtomixGlass/stories/Examples.stories.tsx +42 -42
  31. package/src/components/AtomixGlass/stories/Modes.stories.tsx +5 -5
  32. package/src/components/AtomixGlass/stories/Overview.stories.tsx +3 -3
  33. package/src/components/AtomixGlass/stories/Performance.stories.tsx +2 -2
  34. package/src/components/AtomixGlass/stories/Playground.stories.tsx +45 -45
  35. package/src/components/AtomixGlass/stories/Shaders.stories.tsx +3 -3
  36. package/src/components/Badge/Badge.stories.tsx +1 -1
  37. package/src/components/Badge/Badge.tsx +1 -1
  38. package/src/components/Breadcrumb/Breadcrumb.tsx +90 -77
  39. package/src/components/Breadcrumb/index.ts +2 -2
  40. package/src/components/Button/Button.stories.tsx +1 -1
  41. package/src/components/Button/Button.tsx +2 -1
  42. package/src/components/Button/README.md +2 -2
  43. package/src/components/Callout/Callout.test.tsx +3 -3
  44. package/src/components/Callout/Callout.tsx +2 -2
  45. package/src/components/Callout/README.md +2 -2
  46. package/src/components/Card/Card.tsx +31 -11
  47. package/src/components/Chart/Chart.stories.tsx +1 -1
  48. package/src/components/Chart/Chart.tsx +5 -5
  49. package/src/components/Chart/TreemapChart.tsx +37 -29
  50. package/src/components/DatePicker/readme.md +3 -3
  51. package/src/components/Dropdown/Dropdown.stories.tsx +1 -1
  52. package/src/components/Dropdown/Dropdown.tsx +276 -273
  53. package/src/components/EdgePanel/EdgePanel.stories.tsx +7 -7
  54. package/src/components/Footer/FooterLink.tsx +2 -2
  55. package/src/components/Form/Checkbox.stories.tsx +1 -1
  56. package/src/components/Form/Checkbox.tsx +1 -1
  57. package/src/components/Form/Input.stories.tsx +1 -1
  58. package/src/components/Form/Input.tsx +1 -1
  59. package/src/components/Form/Radio.stories.tsx +1 -1
  60. package/src/components/Form/Radio.tsx +1 -1
  61. package/src/components/Form/Select.stories.tsx +1 -1
  62. package/src/components/Form/Select.tsx +1 -1
  63. package/src/components/Form/Textarea.stories.tsx +1 -1
  64. package/src/components/Form/Textarea.tsx +1 -1
  65. package/src/components/Hero/Hero.stories.tsx +2 -2
  66. package/src/components/Hero/Hero.tsx +2 -2
  67. package/src/components/Messages/Messages.stories.tsx +1 -1
  68. package/src/components/Messages/Messages.tsx +2 -2
  69. package/src/components/Modal/Modal.stories.tsx +1 -1
  70. package/src/components/Navigation/Nav/Nav.stories.tsx +2 -2
  71. package/src/components/Navigation/Nav/Nav.tsx +1 -1
  72. package/src/components/Navigation/Nav/NavItem.tsx +6 -3
  73. package/src/components/Navigation/Navbar/Navbar.stories.tsx +3 -3
  74. package/src/components/Navigation/Navbar/Navbar.tsx +1 -1
  75. package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +2 -2
  76. package/src/components/Navigation/SideMenu/SideMenu.tsx +1 -1
  77. package/src/components/Pagination/Pagination.stories.tsx +1 -1
  78. package/src/components/Pagination/Pagination.tsx +1 -1
  79. package/src/components/Popover/Popover.stories.tsx +1 -1
  80. package/src/components/Popover/Popover.tsx +1 -1
  81. package/src/components/Progress/Progress.tsx +1 -1
  82. package/src/components/Rating/Rating.stories.tsx +1 -1
  83. package/src/components/Rating/Rating.test.tsx +73 -0
  84. package/src/components/Rating/Rating.tsx +25 -37
  85. package/src/components/Spinner/Spinner.tsx +1 -1
  86. package/src/components/Steps/Steps.stories.tsx +1 -1
  87. package/src/components/Steps/Steps.tsx +2 -2
  88. package/src/components/Tabs/Tabs.stories.tsx +1 -1
  89. package/src/components/Tabs/Tabs.tsx +1 -1
  90. package/src/components/Todo/Todo.tsx +0 -1
  91. package/src/components/Toggle/Toggle.stories.tsx +1 -1
  92. package/src/components/Toggle/Toggle.tsx +1 -1
  93. package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
  94. package/src/components/VideoPlayer/VideoPlayer.stories.tsx +2 -2
  95. package/src/lib/composables/__tests__/useAtomixGlassPerf.test.tsx +88 -0
  96. package/src/lib/composables/__tests__/useChart.test.ts +50 -0
  97. package/src/lib/composables/__tests__/useChart.test.tsx +139 -0
  98. package/src/lib/composables/__tests__/useHeroBackgroundSlider.test.tsx +59 -0
  99. package/src/lib/composables/__tests__/useSliderAutoplay.test.tsx +68 -0
  100. package/src/lib/composables/atomix-glass/useGlassBackgroundDetection.ts +329 -0
  101. package/src/lib/composables/atomix-glass/useGlassCornerRadius.ts +82 -0
  102. package/src/lib/composables/atomix-glass/useGlassMouseTracking.ts +153 -0
  103. package/src/lib/composables/atomix-glass/useGlassOverLight.ts +198 -0
  104. package/src/lib/composables/atomix-glass/useGlassSize.ts +117 -0
  105. package/src/lib/composables/atomix-glass/useGlassState.ts +112 -0
  106. package/src/lib/composables/atomix-glass/useGlassTransforms.ts +160 -0
  107. package/src/lib/composables/glass-styles.ts +302 -0
  108. package/src/lib/composables/index.ts +0 -4
  109. package/src/lib/composables/useAtomixGlass.ts +331 -522
  110. package/src/lib/composables/useAtomixGlassStyles.ts +307 -0
  111. package/src/lib/composables/useBarChart.ts +1 -1
  112. package/src/lib/composables/useBreadcrumb.ts +6 -6
  113. package/src/lib/composables/useChart.ts +104 -21
  114. package/src/lib/composables/useHeroBackgroundSlider.ts +16 -7
  115. package/src/lib/composables/useSlider.ts +66 -34
  116. package/src/lib/theme/devtools/CLI.ts +1 -1
  117. package/src/lib/theme/utils/__tests__/themeUtils.test.ts +213 -0
  118. package/src/lib/types/components.ts +18 -21
  119. package/src/lib/utils/__tests__/dom.test.ts +100 -0
  120. package/src/lib/utils/__tests__/fontPreloader.test.ts +102 -0
  121. package/src/styles/02-tools/_tools.breakpoints.scss +1 -1
  122. package/src/styles/02-tools/_tools.utility-api.scss +6 -6
  123. package/src/styles/06-components/_components.accordion.scss +0 -2
  124. package/src/styles/06-components/_components.chart.scss +0 -1
  125. package/src/styles/06-components/_components.dropdown.scss +0 -1
  126. package/src/styles/06-components/_components.edge-panel.scss +0 -2
  127. package/src/styles/06-components/_components.photoviewer.scss +0 -1
  128. package/src/styles/06-components/_components.river.scss +0 -1
  129. package/src/styles/06-components/_components.slider.scss +0 -3
  130. package/src/styles/99-utilities/_utilities.glass-fixes.scss +0 -1
  131. package/src/styles/99-utilities/_utilities.text.scss +1 -0
@@ -45,6 +45,16 @@ export function useSlider(options: UseSliderOptions): UseSliderReturn {
45
45
  const autoplayRef = useRef<NodeJS.Timeout | null>(null);
46
46
  const [autoplayRunning, setAutoplayRunning] = useState(false);
47
47
 
48
+ // Create a ref to store the latest state for the autoplay interval
49
+ const sliderStateRef = useRef({
50
+ isTransitioning: false,
51
+ loop,
52
+ slides,
53
+ slidesToShow,
54
+ speed,
55
+ onSlideChange,
56
+ });
57
+
48
58
  const [realIndex, setRealIndex] = useState(initialSlide);
49
59
  const [internalIndex, setInternalIndex] = useState(0);
50
60
  const [isTransitioning, setIsTransitioning] = useState(false);
@@ -76,6 +86,18 @@ export function useSlider(options: UseSliderOptions): UseSliderReturn {
76
86
  return -(internalIndex * slideWidth) + dragOffset;
77
87
  }, [slideWidth, internalIndex, dragOffset]);
78
88
 
89
+ // Update the ref whenever the relevant state/props change
90
+ useEffect(() => {
91
+ sliderStateRef.current = {
92
+ isTransitioning,
93
+ loop,
94
+ slides,
95
+ slidesToShow,
96
+ speed,
97
+ onSlideChange,
98
+ };
99
+ }, [isTransitioning, loop, slides, slidesToShow, speed, onSlideChange]);
100
+
79
101
  // Autoplay effect
80
102
  useEffect(() => {
81
103
  if (!autoplay) {
@@ -102,9 +124,19 @@ export function useSlider(options: UseSliderOptions): UseSliderReturn {
102
124
 
103
125
  // Create new interval
104
126
  autoplayRef.current = setInterval(() => {
127
+ // Use ref to get the latest state without resetting the interval
128
+ const {
129
+ isTransitioning: currentIsTransitioning,
130
+ loop: currentLoop,
131
+ slides: currentSlides,
132
+ slidesToShow: currentSlidesToShow,
133
+ speed: currentSpeed,
134
+ onSlideChange: currentOnSlideChange,
135
+ } = sliderStateRef.current;
136
+
105
137
  // We need to use a functional update to get the latest values
106
138
  setRealIndex(prevRealIndex => {
107
- if (isTransitioning) return prevRealIndex;
139
+ if (currentIsTransitioning) return prevRealIndex;
108
140
 
109
141
  // Stop autoplay on interaction if disableOnInteraction is true
110
142
  if (disableOnInteraction && autoplayRef.current) {
@@ -114,49 +146,49 @@ export function useSlider(options: UseSliderOptions): UseSliderReturn {
114
146
  }
115
147
 
116
148
  let nextIndex;
117
- if (loop) {
118
- nextIndex = (prevRealIndex + 1) % slides.length;
149
+ if (currentLoop) {
150
+ nextIndex = (prevRealIndex + 1) % currentSlides.length;
119
151
  } else {
120
- nextIndex = Math.min(prevRealIndex + 1, slides.length - slidesToShow);
152
+ nextIndex = Math.min(prevRealIndex + 1, currentSlides.length - currentSlidesToShow);
121
153
  }
122
154
 
123
155
  // Trigger the slide change
124
156
  if (reverseDirection) {
125
157
  // For reverse direction, we would go to previous slide
126
- const prevIndex = loop
158
+ const prevIndex = currentLoop
127
159
  ? prevRealIndex === 0
128
- ? slides.length - 1
160
+ ? currentSlides.length - 1
129
161
  : prevRealIndex - 1
130
162
  : Math.max(prevRealIndex - 1, 0);
131
- setInternalIndex(loop ? slides.length + prevIndex : prevIndex);
163
+ setInternalIndex(currentLoop ? currentSlides.length + prevIndex : prevIndex);
132
164
  setIsTransitioning(true);
133
165
  setDragOffset(0);
134
166
 
135
167
  setTimeout(() => {
136
168
  setIsTransitioning(false);
137
- onSlideChange?.(prevIndex);
138
- }, speed);
169
+ currentOnSlideChange?.(prevIndex);
170
+ }, currentSpeed);
139
171
 
140
172
  return prevIndex;
141
173
  } else {
142
174
  // Normal direction
143
- setInternalIndex(loop ? slides.length + nextIndex : nextIndex);
175
+ setInternalIndex(currentLoop ? currentSlides.length + nextIndex : nextIndex);
144
176
  setIsTransitioning(true);
145
177
  setDragOffset(0);
146
178
 
147
179
  setTimeout(() => {
148
180
  setIsTransitioning(false);
149
- onSlideChange?.(nextIndex);
181
+ currentOnSlideChange?.(nextIndex);
150
182
 
151
183
  // Reposition after transition ends for looped sliders
152
- if (loop && nextIndex >= slides.length * 2) {
184
+ if (currentLoop && nextIndex >= currentSlides.length * 2) {
153
185
  repositioningRef.current = true;
154
- setInternalIndex(slides.length + nextIndex);
186
+ setInternalIndex(currentSlides.length + nextIndex);
155
187
  setTimeout(() => {
156
188
  repositioningRef.current = false;
157
189
  }, 0);
158
190
  }
159
- }, speed);
191
+ }, currentSpeed);
160
192
 
161
193
  return nextIndex;
162
194
  }
@@ -182,35 +214,44 @@ export function useSlider(options: UseSliderOptions): UseSliderReturn {
182
214
  }
183
215
 
184
216
  autoplayRef.current = setInterval(() => {
217
+ const {
218
+ isTransitioning: currentIsTransitioning,
219
+ loop: currentLoop,
220
+ slides: currentSlides,
221
+ slidesToShow: currentSlidesToShow,
222
+ speed: currentSpeed,
223
+ onSlideChange: currentOnSlideChange,
224
+ } = sliderStateRef.current;
225
+
185
226
  setRealIndex(prevRealIndex => {
186
- if (isTransitioning) return prevRealIndex;
227
+ if (currentIsTransitioning) return prevRealIndex;
187
228
 
188
229
  let nextIndex;
189
- if (loop) {
190
- nextIndex = (prevRealIndex + 1) % slides.length;
230
+ if (currentLoop) {
231
+ nextIndex = (prevRealIndex + 1) % currentSlides.length;
191
232
  } else {
192
- nextIndex = Math.min(prevRealIndex + 1, slides.length - slidesToShow);
233
+ nextIndex = Math.min(prevRealIndex + 1, currentSlides.length - currentSlidesToShow);
193
234
  }
194
235
 
195
- setInternalIndex(loop ? slides.length + nextIndex : nextIndex);
236
+ setInternalIndex(currentLoop ? currentSlides.length + nextIndex : nextIndex);
196
237
  setIsTransitioning(true);
197
238
  setDragOffset(0);
198
239
 
199
240
  setTimeout(() => {
200
241
  setIsTransitioning(false);
201
- onSlideChange?.(nextIndex);
242
+ currentOnSlideChange?.(nextIndex);
202
243
 
203
- if (loop) {
244
+ if (currentLoop) {
204
245
  // Reposition after transition ends
205
- if (nextIndex >= slides.length * 2) {
246
+ if (nextIndex >= currentSlides.length * 2) {
206
247
  repositioningRef.current = true;
207
- setInternalIndex(slides.length + nextIndex);
248
+ setInternalIndex(currentSlides.length + nextIndex);
208
249
  setTimeout(() => {
209
250
  repositioningRef.current = false;
210
251
  }, 0);
211
252
  }
212
253
  }
213
- }, speed);
254
+ }, currentSpeed);
214
255
 
215
256
  return nextIndex;
216
257
  });
@@ -237,16 +278,7 @@ export function useSlider(options: UseSliderOptions): UseSliderReturn {
237
278
  }
238
279
  setAutoplayRunning(false);
239
280
  };
240
- }, [
241
- autoplay,
242
- slides.length,
243
- loop,
244
- slidesToShow,
245
- isTransitioning,
246
- speed,
247
- onSlideChange,
248
- repositioningRef,
249
- ]);
281
+ }, [autoplay, repositioningRef]);
250
282
 
251
283
  // Initialize
252
284
  useEffect(() => {
@@ -11,7 +11,7 @@ import { Command } from 'commander';
11
11
  import chalk from 'chalk';
12
12
  import * as fs from 'fs';
13
13
  import * as path from 'path';
14
- import { ThemeValidator } from './ThemeValidator';
14
+ import { ThemeValidator } from './ThemeValidator.js';
15
15
  import boxen from 'boxen';
16
16
 
17
17
  const program = new Command();
@@ -0,0 +1,213 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ hexToRgb,
4
+ rgbToHex,
5
+ getLuminance,
6
+ getContrastRatio,
7
+ getContrastText,
8
+ lighten,
9
+ darken,
10
+ alpha,
11
+ emphasize,
12
+ createSpacing,
13
+ } from '../themeUtils';
14
+
15
+ describe('Theme Utils', () => {
16
+ describe('hexToRgb', () => {
17
+ it('should convert valid 6-digit hex codes', () => {
18
+ expect(hexToRgb('#ffffff')).toEqual({ r: 255, g: 255, b: 255 });
19
+ expect(hexToRgb('#000000')).toEqual({ r: 0, g: 0, b: 0 });
20
+ expect(hexToRgb('#ff0000')).toEqual({ r: 255, g: 0, b: 0 });
21
+ });
22
+
23
+ it('should handle hex codes without hash', () => {
24
+ expect(hexToRgb('ffffff')).toEqual({ r: 255, g: 255, b: 255 });
25
+ });
26
+
27
+ it('should return null for invalid hex codes', () => {
28
+ expect(hexToRgb('invalid')).toBeNull();
29
+ expect(hexToRgb('#xyz')).toBeNull();
30
+ // Current implementation does NOT support 3-digit hex
31
+ expect(hexToRgb('#fff')).toBeNull();
32
+ });
33
+ });
34
+
35
+ describe('rgbToHex', () => {
36
+ it('should convert RGB values to hex string', () => {
37
+ expect(rgbToHex(255, 255, 255)).toBe('#ffffff');
38
+ expect(rgbToHex(0, 0, 0)).toBe('#000000');
39
+ expect(rgbToHex(255, 0, 0)).toBe('#ff0000');
40
+ });
41
+
42
+ it('should clamp values to 0-255 range', () => {
43
+ expect(rgbToHex(300, -50, 256)).toBe('#ff00ff');
44
+ });
45
+
46
+ it('should handle null/undefined values as 0', () => {
47
+ // @ts-ignore
48
+ expect(rgbToHex(null, undefined, 255)).toBe('#0000ff');
49
+ });
50
+ });
51
+
52
+ describe('getLuminance', () => {
53
+ it('should calculate luminance for standard colors', () => {
54
+ expect(getLuminance('#000000')).toBe(0);
55
+ expect(getLuminance('#ffffff')).toBe(1);
56
+ });
57
+
58
+ it('should return 0 for invalid colors', () => {
59
+ expect(getLuminance('invalid')).toBe(0);
60
+ });
61
+ });
62
+
63
+ describe('getContrastRatio', () => {
64
+ it('should calculate contrast ratio between black and white', () => {
65
+ const ratio = getContrastRatio('#000000', '#ffffff');
66
+ expect(ratio).toBeCloseTo(21, 1);
67
+ });
68
+
69
+ it('should be commutative', () => {
70
+ const r1 = getContrastRatio('#ffffff', '#000000');
71
+ const r2 = getContrastRatio('#000000', '#ffffff');
72
+ expect(r1).toBe(r2);
73
+ });
74
+
75
+ it('should return 1 for same colors', () => {
76
+ expect(getContrastRatio('#ffffff', '#ffffff')).toBe(1);
77
+ });
78
+ });
79
+
80
+ describe('getContrastText', () => {
81
+ it('should return white for dark backgrounds', () => {
82
+ expect(getContrastText('#000000')).toBe('#FFFFFF');
83
+ expect(getContrastText('#333333')).toBe('#FFFFFF');
84
+ });
85
+
86
+ it('should return black for light backgrounds', () => {
87
+ expect(getContrastText('#ffffff')).toBe('#000000');
88
+ expect(getContrastText('#eeeeee')).toBe('#000000');
89
+ });
90
+
91
+ it('should respect custom threshold', () => {
92
+ // Very strict threshold might force one or the other
93
+ // But standard usage is tested above
94
+ // Testing logic fallback:
95
+ // If contrast with white < threshold AND contrast with black < threshold
96
+ // it returns whichever has higher contrast.
97
+
98
+ // Let's test a middle grey.
99
+ // #808080.
100
+ // Contrast with white: ~3.95
101
+ // Contrast with black: ~5.32
102
+ // Default threshold is 3.
103
+ // 3.95 >= 3 -> returns white (checked first).
104
+ // Wait, let's check code order:
105
+ // if (contrastWithWhite >= threshold) return '#FFFFFF';
106
+
107
+ expect(getContrastText('#808080', 3)).toBe('#FFFFFF');
108
+
109
+ // If we raise threshold to 4.
110
+ // Contrast white (3.95) < 4.
111
+ // Contrast black (5.32) >= 4.
112
+ // Returns black.
113
+ expect(getContrastText('#808080', 4)).toBe('#000000');
114
+ });
115
+ });
116
+
117
+ describe('lighten', () => {
118
+ it('should lighten a color', () => {
119
+ const result = lighten('#000000', 0.5); // Lighten black by 50%
120
+ // black is 0, 0, 0.
121
+ // lightenValue = val + (255 - val) * amount
122
+ // 0 + 255 * 0.5 = 127.5 -> 128 (0x80)
123
+ expect(result).toBe('#808080');
124
+ });
125
+
126
+ it('should return original color if invalid', () => {
127
+ expect(lighten('invalid')).toBe('invalid');
128
+ });
129
+ });
130
+
131
+ describe('darken', () => {
132
+ it('should darken a color', () => {
133
+ const result = darken('#ffffff', 0.5); // Darken white by 50%
134
+ // white is 255.
135
+ // darkenValue = val * (1 - amount)
136
+ // 255 * 0.5 = 127.5 -> 128 (0x80)
137
+ expect(result).toBe('#808080');
138
+ });
139
+
140
+ it('should return original color if invalid', () => {
141
+ expect(darken('invalid')).toBe('invalid');
142
+ });
143
+ });
144
+
145
+ describe('alpha', () => {
146
+ it('should add alpha to hex color', () => {
147
+ expect(alpha('#ffffff', 0.5)).toBe('rgba(255, 255, 255, 0.5)');
148
+ });
149
+
150
+ it('should clamp opacity', () => {
151
+ expect(alpha('#ffffff', 1.5)).toBe('rgba(255, 255, 255, 1)');
152
+ expect(alpha('#ffffff', -0.5)).toBe('rgba(255, 255, 255, 0)');
153
+ });
154
+
155
+ it('should return original color if invalid', () => {
156
+ expect(alpha('invalid', 0.5)).toBe('invalid');
157
+ });
158
+ });
159
+
160
+ describe('emphasize', () => {
161
+ it('should darken light colors', () => {
162
+ // #ffffff luminance is 1 (> 0.5) -> darken
163
+ const result = emphasize('#ffffff', 0.2);
164
+ // darken #ffffff by 0.2
165
+ // 255 * 0.8 = 204 (0xCC)
166
+ expect(result).toBe('#cccccc');
167
+ });
168
+
169
+ it('should lighten dark colors', () => {
170
+ // #000000 luminance is 0 (<= 0.5) -> lighten
171
+ const result = emphasize('#000000', 0.2);
172
+ // lighten #000000 by 0.2
173
+ // 0 + 255 * 0.2 = 51 (0x33)
174
+ expect(result).toBe('#333333');
175
+ });
176
+ });
177
+
178
+ describe('createSpacing', () => {
179
+ it('should use default spacing (4px)', () => {
180
+ const spacing = createSpacing();
181
+ expect(spacing(1)).toBe('4px');
182
+ expect(spacing(2)).toBe('8px');
183
+ expect(spacing(1, 2)).toBe('4px 8px');
184
+ });
185
+
186
+ it('should use numeric spacing multiplier', () => {
187
+ const spacing = createSpacing(8);
188
+ expect(spacing(1)).toBe('8px');
189
+ expect(spacing(2)).toBe('16px');
190
+ });
191
+
192
+ it('should use array spacing', () => {
193
+ const scale = [0, 4, 8, 16, 32];
194
+ const spacing = createSpacing(scale);
195
+ expect(spacing(1)).toBe('4px');
196
+ expect(spacing(3)).toBe('16px');
197
+ // fallback to value if index out of bounds?
198
+ // Code: spacingInput[value] || value
199
+ expect(spacing(10)).toBe('10px');
200
+ });
201
+
202
+ it('should use function spacing', () => {
203
+ const customFn = (...values: number[]) => values.map(v => `${v}rem`).join(' ');
204
+ const spacing = createSpacing(customFn);
205
+ expect(spacing(1, 2)).toBe('1rem 2rem');
206
+ });
207
+
208
+ it('should handle no arguments', () => {
209
+ const spacing = createSpacing();
210
+ expect(spacing()).toBe('0px');
211
+ });
212
+ });
213
+ });
@@ -215,20 +215,20 @@ export interface OverLightObjectConfig {
215
215
  /**
216
216
  * AtomixGlass component props interface
217
217
  */
218
- export interface AtomixGlassProps {
218
+ export interface AtomixGlassProps extends React.HTMLAttributes<HTMLDivElement> {
219
219
  children?: React.ReactNode;
220
220
  displacementScale?: number;
221
221
  blurAmount?: number;
222
222
  saturation?: number;
223
223
  aberrationIntensity?: number;
224
224
  elasticity?: number;
225
- cornerRadius?: number;
225
+ borderRadius?: number;
226
226
  globalMousePosition?: MousePosition;
227
227
  mouseOffset?: MousePosition;
228
228
  mouseContainer?: React.RefObject<HTMLElement | null> | null;
229
- className?: string;
230
229
  padding?: string;
231
- style?: React.CSSProperties;
230
+ height?: string | number;
231
+ width?: string | number;
232
232
  overLight?: OverLightConfig;
233
233
  mode?: DisplacementMode;
234
234
  onClick?: () => void;
@@ -245,33 +245,25 @@ export interface AtomixGlassProps {
245
245
  | 'waves'
246
246
  | 'noise';
247
247
 
248
- /**
249
- * Accessibility props
250
- */
251
- 'aria-label'?: string;
252
- 'aria-describedby'?: string;
253
- role?: string;
254
- tabIndex?: number;
255
-
256
248
  /**
257
249
  * Performance and accessibility options
258
250
  */
259
251
  reducedMotion?: boolean;
260
252
  highContrast?: boolean;
261
- disableEffects?: boolean;
262
- enableLiquidBlur?: boolean;
263
- enableBorderEffect?: boolean;
264
- enableOverLightLayers?: boolean;
253
+ withoutEffects?: boolean;
254
+ withLiquidBlur?: boolean;
255
+ withBorder?: boolean;
256
+ withOverLightLayers?: boolean;
265
257
 
266
258
  /**
267
259
  * Performance monitoring
268
260
  */
269
- enablePerformanceMonitoring?: boolean;
261
+ debugPerformance?: boolean;
270
262
 
271
263
  /**
272
- * Debug mode for cornerRadius extraction
264
+ * Debug mode for borderRadius extraction
273
265
  */
274
- debugCornerRadius?: boolean;
266
+ debugBorderRadius?: boolean;
275
267
 
276
268
  /**
277
269
  * Debug mode for overLight detection and configuration
@@ -3791,7 +3783,7 @@ export interface VideoPlayerProps extends BaseComponentProps {
3791
3783
  saturation?: number;
3792
3784
  aberrationIntensity?: number;
3793
3785
  elasticity?: number;
3794
- cornerRadius?: number;
3786
+ borderRadius?: number;
3795
3787
  mode?: 'standard' | 'polar' | 'prominent' | 'shader';
3796
3788
  overLight?: boolean;
3797
3789
  };
@@ -4049,6 +4041,11 @@ export interface CardProps extends BaseComponentProps {
4049
4041
  */
4050
4042
  className?: string;
4051
4043
 
4044
+ /**
4045
+ * Optional custom link component (e.g., Next.js Link, React Router Link)
4046
+ */
4047
+ LinkComponent?: React.ElementType;
4048
+
4052
4049
  /**
4053
4050
  * Optional click handler
4054
4051
  */
@@ -6642,7 +6639,7 @@ export interface GlassContainerProps extends BaseComponentProps {
6642
6639
  * Border radius of the glass container
6643
6640
  * @default 999
6644
6641
  */
6645
- cornerRadius?: number;
6642
+ borderRadius?: number;
6646
6643
 
6647
6644
  /**
6648
6645
  * Padding inside the glass container
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { hasClass, addClass, removeClass, toggleClass } from '../dom';
3
+
4
+ describe('dom utils', () => {
5
+ let element: HTMLElement;
6
+
7
+ beforeEach(() => {
8
+ element = document.createElement('div');
9
+ });
10
+
11
+ describe('hasClass', () => {
12
+ it('should return true if element has class', () => {
13
+ element.className = 'test-class';
14
+ expect(hasClass(element, 'test-class')).toBe(true);
15
+ });
16
+
17
+ it('should return false if element does not have class', () => {
18
+ element.className = 'other-class';
19
+ expect(hasClass(element, 'test-class')).toBe(false);
20
+ });
21
+
22
+ it('should return false if element has no classes', () => {
23
+ expect(hasClass(element, 'test-class')).toBe(false);
24
+ });
25
+ });
26
+
27
+ describe('addClass', () => {
28
+ it('should add class if not present', () => {
29
+ addClass(element, 'new-class');
30
+ expect(element.classList.contains('new-class')).toBe(true);
31
+ });
32
+
33
+ it('should not add duplicate class', () => {
34
+ element.className = 'existing-class';
35
+ addClass(element, 'existing-class');
36
+ expect(element.className).toBe('existing-class');
37
+ expect(element.classList.length).toBe(1);
38
+ });
39
+
40
+ it('should preserve existing classes', () => {
41
+ element.className = 'existing-class';
42
+ addClass(element, 'new-class');
43
+ expect(element.classList.contains('existing-class')).toBe(true);
44
+ expect(element.classList.contains('new-class')).toBe(true);
45
+ });
46
+ });
47
+
48
+ describe('removeClass', () => {
49
+ it('should remove existing class', () => {
50
+ element.className = 'test-class';
51
+ removeClass(element, 'test-class');
52
+ expect(element.classList.contains('test-class')).toBe(false);
53
+ });
54
+
55
+ it('should do nothing if class does not exist', () => {
56
+ element.className = 'other-class';
57
+ removeClass(element, 'test-class');
58
+ expect(element.className).toBe('other-class');
59
+ });
60
+
61
+ it('should preserve other classes', () => {
62
+ element.className = 'keep-me remove-me';
63
+ removeClass(element, 'remove-me');
64
+ expect(element.classList.contains('keep-me')).toBe(true);
65
+ expect(element.classList.contains('remove-me')).toBe(false);
66
+ });
67
+ });
68
+
69
+ describe('toggleClass', () => {
70
+ it('should add class if not present', () => {
71
+ toggleClass(element, 'test-class');
72
+ expect(element.classList.contains('test-class')).toBe(true);
73
+ });
74
+
75
+ it('should remove class if present', () => {
76
+ element.className = 'test-class';
77
+ toggleClass(element, 'test-class');
78
+ expect(element.classList.contains('test-class')).toBe(false);
79
+ });
80
+
81
+ it('should add class if force is true', () => {
82
+ toggleClass(element, 'test-class', true);
83
+ expect(element.classList.contains('test-class')).toBe(true);
84
+
85
+ // Should stay added if already present
86
+ toggleClass(element, 'test-class', true);
87
+ expect(element.classList.contains('test-class')).toBe(true);
88
+ });
89
+
90
+ it('should remove class if force is false', () => {
91
+ element.className = 'test-class';
92
+ toggleClass(element, 'test-class', false);
93
+ expect(element.classList.contains('test-class')).toBe(false);
94
+
95
+ // Should stay removed if not present
96
+ toggleClass(element, 'test-class', false);
97
+ expect(element.classList.contains('test-class')).toBe(false);
98
+ });
99
+ });
100
+ });