@shohojdhara/atomix 0.5.1 → 0.5.4

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 (145) hide show
  1. package/atomix.config.ts +45 -33
  2. package/build-tools/webpack-loader.js +5 -4
  3. package/dist/atomix.css +138 -17
  4. package/dist/atomix.css.map +1 -1
  5. package/dist/atomix.min.css +1 -1
  6. package/dist/atomix.min.css.map +1 -1
  7. package/dist/build-tools/webpack-loader.js +5 -4
  8. package/dist/charts.d.ts +23 -23
  9. package/dist/charts.js +40 -37
  10. package/dist/charts.js.map +1 -1
  11. package/dist/config.d.ts +699 -0
  12. package/dist/config.js +17 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/core.d.ts +2 -2
  15. package/dist/core.js +111 -50
  16. package/dist/core.js.map +1 -1
  17. package/dist/forms.d.ts +3 -6
  18. package/dist/forms.js +2 -2
  19. package/dist/forms.js.map +1 -1
  20. package/dist/heavy.d.ts +1 -1
  21. package/dist/heavy.js +173 -111
  22. package/dist/heavy.js.map +1 -1
  23. package/dist/index.d.ts +1881 -790
  24. package/dist/index.esm.js +2713 -816
  25. package/dist/index.esm.js.map +1 -1
  26. package/dist/index.js +2693 -780
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.min.js +1 -1
  29. package/dist/index.min.js.map +1 -1
  30. package/dist/layout.js +59 -60
  31. package/dist/layout.js.map +1 -1
  32. package/dist/theme.d.ts +1390 -276
  33. package/dist/theme.js +2133 -625
  34. package/dist/theme.js.map +1 -1
  35. package/package.json +14 -9
  36. package/scripts/atomix-cli.js +15 -1
  37. package/scripts/cli/__tests__/complexity-utils.test.js +24 -0
  38. package/scripts/cli/__tests__/detector.test.js +50 -0
  39. package/scripts/cli/__tests__/template-engine.test.js +23 -0
  40. package/scripts/cli/__tests__/test-setup.js +3 -0
  41. package/scripts/cli/commands/doctor.js +15 -3
  42. package/scripts/cli/commands/generate.js +113 -51
  43. package/scripts/cli/internal/ai-engine.js +30 -10
  44. package/scripts/cli/internal/complexity-utils.js +60 -0
  45. package/scripts/cli/internal/component-validator.js +49 -16
  46. package/scripts/cli/internal/config-loader.js +30 -20
  47. package/scripts/cli/internal/generator.js +89 -36
  48. package/scripts/cli/internal/hook-generator.js +5 -2
  49. package/scripts/cli/internal/itcss-generator.js +16 -12
  50. package/scripts/cli/templates/next-templates.js +81 -30
  51. package/scripts/cli/templates/storybook-templates.js +12 -2
  52. package/scripts/cli/utils/detector.js +45 -7
  53. package/scripts/cli/utils/diagnostics.js +78 -0
  54. package/scripts/cli/utils/telemetry.js +13 -0
  55. package/src/components/Accordion/Accordion.stories.tsx +4 -0
  56. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +1 -1
  57. package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +219 -0
  58. package/src/components/AtomixGlass/glass-utils.ts +1 -1
  59. package/src/components/Button/Button.tsx +114 -57
  60. package/src/components/Callout/Callout.tsx +4 -4
  61. package/src/components/Chart/ChartRenderer.tsx +1 -1
  62. package/src/components/Chart/DonutChart.tsx +11 -8
  63. package/src/components/EdgePanel/EdgePanel.tsx +119 -115
  64. package/src/components/Form/Select.tsx +4 -4
  65. package/src/components/List/List.tsx +4 -4
  66. package/src/components/Navigation/SideMenu/SideMenu.tsx +6 -6
  67. package/src/components/PhotoViewer/PhotoViewerImage.tsx +1 -1
  68. package/src/components/ProductReview/ProductReview.tsx +4 -2
  69. package/src/components/Rating/Rating.tsx +4 -2
  70. package/src/components/SectionIntro/SectionIntro.tsx +4 -2
  71. package/src/components/Steps/Steps.tsx +1 -1
  72. package/src/components/Tabs/Tabs.tsx +5 -5
  73. package/src/components/Testimonial/Testimonial.tsx +4 -2
  74. package/src/components/VideoPlayer/VideoPlayer.tsx +4 -2
  75. package/src/layouts/CssGrid/CssGrid.stories.tsx +464 -0
  76. package/src/layouts/CssGrid/CssGrid.tsx +215 -0
  77. package/src/layouts/CssGrid/index.ts +8 -0
  78. package/src/layouts/CssGrid/scripts/CssGrid.js +284 -0
  79. package/src/layouts/CssGrid/scripts/index.js +43 -0
  80. package/src/layouts/Grid/scripts/Container.js +139 -0
  81. package/src/layouts/Grid/scripts/Grid.js +184 -0
  82. package/src/layouts/Grid/scripts/GridCol.js +273 -0
  83. package/src/layouts/Grid/scripts/Row.js +154 -0
  84. package/src/layouts/Grid/scripts/index.js +48 -0
  85. package/src/layouts/MasonryGrid/MasonryGrid.tsx +71 -59
  86. package/src/lib/composables/atomix-glass/useGlassSize.ts +1 -1
  87. package/src/lib/composables/useAccordion.ts +5 -5
  88. package/src/lib/composables/useAtomixGlass.ts +3 -3
  89. package/src/lib/composables/useBarChart.ts +2 -2
  90. package/src/lib/composables/useChart.ts +3 -2
  91. package/src/lib/composables/useChartToolbar.ts +48 -66
  92. package/src/lib/composables/useDataTable.ts +1 -1
  93. package/src/lib/composables/useDatePicker.ts +2 -2
  94. package/src/lib/composables/useEdgePanel.ts +45 -54
  95. package/src/lib/composables/useHeroBackgroundSlider.ts +5 -5
  96. package/src/lib/composables/usePhotoViewer.ts +2 -3
  97. package/src/lib/composables/usePieChart.ts +1 -1
  98. package/src/lib/composables/usePopover.ts +151 -139
  99. package/src/lib/composables/useSideMenu.ts +28 -41
  100. package/src/lib/composables/useSlider.ts +2 -6
  101. package/src/lib/composables/useTooltip.ts +2 -2
  102. package/src/lib/config/index.ts +38 -323
  103. package/src/lib/config/loader.ts +419 -0
  104. package/src/lib/config/public-api.ts +43 -0
  105. package/src/lib/config/types.ts +389 -0
  106. package/src/lib/config/validator.ts +305 -0
  107. package/src/lib/theme/adapters/index.ts +1 -1
  108. package/src/lib/theme/adapters/themeAdapter.ts +358 -229
  109. package/src/lib/theme/components/ThemeToggle.tsx +276 -0
  110. package/src/lib/theme/config/configLoader.ts +351 -0
  111. package/src/lib/theme/config/loader.ts +221 -0
  112. package/src/lib/theme/core/createTheme.ts +126 -50
  113. package/src/lib/theme/core/createThemeObject.ts +7 -4
  114. package/src/lib/theme/devtools/Comparator.tsx +1 -1
  115. package/src/lib/theme/devtools/Inspector.tsx +1 -1
  116. package/src/lib/theme/devtools/LiveEditor.tsx +1 -1
  117. package/src/lib/theme/hooks/useThemeSwitcher.ts +164 -0
  118. package/src/lib/theme/index.ts +322 -38
  119. package/src/lib/theme/runtime/ThemeProvider.tsx +45 -11
  120. package/src/lib/theme/runtime/__tests__/ThemeProvider.test.tsx +44 -393
  121. package/src/lib/theme/runtime/useTheme.ts +1 -0
  122. package/src/lib/theme/tokens/tokens.ts +101 -1
  123. package/src/lib/theme/types.ts +91 -0
  124. package/src/lib/theme/utils/performanceMonitor.ts +315 -0
  125. package/src/lib/theme/utils/responsive.ts +280 -0
  126. package/src/lib/theme/utils/themeUtils.ts +531 -117
  127. package/src/styles/01-settings/_index.scss +1 -0
  128. package/src/styles/01-settings/_settings.atomix-glass.scss +174 -0
  129. package/src/styles/01-settings/_settings.masonry-grid.scss +42 -6
  130. package/src/styles/02-tools/_tools.glass.scss +6 -0
  131. package/src/styles/05-objects/_objects.masonry-grid.scss +162 -24
  132. package/src/styles/06-components/_components.atomix-glass.scss +4 -4
  133. package/src/lib/composables/useBreadcrumb.ts +0 -81
  134. package/src/lib/composables/useChartInteractions.ts +0 -123
  135. package/src/lib/composables/useChartPerformance.ts +0 -347
  136. package/src/lib/composables/useDropdown.ts +0 -338
  137. package/src/lib/composables/useModal.ts +0 -110
  138. package/src/lib/hooks/usePerformanceMonitor.ts +0 -148
  139. package/src/lib/utils/displacement-generator.ts +0 -92
  140. package/src/lib/utils/memoryMonitor.ts +0 -191
  141. package/src/styles/01-settings/_settings.testtypecheck.scss +0 -53
  142. package/src/styles/01-settings/_settings.typedbutton.scss +0 -53
  143. package/src/styles/06-components/_components.testbutton.scss +0 -212
  144. package/src/styles/06-components/_components.testtypecheck.scss +0 -212
  145. package/src/styles/06-components/_components.typedbutton.scss +0 -212
@@ -8,19 +8,19 @@ import { EDGE_PANEL } from '../constants/components';
8
8
  * @returns EdgePanel state and methods
9
9
  */
10
10
  export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
11
- // Default EdgePanel properties
12
- const defaultProps: Partial<EdgePanelProps> = {
13
- position: 'start',
14
- mode: 'slide',
15
- isOpen: false,
16
- backdrop: true,
17
- closeOnBackdropClick: true,
18
- closeOnEscape: true,
19
- glass: undefined,
20
- ...initialProps,
21
- };
22
-
23
- const [isOpen, setIsOpen] = useState(defaultProps.isOpen || false);
11
+ const {
12
+ position = 'start',
13
+ mode = 'slide',
14
+ isOpen: propIsOpen = false,
15
+ backdrop = true,
16
+ closeOnBackdropClick = true,
17
+ closeOnEscape = true,
18
+ glass,
19
+ onOpenChange,
20
+ className = '',
21
+ } = initialProps || {};
22
+
23
+ const [isOpen, setIsOpen] = useState(propIsOpen || false);
24
24
  const containerRef = useRef<HTMLDivElement>(null);
25
25
  const backdropRef = useRef<HTMLDivElement>(null);
26
26
 
@@ -30,22 +30,21 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
30
30
  * @returns Class string
31
31
  */
32
32
  const generateEdgePanelClass = (props: Partial<EdgePanelProps>): string => {
33
- const { position = defaultProps.position, className = '', isOpen: propIsOpen } = props;
33
+ const { position: propPosition = position, className: propClassName = className, isOpen: argIsOpen } = props;
34
34
 
35
35
  const baseClass = EDGE_PANEL.CLASSES.BASE;
36
- const positionClass = position ? `${baseClass}--${position}` : '';
37
- const openClass = (propIsOpen ?? isOpen) ? EDGE_PANEL.CLASSES.IS_OPEN : '';
36
+ const positionClass = propPosition ? `${baseClass}--${propPosition}` : '';
37
+ const openClass = (argIsOpen ?? isOpen) ? EDGE_PANEL.CLASSES.IS_OPEN : '';
38
38
 
39
- return `${baseClass} ${positionClass} ${openClass} ${className}`.trim();
39
+ return `${baseClass} ${positionClass} ${openClass} ${propClassName}`.trim();
40
40
  };
41
41
 
42
42
  /**
43
43
  * Adjust body padding in push mode
44
44
  */
45
45
  const adjustBodyPadding = useCallback(() => {
46
- if (!containerRef.current || defaultProps.mode !== 'push') return;
46
+ if (!containerRef.current || mode !== 'push') return;
47
47
 
48
- const { position } = defaultProps;
49
48
  const size =
50
49
  position === 'top' || position === 'bottom'
51
50
  ? containerRef.current.clientHeight
@@ -67,15 +66,13 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
67
66
 
68
67
  document.body.style[paddingProperty as any] = `${size}px`;
69
68
  document.body.classList.add('is-pushed');
70
- }, [defaultProps.mode, defaultProps.position]);
69
+ }, [mode, position]);
71
70
 
72
71
  /**
73
72
  * Reset body padding
74
73
  */
75
74
  const resetBodyPadding = useCallback(() => {
76
- if (defaultProps.mode !== 'push') return;
77
-
78
- const { position } = defaultProps;
75
+ if (mode !== 'push') return;
79
76
 
80
77
  // Map position to CSS padding property
81
78
  let paddingProperty: string;
@@ -93,7 +90,7 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
93
90
 
94
91
  document.body.style[paddingProperty as any] = '';
95
92
  document.body.classList.remove('is-pushed');
96
- }, [defaultProps.mode, defaultProps.position]);
93
+ }, [mode, position]);
97
94
 
98
95
  /**
99
96
  * Open the panel
@@ -104,8 +101,6 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
104
101
  document.body.classList.add('is-edgepanel-open');
105
102
 
106
103
  if (containerRef.current) {
107
- const { mode } = defaultProps;
108
-
109
104
  // Only add animation if not in 'none' mode
110
105
  if (mode !== 'none') {
111
106
  if (useFadeAnimation) {
@@ -148,16 +143,16 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
148
143
  }
149
144
 
150
145
  // If push mode, adjust body padding
151
- if (defaultProps.mode === 'push') {
146
+ if (mode === 'push') {
152
147
  adjustBodyPadding();
153
148
  }
154
149
  }
155
150
 
156
- if (defaultProps.onOpenChange) {
157
- defaultProps.onOpenChange(true);
151
+ if (onOpenChange) {
152
+ onOpenChange(true);
158
153
  }
159
154
  },
160
- [defaultProps, adjustBodyPadding]
155
+ [mode, adjustBodyPadding, onOpenChange]
161
156
  );
162
157
 
163
158
  /**
@@ -166,8 +161,6 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
166
161
  const closePanel = useCallback(
167
162
  (useFadeAnimation = false) => {
168
163
  if (containerRef.current) {
169
- const { position, mode } = defaultProps;
170
-
171
164
  // Only add animation if not in 'none' mode
172
165
  if (mode !== 'none') {
173
166
  if (useFadeAnimation) {
@@ -209,7 +202,7 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
209
202
  }
210
203
 
211
204
  // Reset body padding if push mode
212
- if (defaultProps.mode === 'push') {
205
+ if (mode === 'push') {
213
206
  resetBodyPadding();
214
207
  }
215
208
 
@@ -220,20 +213,20 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
220
213
  setIsOpen(false);
221
214
  document.body.classList.remove('is-edgepanel-open');
222
215
 
223
- if (defaultProps.onOpenChange) {
224
- defaultProps.onOpenChange(false);
216
+ if (onOpenChange) {
217
+ onOpenChange(false);
225
218
  }
226
219
  }, hideDelay);
227
220
  } else {
228
221
  setIsOpen(false);
229
222
  document.body.classList.remove('is-edgepanel-open');
230
223
 
231
- if (defaultProps.onOpenChange) {
232
- defaultProps.onOpenChange(false);
224
+ if (onOpenChange) {
225
+ onOpenChange(false);
233
226
  }
234
227
  }
235
228
  },
236
- [defaultProps, resetBodyPadding]
229
+ [mode, position, onOpenChange, resetBodyPadding]
237
230
  );
238
231
 
239
232
  /**
@@ -241,11 +234,11 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
241
234
  */
242
235
  const handleEscapeKey = useCallback(
243
236
  (event: KeyboardEvent) => {
244
- if (defaultProps.closeOnEscape && event.key === 'Escape' && isOpen) {
237
+ if (closeOnEscape && event.key === 'Escape' && isOpen) {
245
238
  closePanel();
246
239
  }
247
240
  },
248
- [closePanel, defaultProps.closeOnEscape, isOpen]
241
+ [closePanel, closeOnEscape, isOpen]
249
242
  );
250
243
 
251
244
  /**
@@ -253,55 +246,53 @@ export function useEdgePanel(initialProps?: Partial<EdgePanelProps>) {
253
246
  */
254
247
  const handleBackdropClick = useCallback(
255
248
  (event: React.MouseEvent<HTMLDivElement>) => {
256
- if (defaultProps.closeOnBackdropClick && event.target === event.currentTarget) {
249
+ if (closeOnBackdropClick && event.target === event.currentTarget) {
257
250
  closePanel();
258
251
  }
259
252
  },
260
- [closePanel, defaultProps.closeOnBackdropClick]
253
+ [closePanel, closeOnBackdropClick]
261
254
  );
262
255
 
263
256
  /**
264
257
  * Set up event listeners for keyboard events
265
258
  */
266
259
  useEffect(() => {
267
- if (isOpen && defaultProps.closeOnEscape) {
260
+ if (isOpen && closeOnEscape) {
268
261
  document.addEventListener('keydown', handleEscapeKey);
269
262
  }
270
263
 
271
264
  return () => {
272
265
  document.removeEventListener('keydown', handleEscapeKey);
273
266
  };
274
- }, [isOpen, handleEscapeKey, defaultProps.closeOnEscape]);
267
+ }, [isOpen, handleEscapeKey, closeOnEscape]);
275
268
 
276
269
  /**
277
270
  * Set initial transform values
278
271
  */
279
272
  useEffect(() => {
280
273
  if (containerRef.current) {
281
- const { position, mode } = defaultProps;
282
-
283
274
  if (!isOpen && (mode === 'slide' || mode === 'push') && position) {
284
- containerRef.current.style.transform = EDGE_PANEL.TRANSFORM_VALUES[position];
275
+ containerRef.current.style.transform = EDGE_PANEL.TRANSFORM_VALUES[position as keyof typeof EDGE_PANEL.TRANSFORM_VALUES];
285
276
  // Set initial opacity for fade animations
286
- if (defaultProps.glass) {
277
+ if (glass) {
287
278
  containerRef.current.style.opacity = '0';
288
279
  }
289
280
  }
290
281
  }
291
- }, [defaultProps.mode, defaultProps.position, defaultProps.glass, isOpen]);
282
+ }, [mode, position, glass, isOpen]);
292
283
 
293
284
  /**
294
285
  * Sync with prop changes
295
286
  */
296
287
  useEffect(() => {
297
- if (defaultProps.isOpen !== undefined && defaultProps.isOpen !== isOpen) {
298
- if (defaultProps.isOpen) {
299
- openPanel(!!defaultProps.glass);
288
+ if (propIsOpen !== undefined && propIsOpen !== isOpen) {
289
+ if (propIsOpen) {
290
+ openPanel(!!glass);
300
291
  } else {
301
- closePanel(!!defaultProps.glass);
292
+ closePanel(!!glass);
302
293
  }
303
294
  }
304
- }, [defaultProps.isOpen, closePanel, isOpen, openPanel, defaultProps.glass]);
295
+ }, [propIsOpen, closePanel, isOpen, openPanel, glass]);
305
296
 
306
297
  return {
307
298
  isOpen,
@@ -18,12 +18,12 @@ export interface UseHeroBackgroundSliderResult {
18
18
  /**
19
19
  * Array of refs for slide container elements
20
20
  */
21
- slideRefs: React.RefObject<HTMLDivElement>[];
21
+ slideRefs: React.RefObject<HTMLDivElement | null>[];
22
22
 
23
23
  /**
24
24
  * Array of refs for video elements
25
25
  */
26
- videoRefs: React.RefObject<HTMLVideoElement>[];
26
+ videoRefs: React.RefObject<HTMLVideoElement | null>[];
27
27
 
28
28
  /**
29
29
  * Handle slide transition to next index
@@ -55,18 +55,18 @@ export function useHeroBackgroundSlider(
55
55
  const [isTransitioning, setIsTransitioning] = useState(false);
56
56
  const autoplayRef = useRef<NodeJS.Timeout | null>(null);
57
57
  const isPausedRef = useRef(false);
58
- const callbackRef = useRef<() => void>();
58
+ const callbackRef = useRef<() => void | undefined>(undefined);
59
59
 
60
60
  // Create refs for slide containers
61
61
  const slideRefs = useMemo(
62
62
  () => slides.map(() => React.createRef<HTMLDivElement>()),
63
- [slides.length]
63
+ [slides]
64
64
  );
65
65
 
66
66
  // Create refs for video elements
67
67
  const videoRefs = useMemo(
68
68
  () => slides.map(() => React.createRef<HTMLVideoElement>()),
69
- [slides.length]
69
+ [slides]
70
70
  );
71
71
 
72
72
  /**
@@ -305,7 +305,7 @@ export const usePhotoViewer = ({
305
305
  };
306
306
  });
307
307
  },
308
- [isMounted, currentIndex, calculateBounds, constrainPosition]
308
+ [currentIndex, calculateBounds, constrainPosition]
309
309
  );
310
310
 
311
311
  const setImagePosition = useCallback(
@@ -366,7 +366,7 @@ export const usePhotoViewer = ({
366
366
  };
367
367
  });
368
368
  },
369
- [isMounted, currentIndex, calculateBounds, constrainPosition]
369
+ [currentIndex, calculateBounds, constrainPosition]
370
370
  );
371
371
 
372
372
  // Handle mouse wheel for zooming with proper bounds
@@ -866,7 +866,6 @@ export const usePhotoViewer = ({
866
866
  });
867
867
  },
868
868
  [
869
- isMounted,
870
869
  enableGestures,
871
870
  isDragging,
872
871
  startDragPosition,
@@ -294,7 +294,7 @@ export function usePieChart(data: ChartDataPoint[], options: PieChartOptions = {
294
294
 
295
295
  return parts.join(' - ');
296
296
  },
297
- [options.labelFormatter, options.showLabels, options.showPercentages, options.showValues]
297
+ [options]
298
298
  );
299
299
 
300
300
  // Get slice transform for hover effect
@@ -1,4 +1,4 @@
1
- import { useState, useRef, useEffect, RefObject } from 'react';
1
+ import { useState, useRef, useEffect, RefObject, useCallback } from 'react';
2
2
 
3
3
  type PopoverPosition = 'top' | 'bottom' | 'left' | 'right';
4
4
  type PopoverTrigger = 'click' | 'hover';
@@ -57,14 +57,17 @@ export const usePopover = ({
57
57
  const isOpenState = isControlled ? controlledIsOpen : isOpen;
58
58
 
59
59
  // Define setIsOpen function before using it in useEffect
60
- const setIsOpen = (newIsOpen: boolean) => {
61
- if (!isControlled) {
62
- setIsOpenState(newIsOpen);
63
- }
64
- if (onOpenChange) {
65
- onOpenChange(newIsOpen);
66
- }
67
- };
60
+ const setIsOpen = useCallback(
61
+ (newIsOpen: boolean) => {
62
+ if (!isControlled) {
63
+ setIsOpenState(newIsOpen);
64
+ }
65
+ if (onOpenChange) {
66
+ onOpenChange(newIsOpen);
67
+ }
68
+ },
69
+ [isControlled, onOpenChange]
70
+ );
68
71
 
69
72
  // Handle hover events if trigger is hover
70
73
  useEffect(() => {
@@ -115,141 +118,150 @@ export const usePopover = ({
115
118
  popoverRef.current.addEventListener('mouseenter', handlePopoverMouseEnter);
116
119
  popoverRef.current.addEventListener('mouseleave', handlePopoverMouseLeave);
117
120
 
121
+ const currentTrigger = triggerRef.current;
122
+ const currentPopover = popoverRef.current;
123
+
118
124
  return () => {
119
- if (triggerRef.current) {
120
- triggerRef.current.removeEventListener('mouseenter', handleTriggerMouseEnter);
121
- triggerRef.current.removeEventListener('mouseleave', handleTriggerMouseLeave);
125
+ if (currentTrigger) {
126
+ currentTrigger.removeEventListener('mouseenter', handleTriggerMouseEnter);
127
+ currentTrigger.removeEventListener('mouseleave', handleTriggerMouseLeave);
122
128
  }
123
- if (popoverRef.current) {
124
- popoverRef.current.removeEventListener('mouseenter', handlePopoverMouseEnter);
125
- popoverRef.current.removeEventListener('mouseleave', handlePopoverMouseLeave);
129
+ if (currentPopover) {
130
+ currentPopover.removeEventListener('mouseenter', handlePopoverMouseEnter);
131
+ currentPopover.removeEventListener('mouseleave', handlePopoverMouseLeave);
126
132
  }
127
133
  if (timeoutRef.current !== null) {
128
134
  window.clearTimeout(timeoutRef.current);
129
135
  }
130
136
  };
131
- }, [trigger, delay, isOpenState]);
132
-
133
- const updatePosition = (event?: Event) => {
134
- if (!triggerRef.current || !popoverRef.current) return;
135
-
136
- const triggerRect = triggerRef.current.getBoundingClientRect();
137
- const popoverRect = popoverRef.current.getBoundingClientRect();
138
- const viewportWidth = window.innerWidth;
139
- const viewportHeight = window.innerHeight;
140
-
141
- // Check if the trigger is near viewport edges
142
- const isNearViewportEdge =
143
- triggerRect.top < 50 ||
144
- triggerRect.bottom > viewportHeight - 50 ||
145
- triggerRect.left < 50 ||
146
- triggerRect.right > viewportWidth - 50;
147
-
148
- // If this is a scroll update and trigger isn't near edges, skip repositioning
149
- if (event?.type === 'scroll' && !isNearViewportEdge) {
150
- return;
151
- }
152
-
153
- // Calculate space available in each direction
154
- const spaceTop = triggerRect.top;
155
- const spaceBottom = viewportHeight - triggerRect.bottom;
156
- const spaceLeft = triggerRect.left;
157
- const spaceRight = viewportWidth - triggerRect.right;
158
-
159
- // Determine best position based on available space
160
- let bestPosition: PopoverPosition = position === 'auto' ? 'top' : (position as PopoverPosition);
161
-
162
- // If specified position is 'auto', find the position with most space
163
- if (position === 'auto') {
164
- const spaces = [
165
- { position: 'top', space: spaceTop },
166
- { position: 'right', space: spaceRight },
167
- { position: 'bottom', space: spaceBottom },
168
- { position: 'left', space: spaceLeft },
169
- ];
170
-
171
- // Sort by available space (descending)
172
- spaces.sort((a, b) => b.space - a.space);
173
-
174
- // Select position with most space
175
- bestPosition = spaces[0]?.position as PopoverPosition;
176
- } else {
177
- // Check if the preferred position has enough space
178
- const needsFlip =
179
- (position === 'top' &&
180
- spaceTop < popoverRect.height + offset &&
181
- spaceBottom >= popoverRect.height + offset) ||
182
- (position === 'bottom' &&
183
- spaceBottom < popoverRect.height + offset &&
184
- spaceTop >= popoverRect.height + offset) ||
185
- (position === 'left' &&
186
- spaceLeft < popoverRect.width + offset &&
187
- spaceRight >= popoverRect.width + offset) ||
188
- (position === 'right' &&
189
- spaceRight < popoverRect.width + offset &&
190
- spaceLeft >= popoverRect.width + offset);
191
-
192
- if (needsFlip) {
193
- // Flip to the opposite side
194
- const oppositePositions: Record<PopoverPosition | 'auto', PopoverPosition> = {
195
- top: 'bottom',
196
- bottom: 'top',
197
- left: 'right',
198
- right: 'left',
199
- auto: 'bottom',
200
- };
201
- bestPosition = oppositePositions[position as PopoverPosition | 'auto'];
137
+ }, [trigger, delay, isOpenState, setIsOpen]);
138
+
139
+ const updatePosition = useCallback(
140
+ (event?: Event) => {
141
+ if (!triggerRef.current || !popoverRef.current) return;
142
+
143
+ const triggerRect = triggerRef.current.getBoundingClientRect();
144
+ const popoverRect = popoverRef.current.getBoundingClientRect();
145
+ const viewportWidth = window.innerWidth;
146
+ const viewportHeight = window.innerHeight;
147
+
148
+ // Check if the trigger is near viewport edges
149
+ const isNearViewportEdge =
150
+ triggerRect.top < 50 ||
151
+ triggerRect.bottom > viewportHeight - 50 ||
152
+ triggerRect.left < 50 ||
153
+ triggerRect.right > viewportWidth - 50;
154
+
155
+ // If this is a scroll update and trigger isn't near edges, skip repositioning
156
+ if (event?.type === 'scroll' && !isNearViewportEdge) {
157
+ return;
202
158
  }
203
- }
204
-
205
- setCurrentPosition(bestPosition);
206
-
207
- // Calculate position based on the determined best position
208
- let top = 0;
209
- let left = 0;
210
-
211
- // Calculate viewport-relative position
212
- switch (bestPosition) {
213
- case 'top':
214
- top = triggerRect.top - popoverRect.height - offset;
215
- left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2;
216
- break;
217
- case 'bottom':
218
- top = triggerRect.bottom + offset;
219
- left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2;
220
- break;
221
- case 'left':
222
- top = triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2;
223
- left = triggerRect.left - popoverRect.width - offset;
224
- break;
225
- case 'right':
226
- top = triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2;
227
- left = triggerRect.right + offset;
228
- break;
229
- }
230
-
231
- // Constrain to viewport boundaries
232
- if (left < 0) {
233
- left = 5;
234
- } else if (left + popoverRect.width > viewportWidth) {
235
- left = viewportWidth - popoverRect.width - 5;
236
- }
237
-
238
- if (top < 0) {
239
- top = 5;
240
- } else if (top + popoverRect.height > viewportHeight) {
241
- top = viewportHeight - popoverRect.height - 5;
242
- }
243
-
244
- // Add scroll position to convert viewport coordinates to absolute position
245
- const absoluteTop = top + window.scrollY;
246
- const absoluteLeft = left + window.scrollX;
247
-
248
- // Apply position using absolute positioning to follow when scrolling
249
- popoverRef.current.style.position = 'absolute';
250
- popoverRef.current.style.top = `${absoluteTop}px`;
251
- popoverRef.current.style.left = `${absoluteLeft}px`;
252
- };
159
+
160
+ // Calculate space available in each direction
161
+ const spaceTop = triggerRect.top;
162
+ const spaceBottom = viewportHeight - triggerRect.bottom;
163
+ const spaceLeft = triggerRect.left;
164
+ const spaceRight = viewportWidth - triggerRect.right;
165
+
166
+ // Determine best position based on available space
167
+ let bestPosition: PopoverPosition =
168
+ position === 'auto' ? 'top' : (position as PopoverPosition);
169
+
170
+ // If specified position is 'auto', find the position with most space
171
+ if (position === 'auto') {
172
+ const spaces = [
173
+ { position: 'top', space: spaceTop },
174
+ { position: 'right', space: spaceRight },
175
+ { position: 'bottom', space: spaceBottom },
176
+ { position: 'left', space: spaceLeft },
177
+ ];
178
+
179
+ // Sort by available space (descending)
180
+ spaces.sort((a, b) => b.space - a.space);
181
+
182
+ // Select position with most space
183
+ bestPosition = spaces[0]?.position as PopoverPosition;
184
+ } else {
185
+ // Check if the preferred position has enough space
186
+ const needsFlip =
187
+ (position === 'top' &&
188
+ spaceTop < popoverRect.height + offset &&
189
+ spaceBottom >= popoverRect.height + offset) ||
190
+ (position === 'bottom' &&
191
+ spaceBottom < popoverRect.height + offset &&
192
+ spaceTop >= popoverRect.height + offset) ||
193
+ (position === 'left' &&
194
+ spaceLeft < popoverRect.width + offset &&
195
+ spaceRight >= popoverRect.width + offset) ||
196
+ (position === 'right' &&
197
+ spaceRight < popoverRect.width + offset &&
198
+ spaceLeft >= popoverRect.width + offset);
199
+
200
+ if (needsFlip) {
201
+ // Flip to the opposite side
202
+ const oppositePositions: Record<PopoverPosition | 'auto', PopoverPosition> = {
203
+ top: 'bottom',
204
+ bottom: 'top',
205
+ left: 'right',
206
+ right: 'left',
207
+ auto: 'bottom',
208
+ };
209
+ bestPosition = oppositePositions[position as PopoverPosition | 'auto'];
210
+ }
211
+ }
212
+
213
+ setCurrentPosition(bestPosition);
214
+
215
+ // Calculate position based on the determined best position
216
+ let top = 0;
217
+ let left = 0;
218
+
219
+ // Calculate viewport-relative position
220
+ switch (bestPosition) {
221
+ case 'top':
222
+ top = triggerRect.top - popoverRect.height - offset;
223
+ left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2;
224
+ break;
225
+ case 'bottom':
226
+ top = triggerRect.bottom + offset;
227
+ left = triggerRect.left + triggerRect.width / 2 - popoverRect.width / 2;
228
+ break;
229
+ case 'left':
230
+ top = triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2;
231
+ left = triggerRect.left - popoverRect.width - offset;
232
+ break;
233
+ case 'right':
234
+ top = triggerRect.top + triggerRect.height / 2 - popoverRect.height / 2;
235
+ left = triggerRect.right + offset;
236
+ break;
237
+ }
238
+
239
+ // Constrain to viewport boundaries
240
+ if (left < 0) {
241
+ left = 5;
242
+ } else if (left + popoverRect.width > viewportWidth) {
243
+ left = viewportWidth - popoverRect.width - 5;
244
+ }
245
+
246
+ if (top < 0) {
247
+ top = 5;
248
+ } else if (top + popoverRect.height > viewportHeight) {
249
+ top = viewportHeight - popoverRect.height - 5;
250
+ }
251
+
252
+ // Add scroll position to convert viewport coordinates to absolute position
253
+ const absoluteTop = top + window.scrollY;
254
+ const absoluteLeft = left + window.scrollX;
255
+
256
+ // Apply position using absolute positioning to follow when scrolling
257
+ if (popoverRef.current) {
258
+ popoverRef.current.style.position = 'absolute';
259
+ popoverRef.current.style.top = `${absoluteTop}px`;
260
+ popoverRef.current.style.left = `${absoluteLeft}px`;
261
+ }
262
+ },
263
+ [position, offset]
264
+ );
253
265
 
254
266
  // Position the popover
255
267
  useEffect(() => {
@@ -289,7 +301,7 @@ export const usePopover = ({
289
301
  }
290
302
  clearInterval(intervalId);
291
303
  };
292
- }, [isOpenState, position, offset]);
304
+ }, [isOpenState, updatePosition]);
293
305
 
294
306
  // Handle click outside to close popover
295
307
  useEffect(() => {
@@ -311,7 +323,7 @@ export const usePopover = ({
311
323
  return () => {
312
324
  document.removeEventListener('mousedown', handleClickOutside);
313
325
  };
314
- }, [isOpenState, closeOnClickOutside]);
326
+ }, [isOpenState, closeOnClickOutside, setIsOpen]);
315
327
 
316
328
  // Handle escape key to close popover
317
329
  useEffect(() => {
@@ -328,7 +340,7 @@ export const usePopover = ({
328
340
  return () => {
329
341
  document.removeEventListener('keydown', handleEscapeKey);
330
342
  };
331
- }, [isOpenState, closeOnEscape]);
343
+ }, [isOpenState, closeOnEscape, setIsOpen]);
332
344
 
333
345
  // Clean up on unmount
334
346
  useEffect(() => {