@shohojdhara/atomix 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/atomix.config.ts +12 -0
  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 +624 -0
  12. package/dist/config.js +59 -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 +98 -65
  24. package/dist/index.esm.js +427 -422
  25. package/dist/index.esm.js.map +1 -1
  26. package/dist/index.js +394 -391
  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.js +4 -4
  33. package/dist/theme.js.map +1 -1
  34. package/package.json +14 -9
  35. package/scripts/atomix-cli.js +15 -1
  36. package/scripts/cli/__tests__/complexity-utils.test.js +24 -0
  37. package/scripts/cli/__tests__/detector.test.js +50 -0
  38. package/scripts/cli/__tests__/template-engine.test.js +23 -0
  39. package/scripts/cli/__tests__/test-setup.js +3 -0
  40. package/scripts/cli/commands/doctor.js +15 -3
  41. package/scripts/cli/commands/generate.js +113 -51
  42. package/scripts/cli/internal/ai-engine.js +30 -10
  43. package/scripts/cli/internal/complexity-utils.js +60 -0
  44. package/scripts/cli/internal/component-validator.js +49 -16
  45. package/scripts/cli/internal/generator.js +89 -36
  46. package/scripts/cli/internal/hook-generator.js +5 -2
  47. package/scripts/cli/internal/itcss-generator.js +16 -12
  48. package/scripts/cli/templates/next-templates.js +81 -30
  49. package/scripts/cli/templates/storybook-templates.js +12 -2
  50. package/scripts/cli/utils/detector.js +45 -7
  51. package/scripts/cli/utils/diagnostics.js +78 -0
  52. package/scripts/cli/utils/telemetry.js +13 -0
  53. package/src/components/Accordion/Accordion.stories.tsx +4 -0
  54. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +1 -1
  55. package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +219 -0
  56. package/src/components/AtomixGlass/glass-utils.ts +1 -1
  57. package/src/components/Button/Button.tsx +114 -57
  58. package/src/components/Callout/Callout.tsx +4 -4
  59. package/src/components/Chart/ChartRenderer.tsx +1 -1
  60. package/src/components/Chart/DonutChart.tsx +11 -8
  61. package/src/components/EdgePanel/EdgePanel.tsx +119 -115
  62. package/src/components/Form/Select.tsx +4 -4
  63. package/src/components/List/List.tsx +4 -4
  64. package/src/components/Navigation/SideMenu/SideMenu.tsx +6 -6
  65. package/src/components/PhotoViewer/PhotoViewerImage.tsx +1 -1
  66. package/src/components/ProductReview/ProductReview.tsx +4 -2
  67. package/src/components/Rating/Rating.tsx +4 -2
  68. package/src/components/SectionIntro/SectionIntro.tsx +4 -2
  69. package/src/components/Steps/Steps.tsx +1 -1
  70. package/src/components/Tabs/Tabs.tsx +5 -5
  71. package/src/components/Testimonial/Testimonial.tsx +4 -2
  72. package/src/components/VideoPlayer/VideoPlayer.tsx +4 -2
  73. package/src/layouts/CssGrid/CssGrid.stories.tsx +464 -0
  74. package/src/layouts/CssGrid/CssGrid.tsx +215 -0
  75. package/src/layouts/CssGrid/index.ts +8 -0
  76. package/src/layouts/CssGrid/scripts/CssGrid.js +284 -0
  77. package/src/layouts/CssGrid/scripts/index.js +43 -0
  78. package/src/layouts/Grid/scripts/Container.js +139 -0
  79. package/src/layouts/Grid/scripts/Grid.js +184 -0
  80. package/src/layouts/Grid/scripts/GridCol.js +273 -0
  81. package/src/layouts/Grid/scripts/Row.js +154 -0
  82. package/src/layouts/Grid/scripts/index.js +48 -0
  83. package/src/layouts/MasonryGrid/MasonryGrid.tsx +71 -59
  84. package/src/lib/composables/atomix-glass/useGlassSize.ts +1 -1
  85. package/src/lib/composables/useAccordion.ts +5 -5
  86. package/src/lib/composables/useAtomixGlass.ts +3 -3
  87. package/src/lib/composables/useBarChart.ts +2 -2
  88. package/src/lib/composables/useChart.ts +3 -2
  89. package/src/lib/composables/useChartToolbar.ts +48 -66
  90. package/src/lib/composables/useDataTable.ts +1 -1
  91. package/src/lib/composables/useDatePicker.ts +2 -2
  92. package/src/lib/composables/useEdgePanel.ts +45 -54
  93. package/src/lib/composables/useHeroBackgroundSlider.ts +5 -5
  94. package/src/lib/composables/usePhotoViewer.ts +2 -3
  95. package/src/lib/composables/usePieChart.ts +1 -1
  96. package/src/lib/composables/usePopover.ts +151 -139
  97. package/src/lib/composables/useSideMenu.ts +28 -41
  98. package/src/lib/composables/useSlider.ts +2 -6
  99. package/src/lib/composables/useTooltip.ts +2 -2
  100. package/src/lib/config/index.ts +39 -0
  101. package/src/lib/theme/devtools/Comparator.tsx +1 -1
  102. package/src/lib/theme/devtools/Inspector.tsx +1 -1
  103. package/src/lib/theme/devtools/LiveEditor.tsx +1 -1
  104. package/src/lib/theme/runtime/ThemeProvider.tsx +1 -1
  105. package/src/styles/01-settings/_index.scss +1 -0
  106. package/src/styles/01-settings/_settings.atomix-glass.scss +174 -0
  107. package/src/styles/01-settings/_settings.masonry-grid.scss +42 -6
  108. package/src/styles/02-tools/_tools.glass.scss +6 -0
  109. package/src/styles/05-objects/_objects.masonry-grid.scss +162 -24
  110. package/src/styles/06-components/_components.atomix-glass.scss +4 -4
  111. package/src/lib/composables/useBreadcrumb.ts +0 -81
  112. package/src/lib/composables/useChartInteractions.ts +0 -123
  113. package/src/lib/composables/useChartPerformance.ts +0 -347
  114. package/src/lib/composables/useDropdown.ts +0 -338
  115. package/src/lib/composables/useModal.ts +0 -110
  116. package/src/lib/hooks/usePerformanceMonitor.ts +0 -148
  117. package/src/lib/utils/displacement-generator.ts +0 -92
  118. package/src/lib/utils/memoryMonitor.ts +0 -191
  119. package/src/styles/01-settings/_settings.testtypecheck.scss +0 -53
  120. package/src/styles/01-settings/_settings.typedbutton.scss +0 -53
  121. package/src/styles/06-components/_components.testbutton.scss +0 -212
  122. package/src/styles/06-components/_components.testtypecheck.scss +0 -212
  123. package/src/styles/06-components/_components.typedbutton.scss +0 -212
@@ -1,347 +0,0 @@
1
- import { useCallback, useMemo, useRef, useState } from 'react';
2
- import { ChartDataPoint, ChartDataset } from '../types/components';
3
-
4
- /**
5
- * Performance optimization hook for charts
6
- * Provides memoization, virtualization, and efficient re-rendering
7
- */
8
- export function useChartPerformance() {
9
- const renderCountRef = useRef(0);
10
- const lastRenderTimeRef = useRef(Date.now());
11
- const [performanceMetrics, setPerformanceMetrics] = useState({
12
- renderCount: 0,
13
- averageRenderTime: 0,
14
- lastRenderDuration: 0,
15
- });
16
-
17
- /**
18
- * Memoized chart data processing
19
- */
20
- const processDatasetsOptimized = useCallback((datasets: ChartDataset[]) => {
21
- const startTime = performance.now();
22
-
23
- // Process datasets with optimizations
24
- const processedDatasets = datasets.map(dataset => ({
25
- ...dataset,
26
- data: dataset.data.map((point, index) => ({
27
- ...point,
28
- // Add index for efficient lookups
29
- _index: index,
30
- // Pre-calculate commonly used values
31
- _stringValue: point.value.toString(),
32
- _hasMetadata: Boolean(point.metadata && Object.keys(point.metadata).length > 0),
33
- })),
34
- // Pre-calculate dataset statistics
35
- _stats: {
36
- min: Math.min(...dataset.data.map(p => p.value)),
37
- max: Math.max(...dataset.data.map(p => p.value)),
38
- average: dataset.data.reduce((sum, p) => sum + p.value, 0) / dataset.data.length,
39
- count: dataset.data.length,
40
- },
41
- }));
42
-
43
- const endTime = performance.now();
44
- const processingTime = endTime - startTime;
45
-
46
- // Update performance metrics
47
- renderCountRef.current += 1;
48
- const currentTime = Date.now();
49
- const timeSinceLastRender = currentTime - lastRenderTimeRef.current;
50
- lastRenderTimeRef.current = currentTime;
51
-
52
- setPerformanceMetrics(prev => ({
53
- renderCount: renderCountRef.current,
54
- averageRenderTime:
55
- (prev.averageRenderTime * (renderCountRef.current - 1) + processingTime) /
56
- renderCountRef.current,
57
- lastRenderDuration: processingTime,
58
- }));
59
-
60
- return processedDatasets;
61
- }, []);
62
-
63
- /**
64
- * Optimized scale calculations with caching
65
- */
66
- const createOptimizedScales = useCallback(
67
- (
68
- datasets: ChartDataset[],
69
- width: number,
70
- height: number,
71
- padding: { top: number; right: number; bottom: number; left: number }
72
- ) => {
73
- if (!datasets.length) return null;
74
-
75
- const innerWidth = width - padding.left - padding.right;
76
- const innerHeight = height - padding.top - padding.bottom;
77
-
78
- // Calculate bounds efficiently
79
- const allValues = datasets.flatMap(dataset => dataset.data.map(d => d.value));
80
- const minValue = Math.min(...allValues);
81
- const maxValue = Math.max(...allValues);
82
- const valueRange = maxValue - minValue;
83
-
84
- // Pre-calculate scale functions for better performance
85
- const xScale = (i: number, dataLength: number) =>
86
- padding.left + (i / (dataLength - 1)) * innerWidth;
87
-
88
- const yScale = (value: number) =>
89
- padding.top + innerHeight - ((value - minValue) / valueRange) * innerHeight;
90
-
91
- return {
92
- xScale,
93
- yScale,
94
- minValue,
95
- maxValue,
96
- valueRange,
97
- innerWidth,
98
- innerHeight,
99
- };
100
- },
101
- []
102
- );
103
-
104
- /**
105
- * Virtualization for large datasets
106
- */
107
- const useDataVirtualization = useCallback(
108
- (
109
- data: ChartDataPoint[],
110
- viewportStart: number,
111
- viewportEnd: number,
112
- bufferSize: number = 50
113
- ) => {
114
- if (data.length <= 1000) {
115
- // No virtualization needed for small datasets
116
- return {
117
- visibleData: data,
118
- startIndex: 0,
119
- endIndex: data.length - 1,
120
- isVirtualized: false,
121
- };
122
- }
123
-
124
- const start = Math.max(0, viewportStart - bufferSize);
125
- const end = Math.min(data.length - 1, viewportEnd + bufferSize);
126
-
127
- return {
128
- visibleData: data.slice(start, end + 1),
129
- startIndex: start,
130
- endIndex: end,
131
- isVirtualized: true,
132
- totalLength: data.length,
133
- };
134
- },
135
- []
136
- );
137
-
138
- /**
139
- * Debounced data updates for real-time charts
140
- * Returns a debounced function that maintains timeout state across calls
141
- * Uses a closure to maintain state - each call to useDebouncedUpdates creates
142
- * a new debounced function with its own persistent timeout state
143
- */
144
- const useDebouncedUpdates = useCallback((updateFunction: () => void, delay: number = 100) => {
145
- // Use a closure variable to maintain timeout state across multiple calls to the returned function
146
- // This variable is created once when useDebouncedUpdates is called and persists
147
- // across all invocations of the returned debounced function
148
- let timeoutId: NodeJS.Timeout | null = null;
149
-
150
- const debouncedFn: (() => void) & { cancel: () => void } = () => {
151
- // Clear any existing timeout before setting a new one
152
- if (timeoutId !== null) {
153
- clearTimeout(timeoutId);
154
- timeoutId = null;
155
- }
156
-
157
- // Set new timeout and store the ID
158
- timeoutId = setTimeout(() => {
159
- updateFunction();
160
- timeoutId = null;
161
- }, delay);
162
- };
163
-
164
- // Add cleanup method to cancel pending debounced calls
165
- debouncedFn.cancel = () => {
166
- if (timeoutId !== null) {
167
- clearTimeout(timeoutId);
168
- timeoutId = null;
169
- }
170
- };
171
-
172
- return debouncedFn;
173
- }, []);
174
-
175
- /**
176
- * Memory-efficient color palette generation using CSS custom properties
177
- */
178
- const generateOptimizedColorPalette = useMemo(() => {
179
- const baseColors = [
180
- 'var(--atomix-primary)',
181
- 'var(--atomix-secondary)',
182
- 'var(--atomix-success)',
183
- 'var(--atomix-info)',
184
- 'var(--atomix-warning)',
185
- 'var(--atomix-error)',
186
- 'var(--atomix-primary-5)',
187
- 'var(--atomix-primary-7)',
188
- 'var(--atomix-primary-3)',
189
- 'var(--atomix-gray-6)',
190
- 'var(--atomix-gray-8)',
191
- 'var(--atomix-gray-4)',
192
- 'var(--atomix-primary-2)',
193
- 'var(--atomix-primary-8)',
194
- 'var(--atomix-gray-5)',
195
- 'var(--atomix-gray-7)',
196
- 'var(--atomix-primary-4)',
197
- 'var(--atomix-primary-6)',
198
- ];
199
-
200
- // Pre-generate a large palette to avoid runtime calculations
201
- const extendedColors: string[] = [];
202
- for (let i = 0; i < 100; i++) {
203
- extendedColors.push(baseColors[i % baseColors.length] || '#000000');
204
- }
205
-
206
- return (index: number) => extendedColors[index % extendedColors.length];
207
- }, []);
208
-
209
- /**
210
- * Optimized animation frame handling
211
- * Returns animation control functions that maintain state across calls
212
- * Uses closures to maintain state - each call to useAnimationFrame creates
213
- * a new animation controller with its own persistent state
214
- */
215
- const useAnimationFrame = useCallback((callback: () => void) => {
216
- // Use closure variables to maintain animation state across multiple calls
217
- // These variables are created once when useAnimationFrame is called and persist
218
- // across all invocations of the returned animation control functions
219
- let requestId: number | null = null;
220
- let previousTime: number | null = null;
221
-
222
- const animate = (time: number) => {
223
- if (previousTime !== null && previousTime !== undefined) {
224
- const deltaTime = time - previousTime;
225
- callback();
226
- }
227
- previousTime = time;
228
- requestId = requestAnimationFrame(animate);
229
- };
230
-
231
- const startAnimation = () => {
232
- // Only start if not already running
233
- if (requestId === null) {
234
- requestId = requestAnimationFrame(animate);
235
- }
236
- };
237
-
238
- const stopAnimation = () => {
239
- if (requestId !== null) {
240
- cancelAnimationFrame(requestId);
241
- requestId = null;
242
- }
243
- previousTime = null;
244
- };
245
-
246
- return { startAnimation, stopAnimation };
247
- }, []);
248
-
249
- /**
250
- * Smart re-rendering based on data changes
251
- */
252
- const shouldUpdateChart = useCallback(
253
- (prevDatasets: ChartDataset[], newDatasets: ChartDataset[], threshold: number = 0.01) => {
254
- if (prevDatasets.length !== newDatasets.length) return true;
255
-
256
- // Check if any significant data changes occurred
257
- for (let i = 0; i < prevDatasets.length; i++) {
258
- const prevDataset = prevDatasets[i];
259
- const newDataset = newDatasets[i];
260
- if (!prevDataset || !newDataset) continue;
261
-
262
- const prevData = prevDataset.data;
263
- const newData = newDataset.data;
264
-
265
- if (prevData.length !== newData.length) return true;
266
-
267
- // Check for significant value changes
268
- for (let j = 0; j < prevData.length; j++) {
269
- const prevPoint = prevData[j];
270
- const newPoint = newData[j];
271
- if (!prevPoint || !newPoint) continue;
272
-
273
- const valueDiff = Math.abs(prevPoint.value - newPoint.value);
274
- const relativeChange = valueDiff / Math.abs(prevPoint.value);
275
-
276
- if (relativeChange > threshold) return true;
277
- }
278
- }
279
-
280
- return false;
281
- },
282
- []
283
- );
284
-
285
- /**
286
- * Canvas optimization for better performance
287
- */
288
- const optimizeCanvasRendering = useCallback(
289
- (
290
- canvasRef: React.RefObject<HTMLCanvasElement>,
291
- pixelRatio: number = window.devicePixelRatio || 1
292
- ) => {
293
- if (!canvasRef.current) return null;
294
-
295
- const canvas = canvasRef.current;
296
- const ctx = canvas.getContext('2d');
297
-
298
- if (!ctx) return null;
299
-
300
- // Set up high-DPI rendering
301
- const rect = canvas.getBoundingClientRect();
302
- canvas.width = rect.width * pixelRatio;
303
- canvas.height = rect.height * pixelRatio;
304
- canvas.style.width = rect.width + 'px';
305
- canvas.style.height = rect.height + 'px';
306
- ctx.scale(pixelRatio, pixelRatio);
307
-
308
- // Optimize canvas settings for better performance
309
- ctx.imageSmoothingEnabled = false;
310
- ctx.fillStyle = 'transparent';
311
- ctx.clearRect(0, 0, canvas.width, canvas.height);
312
-
313
- return { canvas, ctx, width: rect.width, height: rect.height };
314
- },
315
- []
316
- );
317
-
318
- /**
319
- * Batch DOM updates for better performance
320
- */
321
- const batchDOMUpdates = useCallback((updates: (() => void)[]) => {
322
- // Use requestAnimationFrame to batch DOM updates
323
- requestAnimationFrame(() => {
324
- updates.forEach(update => update());
325
- });
326
- }, []);
327
-
328
- return {
329
- // Data processing
330
- processDatasetsOptimized,
331
- createOptimizedScales,
332
- useDataVirtualization,
333
-
334
- // Performance monitoring
335
- performanceMetrics,
336
- shouldUpdateChart,
337
-
338
- // Rendering optimizations
339
- generateOptimizedColorPalette,
340
- optimizeCanvasRendering,
341
- batchDOMUpdates,
342
-
343
- // Animation and updates
344
- useAnimationFrame,
345
- useDebouncedUpdates,
346
- };
347
- }
@@ -1,338 +0,0 @@
1
- import { useState, useRef, useEffect, useCallback } from 'react';
2
- import { DROPDOWN } from '../constants/components';
3
- import type { DropdownPlacement, DropdownTrigger } from '../types/components';
4
-
5
- interface UseDropdownProps {
6
- placement?: DropdownPlacement;
7
- trigger?: DropdownTrigger;
8
- offset?: number;
9
- defaultOpen?: boolean;
10
- isOpen?: boolean;
11
- onOpenChange?: (isOpen: boolean) => void;
12
- closeOnClickOutside?: boolean;
13
- closeOnEscape?: boolean;
14
- id?: string;
15
- }
16
-
17
- interface UseDropdownReturn {
18
- isOpen: boolean;
19
- setIsOpen: (isOpen: boolean) => void;
20
- triggerRef: React.RefObject<HTMLElement | null>;
21
- menuRef: React.RefObject<HTMLElement | null>;
22
- dropdownId: string;
23
- currentPlacement: DropdownPlacement;
24
- updatePosition: () => void;
25
- }
26
-
27
- /**
28
- * Hook for managing dropdown state and position
29
- */
30
- export const useDropdown = ({
31
- placement = DROPDOWN.DEFAULTS.PLACEMENT as DropdownPlacement,
32
- trigger = DROPDOWN.DEFAULTS.TRIGGER as DropdownTrigger,
33
- offset = DROPDOWN.DEFAULTS.OFFSET,
34
- defaultOpen = false,
35
- isOpen: controlledIsOpen,
36
- onOpenChange,
37
- closeOnClickOutside = true,
38
- closeOnEscape = true,
39
- id,
40
- }: UseDropdownProps): UseDropdownReturn => {
41
- // Generate unique ID for the dropdown menu
42
- const uniqueId = useRef(`dropdown-${id || Math.random().toString(36).substring(2, 9)}`);
43
-
44
- // Setup controlled vs uncontrolled state
45
- const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState(defaultOpen);
46
-
47
- // Use either controlled or uncontrolled state
48
- const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : uncontrolledIsOpen;
49
-
50
- // Callback to update open state with notification to parent
51
- const setIsOpen = useCallback(
52
- (nextIsOpen: boolean) => {
53
- if (controlledIsOpen === undefined) {
54
- setUncontrolledIsOpen(nextIsOpen);
55
- }
56
-
57
- if (onOpenChange) {
58
- onOpenChange(nextIsOpen);
59
- }
60
- },
61
- [controlledIsOpen, onOpenChange]
62
- );
63
-
64
- // Refs for trigger and dropdown menu elements
65
- const triggerRef = useRef<HTMLElement>(null);
66
- const menuRef = useRef<HTMLElement>(null);
67
-
68
- // Current placement state
69
- const [currentPlacement, setCurrentPlacement] = useState(placement);
70
-
71
- // Handle click outside
72
- useEffect(() => {
73
- if (!isOpen || !closeOnClickOutside) return undefined;
74
-
75
- const handleClickOutside = (event: MouseEvent) => {
76
- if (
77
- menuRef.current &&
78
- triggerRef.current &&
79
- !menuRef.current.contains(event.target as Node) &&
80
- !triggerRef.current.contains(event.target as Node)
81
- ) {
82
- setIsOpen(false);
83
- }
84
- };
85
-
86
- document.addEventListener('mousedown', handleClickOutside);
87
-
88
- return () => {
89
- document.removeEventListener('mousedown', handleClickOutside);
90
- };
91
- }, [isOpen, closeOnClickOutside, setIsOpen]);
92
-
93
- // Handle escape key
94
- useEffect(() => {
95
- if (!isOpen || !closeOnEscape) return undefined;
96
-
97
- const handleEscapeKey = (event: KeyboardEvent) => {
98
- if (event.key === 'Escape') {
99
- setIsOpen(false);
100
- }
101
- };
102
-
103
- document.addEventListener('keydown', handleEscapeKey);
104
-
105
- return () => {
106
- document.removeEventListener('keydown', handleEscapeKey);
107
- };
108
- }, [isOpen, closeOnEscape, setIsOpen]);
109
-
110
- // Handle arrow key navigation
111
- useEffect(() => {
112
- if (!isOpen || !menuRef.current) return undefined;
113
-
114
- const handleKeyDown = (event: KeyboardEvent) => {
115
- const menu = menuRef.current;
116
- if (!menu) return;
117
-
118
- const items = Array.from(
119
- menu.querySelectorAll<HTMLElement>('[role="menuitem"]:not([disabled])')
120
- );
121
- if (items.length === 0) return;
122
-
123
- const currentIndex = items.indexOf(document.activeElement as HTMLElement);
124
-
125
- switch (event.key) {
126
- case 'ArrowDown':
127
- event.preventDefault();
128
- const nextIndex = (currentIndex + 1) % items.length;
129
- const nextItem = items[nextIndex];
130
- if (nextItem) nextItem.focus();
131
- break;
132
- case 'ArrowUp':
133
- event.preventDefault();
134
- const prevIndex = (currentIndex - 1 + items.length) % items.length;
135
- const prevItem = items[prevIndex];
136
- if (prevItem) prevItem.focus();
137
- break;
138
- case 'Home':
139
- event.preventDefault();
140
- const firstItem = items[0];
141
- if (firstItem) firstItem.focus();
142
- break;
143
- case 'End':
144
- event.preventDefault();
145
- const lastItem = items[items.length - 1];
146
- if (lastItem) lastItem.focus();
147
- break;
148
- case 'Tab':
149
- // Close dropdown on tab
150
- setIsOpen(false);
151
- break;
152
- default:
153
- break;
154
- }
155
- };
156
-
157
- document.addEventListener('keydown', handleKeyDown);
158
-
159
- return () => {
160
- document.removeEventListener('keydown', handleKeyDown);
161
- };
162
- }, [isOpen, setIsOpen]);
163
-
164
- // Focus management when dropdown opens
165
- useEffect(() => {
166
- if (isOpen && menuRef.current) {
167
- const firstItem = menuRef.current.querySelector<HTMLElement>(
168
- '[role="menuitem"]:not([disabled])'
169
- );
170
- if (firstItem) {
171
- setTimeout(() => firstItem.focus(), 0);
172
- }
173
- }
174
- }, [isOpen]);
175
-
176
- // Helper function to get the flipped placement if needed
177
- const getFlippedPlacement = useCallback(
178
- (
179
- placement: DropdownPlacement,
180
- triggerRect: DOMRect,
181
- menuRect: DOMRect,
182
- offset: number
183
- ): DropdownPlacement => {
184
- const viewportWidth = window.innerWidth;
185
- const viewportHeight = window.innerHeight;
186
-
187
- // Start with the requested placement
188
- let newPlacement = placement;
189
-
190
- // Flip vertical placement if needed
191
- if (
192
- placement.startsWith('bottom') &&
193
- triggerRect.bottom + menuRect.height + offset > viewportHeight
194
- ) {
195
- newPlacement = placement.replace('bottom', 'top') as DropdownPlacement;
196
- } else if (placement.startsWith('top') && triggerRect.top - menuRect.height - offset < 0) {
197
- newPlacement = placement.replace('top', 'bottom') as DropdownPlacement;
198
- }
199
-
200
- // Flip horizontal placement if needed
201
- if (placement.startsWith('left') && triggerRect.left - menuRect.width - offset < 0) {
202
- newPlacement = placement.replace('left', 'right') as DropdownPlacement;
203
- } else if (
204
- placement.startsWith('right') &&
205
- triggerRect.right + menuRect.width + offset > viewportWidth
206
- ) {
207
- newPlacement = placement.replace('right', 'left') as DropdownPlacement;
208
- }
209
-
210
- // Adjust alignment for top/bottom placements
211
- if (newPlacement.startsWith('top') || newPlacement.startsWith('bottom')) {
212
- if (newPlacement.endsWith('start') && triggerRect.left + menuRect.width > viewportWidth) {
213
- newPlacement = newPlacement.replace('start', 'end') as DropdownPlacement;
214
- } else if (newPlacement.endsWith('end') && triggerRect.right - menuRect.width < 0) {
215
- newPlacement = newPlacement.replace('end', 'start') as DropdownPlacement;
216
- }
217
- }
218
-
219
- return newPlacement;
220
- },
221
- []
222
- );
223
-
224
- // Helper function to calculate position based on placement
225
- const calculatePosition = useCallback(
226
- (
227
- placement: DropdownPlacement,
228
- triggerRect: DOMRect,
229
- menuRect: DOMRect,
230
- offset: number
231
- ): { top: number; left: number } => {
232
- let top = 0;
233
- let left = 0;
234
-
235
- // Vertical positioning
236
- if (placement.startsWith('bottom')) {
237
- top = triggerRect.height + offset;
238
- } else if (placement.startsWith('top')) {
239
- top = -menuRect.height - offset;
240
- } else if (placement.startsWith('left') || placement.startsWith('right')) {
241
- top = triggerRect.height / 2 - menuRect.height / 2;
242
- }
243
-
244
- // Horizontal positioning
245
- if (placement.startsWith('left')) {
246
- left = -menuRect.width - offset;
247
- } else if (placement.startsWith('right')) {
248
- left = triggerRect.width + offset;
249
- } else if (placement.endsWith('start')) {
250
- left = 0;
251
- } else if (placement.endsWith('end')) {
252
- left = triggerRect.width - menuRect.width;
253
- } else {
254
- left = triggerRect.width / 2 - menuRect.width / 2;
255
- }
256
-
257
- return { top, left };
258
- },
259
- []
260
- );
261
-
262
- // Calculate and update dropdown position
263
- const updatePosition = useCallback(() => {
264
- if (!isOpen || !triggerRef.current || !menuRef.current) return;
265
-
266
- const triggerRect = triggerRef.current.getBoundingClientRect();
267
- const menuRect = menuRef.current.getBoundingClientRect();
268
-
269
- // Get the optimal placement
270
- const newPlacement = getFlippedPlacement(placement, triggerRect, menuRect, offset);
271
-
272
- // Calculate position based on the new placement
273
- const { top, left } = calculatePosition(newPlacement, triggerRect, menuRect, offset);
274
-
275
- // Apply position
276
- menuRef.current.style.position = 'absolute';
277
- menuRef.current.style.top = `${top}px`;
278
- menuRef.current.style.left = `${left}px`;
279
-
280
- // Update placement state if it changed
281
- if (newPlacement !== currentPlacement) {
282
- setCurrentPlacement(newPlacement);
283
- }
284
- }, [isOpen, offset, placement, currentPlacement, getFlippedPlacement, calculatePosition]);
285
-
286
- // Update position when menu is opened
287
- useEffect(() => {
288
- if (!isOpen) return undefined;
289
-
290
- // Initial position update
291
- updatePosition();
292
-
293
- // Use ResizeObserver to detect size changes in the menu
294
- let resizeObserver: ResizeObserver | null = null;
295
- if (menuRef.current && typeof ResizeObserver !== 'undefined') {
296
- resizeObserver = new ResizeObserver(() => {
297
- requestAnimationFrame(updatePosition);
298
- });
299
- resizeObserver.observe(menuRef.current);
300
- }
301
-
302
- // Update position on resize/scroll
303
- const handleResize = () => {
304
- requestAnimationFrame(updatePosition);
305
- };
306
-
307
- const handleScroll = () => {
308
- requestAnimationFrame(updatePosition);
309
- };
310
-
311
- window.addEventListener('resize', handleResize);
312
- window.addEventListener('scroll', handleScroll, { passive: true });
313
-
314
- // Fallback for browsers without ResizeObserver or for dynamic content changes
315
- // Use a less frequent interval (500ms instead of 200ms)
316
- const intervalId = window.setInterval(updatePosition, 500);
317
-
318
- return () => {
319
- if (resizeObserver && menuRef.current) {
320
- resizeObserver.unobserve(menuRef.current);
321
- resizeObserver.disconnect();
322
- }
323
- window.removeEventListener('resize', handleResize);
324
- window.removeEventListener('scroll', handleScroll);
325
- window.clearInterval(intervalId);
326
- };
327
- }, [isOpen, updatePosition]);
328
-
329
- return {
330
- isOpen,
331
- setIsOpen,
332
- triggerRef,
333
- menuRef,
334
- dropdownId: uniqueId.current,
335
- currentPlacement,
336
- updatePosition,
337
- };
338
- };