@shohojdhara/atomix 0.4.8 → 0.4.9

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 (165) hide show
  1. package/atomix.config.ts +58 -1
  2. package/dist/atomix.css +148 -120
  3. package/dist/atomix.css.map +1 -1
  4. package/dist/atomix.min.css +1 -1
  5. package/dist/atomix.min.css.map +1 -1
  6. package/dist/charts.d.ts +33 -0
  7. package/dist/charts.js +1227 -122
  8. package/dist/charts.js.map +1 -1
  9. package/dist/core.d.ts +33 -10
  10. package/dist/core.js +1052 -41
  11. package/dist/core.js.map +1 -1
  12. package/dist/forms.d.ts +33 -0
  13. package/dist/forms.js +2086 -1035
  14. package/dist/forms.js.map +1 -1
  15. package/dist/heavy.d.ts +42 -1
  16. package/dist/heavy.js +1620 -600
  17. package/dist/heavy.js.map +1 -1
  18. package/dist/index.d.ts +441 -270
  19. package/dist/index.esm.js +1900 -638
  20. package/dist/index.esm.js.map +1 -1
  21. package/dist/index.js +1935 -670
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.min.js +1 -1
  24. package/dist/index.min.js.map +1 -1
  25. package/package.json +6 -3
  26. package/scripts/atomix-cli.js +148 -4
  27. package/scripts/cli/__tests__/basic.test.js +3 -2
  28. package/scripts/cli/__tests__/clean.test.js +278 -0
  29. package/scripts/cli/__tests__/component-validator.test.js +433 -0
  30. package/scripts/cli/__tests__/generator.test.js +613 -0
  31. package/scripts/cli/__tests__/glass-motion.test.js +256 -0
  32. package/scripts/cli/__tests__/integration.test.js +719 -108
  33. package/scripts/cli/__tests__/migrate.test.js +74 -0
  34. package/scripts/cli/__tests__/security.test.js +206 -0
  35. package/scripts/cli/__tests__/test-setup.js +3 -1
  36. package/scripts/cli/__tests__/theme-bridge.test.js +507 -0
  37. package/scripts/cli/__tests__/token-provider.test.js +361 -0
  38. package/scripts/cli/__tests__/utils.test.js +5 -5
  39. package/scripts/cli/commands/benchmark.js +105 -0
  40. package/scripts/cli/commands/build-theme.js +4 -1
  41. package/scripts/cli/commands/clean.js +109 -0
  42. package/scripts/cli/commands/doctor.js +88 -0
  43. package/scripts/cli/commands/generate.js +135 -14
  44. package/scripts/cli/commands/init.js +45 -18
  45. package/scripts/cli/commands/migrate.js +106 -0
  46. package/scripts/cli/commands/sync-tokens.js +206 -0
  47. package/scripts/cli/commands/theme-bridge.js +248 -0
  48. package/scripts/cli/commands/tokens.js +157 -0
  49. package/scripts/cli/commands/validate.js +194 -0
  50. package/scripts/cli/internal/ai-engine.js +156 -0
  51. package/scripts/cli/internal/component-validator.js +443 -0
  52. package/scripts/cli/internal/config-loader.js +162 -0
  53. package/scripts/cli/internal/filesystem.js +102 -2
  54. package/scripts/cli/internal/generator.js +359 -39
  55. package/scripts/cli/internal/glass-generator.js +398 -0
  56. package/scripts/cli/internal/hook-generator.js +369 -0
  57. package/scripts/cli/internal/hooks.js +61 -0
  58. package/scripts/cli/internal/itcss-generator.js +565 -0
  59. package/scripts/cli/internal/motion-generator.js +679 -0
  60. package/scripts/cli/internal/template-engine.js +301 -0
  61. package/scripts/cli/internal/theme-bridge.js +664 -0
  62. package/scripts/cli/internal/tokens/engine.js +122 -0
  63. package/scripts/cli/internal/tokens/provider.js +34 -0
  64. package/scripts/cli/internal/tokens/providers/figma.js +50 -0
  65. package/scripts/cli/internal/tokens/providers/style-dictionary.js +48 -0
  66. package/scripts/cli/internal/tokens/providers/w3c.js +48 -0
  67. package/scripts/cli/internal/tokens/token-provider.js +443 -0
  68. package/scripts/cli/internal/tokens/token-validator.js +513 -0
  69. package/scripts/cli/internal/validator.js +276 -0
  70. package/scripts/cli/internal/wizard.js +60 -6
  71. package/scripts/cli/mappings.js +23 -0
  72. package/scripts/cli/migration-tools.js +164 -94
  73. package/scripts/cli/plugins/style-dictionary.js +46 -0
  74. package/scripts/cli/templates/README.md +525 -95
  75. package/scripts/cli/templates/common-templates.js +40 -14
  76. package/scripts/cli/templates/components/react-component.ts +282 -0
  77. package/scripts/cli/templates/config/project-config.ts +112 -0
  78. package/scripts/cli/templates/hooks/use-component.ts +477 -0
  79. package/scripts/cli/templates/index.js +19 -4
  80. package/scripts/cli/templates/index.ts +171 -0
  81. package/scripts/cli/templates/next-templates.js +72 -0
  82. package/scripts/cli/templates/react-templates.js +70 -126
  83. package/scripts/cli/templates/scss-templates.js +35 -35
  84. package/scripts/cli/templates/stories/storybook-story.ts +241 -0
  85. package/scripts/cli/templates/styles/scss-component.ts +255 -0
  86. package/scripts/cli/templates/tests/vitest-test.ts +229 -0
  87. package/scripts/cli/templates/token-templates.js +337 -1
  88. package/scripts/cli/templates/tokens/token-generators.ts +1088 -0
  89. package/scripts/cli/templates/types/component-types.ts +145 -0
  90. package/scripts/cli/templates/utils/testing-utils.ts +144 -0
  91. package/scripts/cli/templates/vanilla-templates.js +39 -0
  92. package/scripts/cli/token-manager.js +8 -2
  93. package/scripts/cli/utils/cache-manager.js +240 -0
  94. package/scripts/cli/utils/detector.js +46 -0
  95. package/scripts/cli/utils/diagnostics.js +289 -0
  96. package/scripts/cli/utils/error.js +45 -3
  97. package/scripts/cli/utils/helpers.js +24 -0
  98. package/scripts/cli/utils/logger.js +1 -1
  99. package/scripts/cli/utils/security.js +302 -0
  100. package/scripts/cli/utils/telemetry.js +115 -0
  101. package/scripts/cli/utils/validation.js +4 -38
  102. package/scripts/cli/utils.js +46 -0
  103. package/src/components/Accordion/Accordion.stories.tsx +0 -18
  104. package/src/components/Accordion/Accordion.test.tsx +0 -17
  105. package/src/components/Accordion/Accordion.tsx +0 -4
  106. package/src/components/AtomixGlass/AtomixGlass.tsx +102 -2
  107. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +125 -12
  108. package/src/components/AtomixGlass/PerformanceDashboard.tsx +219 -0
  109. package/src/components/AtomixGlass/README.md +25 -10
  110. package/src/components/AtomixGlass/animation-system.ts +578 -0
  111. package/src/components/AtomixGlass/shader-utils.ts +4 -1
  112. package/src/components/AtomixGlass/stories/Overview.stories.tsx +157 -6
  113. package/src/components/AtomixGlass/stories/Phase1-Animation.stories.tsx +653 -0
  114. package/src/components/AtomixGlass/stories/Phase1-Test.stories.tsx +95 -0
  115. package/src/components/AtomixGlass/stories/Playground.stories.tsx +51 -51
  116. package/src/components/AtomixGlass/stories/shared-components.tsx +6 -0
  117. package/src/components/Avatar/Avatar.tsx +1 -1
  118. package/src/components/Button/Button.stories.disabled-link.tsx +10 -0
  119. package/src/components/Button/Button.stories.tsx +10 -0
  120. package/src/components/Button/Button.test.tsx +16 -11
  121. package/src/components/Button/Button.tsx +4 -4
  122. package/src/components/Card/Card.tsx +1 -1
  123. package/src/components/Dropdown/Dropdown.tsx +12 -12
  124. package/src/components/Form/Select.tsx +62 -3
  125. package/src/components/Modal/Modal.tsx +14 -3
  126. package/src/components/Navigation/Navbar/Navbar.tsx +44 -0
  127. package/src/components/Slider/Slider.stories.tsx +3 -3
  128. package/src/components/Slider/Slider.tsx +38 -0
  129. package/src/components/Steps/Steps.tsx +3 -3
  130. package/src/components/Tabs/Tabs.tsx +77 -8
  131. package/src/components/Testimonial/Testimonial.tsx +1 -1
  132. package/src/components/TypedButton/TypedButton.stories.tsx +59 -0
  133. package/src/components/TypedButton/TypedButton.tsx +39 -0
  134. package/src/components/TypedButton/index.ts +2 -0
  135. package/src/components/VideoPlayer/VideoPlayer.tsx +11 -4
  136. package/src/lib/composables/index.ts +4 -7
  137. package/src/lib/composables/types.ts +45 -0
  138. package/src/lib/composables/useAccordion.ts +0 -7
  139. package/src/lib/composables/useAtomixGlass.ts +144 -5
  140. package/src/lib/composables/useChartExport.ts +3 -13
  141. package/src/lib/composables/useDropdown.ts +66 -0
  142. package/src/lib/composables/useFocusTrap.ts +80 -0
  143. package/src/lib/composables/usePerformanceMonitor.ts +448 -0
  144. package/src/lib/composables/useResponsiveGlass.presets.ts +192 -0
  145. package/src/lib/composables/useResponsiveGlass.ts +441 -0
  146. package/src/lib/composables/useTooltip.ts +16 -0
  147. package/src/lib/composables/useTypedButton.ts +66 -0
  148. package/src/lib/config/index.ts +62 -5
  149. package/src/lib/constants/components.ts +55 -0
  150. package/src/lib/theme/devtools/__tests__/useHistory.test.tsx +150 -0
  151. package/src/lib/theme/tokens/centralized-tokens.ts +120 -0
  152. package/src/lib/theme/utils/__tests__/domUtils.test.ts +101 -0
  153. package/src/lib/types/components.ts +37 -11
  154. package/src/lib/types/glass.ts +35 -0
  155. package/src/lib/types/index.ts +1 -0
  156. package/src/lib/utils/displacement-generator.ts +1 -1
  157. package/src/styles/01-settings/_settings.testtypecheck.scss +53 -0
  158. package/src/styles/01-settings/_settings.typedbutton.scss +53 -0
  159. package/src/styles/06-components/_components.testbutton.scss +212 -0
  160. package/src/styles/06-components/_components.testtypecheck.scss +212 -0
  161. package/src/styles/06-components/_components.typedbutton.scss +212 -0
  162. package/src/styles/99-utilities/_index.scss +1 -0
  163. package/src/styles/99-utilities/_utilities.text.scss +1 -1
  164. package/src/styles/99-utilities/_utilities.touch-target.scss +36 -0
  165. package/src/styles/06-components/old.chart.styles.scss +0 -2788
@@ -0,0 +1,45 @@
1
+ /**
2
+ * TestTypeCheck Hook Types
3
+ * Generated by Atomix CLI
4
+ */
5
+
6
+ /**
7
+ * Component State Interface
8
+ */
9
+ export interface ComponentState {
10
+ isActive: boolean;
11
+ isHovered: boolean;
12
+ isFocused: boolean;
13
+ isLoading: boolean;
14
+ variant: 'primary' | 'secondary' | 'outline' | string;
15
+ size: 'sm' | 'md' | 'lg' | string;
16
+ }
17
+
18
+ /**
19
+ * Event Handler Types
20
+ */
21
+ export type ClickHandler = (event: React.MouseEvent<HTMLDivElement>) => void;
22
+ export type HoverHandler = (event: React.MouseEvent<HTMLDivElement>) => void;
23
+ export type FocusHandler = (event: React.FocusEvent<HTMLDivElement>) => void;
24
+ export type BlurHandler = (event: React.FocusEvent<HTMLDivElement>) => void;
25
+ export type StateChangeHandler = (state: ComponentState) => void;
26
+
27
+ /**
28
+ * Variant Configuration
29
+ */
30
+ export type VariantType = 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
31
+
32
+ /**
33
+ * Size Configuration
34
+ */
35
+ export type SizeType = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
36
+
37
+ /**
38
+ * Glass Effect Configuration
39
+ */
40
+ export interface GlassConfig {
41
+ displacementScale?: number;
42
+ blurAmount?: number;
43
+ saturation?: number;
44
+ elasticity?: number;
45
+ }
@@ -66,13 +66,6 @@ export function useAccordion(
66
66
  }
67
67
 
68
68
  defaultProps.onOpenChange?.(nextOpen);
69
-
70
- // Call legacy handlers
71
- if (nextOpen) {
72
- defaultProps.onOpen?.();
73
- } else {
74
- defaultProps.onClose?.();
75
- }
76
69
  }
77
70
  };
78
71
 
@@ -23,6 +23,13 @@ import {
23
23
  lerp,
24
24
  } from '../../components/AtomixGlass/glass-utils';
25
25
  import { updateAtomixGlassStyles } from './useAtomixGlassStyles';
26
+ // Phase 1: Time-Based Animation System
27
+ import {
28
+ createAnimationLoop,
29
+ createFBMEngine,
30
+ getFBMConfigForQuality,
31
+ liquidGlassWithTime,
32
+ } from '../../components/AtomixGlass/animation-system';
26
33
 
27
34
  const { CONSTANTS } = ATOMIX_GLASS;
28
35
 
@@ -151,6 +158,13 @@ interface UseAtomixGlassOptions extends Omit<AtomixGlassProps, 'children'> {
151
158
  wrapperRef?: React.RefObject<HTMLDivElement>;
152
159
  children?: React.ReactNode;
153
160
  isFixedOrSticky?: boolean;
161
+ // Phase 1: Time-Based Animation System
162
+ withLiquidBlur?: boolean;
163
+ animationQuality?: 'low' | 'medium' | 'high';
164
+ timeSpeed?: number;
165
+ noiseAmplitude?: number;
166
+ noiseFrequency?: number;
167
+ displacementStrength?: number;
154
168
  }
155
169
 
156
170
  interface UseAtomixGlassReturn {
@@ -182,6 +196,10 @@ interface UseAtomixGlassReturn {
182
196
  // Transform calculations
183
197
  transformStyle: string;
184
198
 
199
+ // Phase 1: Animation System - Shader time control
200
+ getShaderTime: () => number;
201
+ applyTimeBasedDistortion: (uv: { x: number; y: number }) => { x: number; y: number };
202
+
185
203
  // Event handlers
186
204
  handleMouseEnter: () => void;
187
205
  handleMouseLeave: () => void;
@@ -218,6 +236,14 @@ export function useAtomixGlass({
218
236
  padding,
219
237
  withLiquidBlur,
220
238
  isFixedOrSticky = false,
239
+ // Phase 1: Animation System Props
240
+ withTimeAnimation = ATOMIX_GLASS.DEFAULTS.WITH_TIME_ANIMATION,
241
+ animationSpeed = ATOMIX_GLASS.DEFAULTS.ANIMATION_SPEED,
242
+ withMultiLayerDistortion = ATOMIX_GLASS.DEFAULTS.WITH_MULTI_LAYER_DISTORTION,
243
+ distortionOctaves = ATOMIX_GLASS.DEFAULTS.DISTORTION_OCTAVES,
244
+ distortionLacunarity = ATOMIX_GLASS.DEFAULTS.DISTORTION_LACUNARITY,
245
+ distortionGain = ATOMIX_GLASS.DEFAULTS.DISTORTION_GAIN,
246
+ distortionQuality = ATOMIX_GLASS.DEFAULTS.DISTORTION_QUALITY,
221
247
  }: UseAtomixGlassOptions): UseAtomixGlassReturn {
222
248
  // State
223
249
  const [isHovered, setIsHovered] = useState(false);
@@ -243,6 +269,115 @@ export function useAtomixGlass({
243
269
  const [userPrefersHighContrast, setUserPrefersHighContrast] = useState(false);
244
270
  const [detectedOverLight, setDetectedOverLight] = useState(false);
245
271
 
272
+ // ============================================================================
273
+ // Phase 1: Time-Based Animation System (Feature 1.1)
274
+ // ============================================================================
275
+
276
+ // Animation state refs
277
+ const animationFrameIdRef = useRef<number | null>(null);
278
+ const animationStartTimeRef = useRef<number>(0);
279
+ const elapsedTimeRef = useRef<number>(0);
280
+ const shaderTimeRef = useRef<number>(0);
281
+
282
+ /**
283
+ * Get FBM configuration based on quality preset or custom values
284
+ */
285
+ const fbmConfig = useMemo(() => {
286
+ // If quality preset is provided, use it as base
287
+ const preset = getFBMConfigForQuality(distortionQuality);
288
+
289
+ // Override with custom values if provided
290
+ return {
291
+ octaves: distortionOctaves ?? preset.octaves,
292
+ lacunarity: distortionLacunarity ?? preset.lacunarity,
293
+ gain: distortionGain ?? preset.gain,
294
+ };
295
+ }, [distortionQuality, distortionOctaves, distortionLacunarity, distortionGain]);
296
+
297
+ /**
298
+ * Create FBM engine for multi-layer distortion
299
+ */
300
+ const fbmEngine = useMemo(() => {
301
+ if (!withMultiLayerDistortion) return null;
302
+ return createFBMEngine(fbmConfig);
303
+ }, [withMultiLayerDistortion, fbmConfig]);
304
+
305
+ /**
306
+ * Determine effective animation settings
307
+ */
308
+ const effectiveReducedMotion = useMemo(
309
+ () => reducedMotion || userPrefersReducedMotion,
310
+ [reducedMotion, userPrefersReducedMotion]
311
+ );
312
+
313
+ const effectiveWithTimeAnimation = useMemo(() => {
314
+ return withTimeAnimation && !effectiveReducedMotion;
315
+ }, [withTimeAnimation, effectiveReducedMotion]);
316
+
317
+ /**
318
+ * Animation loop for time-based effects
319
+ */
320
+ useEffect(() => {
321
+ if (!effectiveWithTimeAnimation || typeof window === 'undefined') {
322
+ return undefined;
323
+ }
324
+
325
+ let lastFrameTime = performance.now();
326
+
327
+ /**
328
+ * Animation frame handler
329
+ */
330
+ const animate = (currentTime: number) => {
331
+ // Calculate delta time
332
+ const deltaTime = currentTime - lastFrameTime;
333
+ lastFrameTime = currentTime;
334
+
335
+ // Apply animation speed multiplier
336
+ const scaledDelta = deltaTime * animationSpeed;
337
+ elapsedTimeRef.current += scaledDelta;
338
+ shaderTimeRef.current = elapsedTimeRef.current;
339
+
340
+ // Continue animation loop
341
+ animationFrameIdRef.current = requestAnimationFrame(animate);
342
+ };
343
+
344
+ // Start animation
345
+ animationStartTimeRef.current = performance.now();
346
+ animationFrameIdRef.current = requestAnimationFrame(animate);
347
+
348
+ // Cleanup
349
+ return () => {
350
+ if (animationFrameIdRef.current !== null) {
351
+ cancelAnimationFrame(animationFrameIdRef.current);
352
+ animationFrameIdRef.current = null;
353
+ }
354
+ };
355
+ }, [effectiveWithTimeAnimation, animationSpeed]);
356
+
357
+ /**
358
+ * Get current shader time for animations
359
+ */
360
+ const getShaderTime = useCallback(() => {
361
+ return shaderTimeRef.current;
362
+ }, []);
363
+
364
+ /**
365
+ * Apply time-based distortion to UV coordinates
366
+ */
367
+ const applyTimeBasedDistortion = useCallback(
368
+ (uv: { x: number; y: number }): { x: number; y: number } => {
369
+ if (!effectiveWithTimeAnimation || !fbmEngine) {
370
+ return uv;
371
+ }
372
+
373
+ const time = shaderTimeRef.current;
374
+
375
+ // Apply liquid glass distortion with time
376
+ return liquidGlassWithTime(uv, time, fbmConfig);
377
+ },
378
+ [effectiveWithTimeAnimation, fbmEngine, fbmConfig]
379
+ );
380
+
246
381
  // Memoized derived values
247
382
  const effectiveBorderRadius = useMemo(() => {
248
383
  if (borderRadius !== undefined) {
@@ -259,10 +394,6 @@ export function useAtomixGlass({
259
394
  cachedRectRef
260
395
  });
261
396
 
262
- const effectiveReducedMotion = useMemo(
263
- () => reducedMotion || userPrefersReducedMotion,
264
- [reducedMotion, userPrefersReducedMotion]
265
- );
266
397
 
267
398
  const effectiveHighContrast = useMemo(
268
399
  () => highContrast || userPrefersHighContrast,
@@ -701,6 +832,12 @@ export function useAtomixGlass({
701
832
  const tick = () => {
702
833
  if (!lerpActiveRef.current) return;
703
834
 
835
+ // Add ref validity check to prevent memory leaks
836
+ if (!glassRef.current || !wrapperRef?.current) {
837
+ lerpActiveRef.current = false;
838
+ return;
839
+ }
840
+
704
841
  const cur = internalMouseOffsetRef.current;
705
842
  const tgt = targetMouseOffsetRef.current;
706
843
 
@@ -726,7 +863,7 @@ export function useAtomixGlass({
726
863
 
727
864
  // Imperative style update with the smoothed values
728
865
  updateAtomixGlassStyles(
729
- wrapperRef?.current || null,
866
+ wrapperRef.current,
730
867
  glassRef.current,
731
868
  {
732
869
  mouseOffset: internalMouseOffsetRef.current,
@@ -916,6 +1053,8 @@ export function useAtomixGlass({
916
1053
  mouseOffset, // This is now static (refs or props) unless prop changes
917
1054
  overLightConfig,
918
1055
  transformStyle,
1056
+ getShaderTime,
1057
+ applyTimeBasedDistortion,
919
1058
  handleMouseEnter,
920
1059
  handleMouseLeave,
921
1060
  handleMouseDown,
@@ -139,20 +139,10 @@ export function useChartExport() {
139
139
 
140
140
  // Export as PDF
141
141
  const exportAsPDF = useCallback(
142
- async (svgElement: SVGSVGElement, options: ExportOptions): Promise<void> => {
143
- // Note: This requires a PDF library like jsPDF
144
- // For now, we'll convert to canvas and then to PDF
145
- const canvas = await svgToCanvas(svgElement, options);
146
-
147
- // This would require jsPDF library
148
- // const pdf = new jsPDF();
149
- // const imgData = canvas.toDataURL('image/png');
150
- // pdf.addImage(imgData, 'PNG', 0, 0);
151
- // pdf.save(options.filename || 'chart.pdf');
152
-
153
- console.warn('PDF export requires jsPDF library to be installed');
142
+ async (_svgElement: SVGSVGElement, _options: ExportOptions): Promise<void> => {
143
+ console.warn('PDF export requires a PDF library to be installed');
154
144
  },
155
- [svgToCanvas]
145
+ []
156
146
  );
157
147
 
158
148
  // Export data as CSV
@@ -107,6 +107,72 @@ export const useDropdown = ({
107
107
  };
108
108
  }, [isOpen, closeOnEscape, setIsOpen]);
109
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
+
110
176
  // Helper function to get the flipped placement if needed
111
177
  const getFlippedPlacement = useCallback(
112
178
  (
@@ -0,0 +1,80 @@
1
+ import { useEffect, useRef, useCallback } from 'react';
2
+
3
+ /**
4
+ * Hook to trap focus within an element
5
+ */
6
+ export function useFocusTrap(isOpen: boolean) {
7
+ const containerRef = useRef<HTMLDivElement>(null);
8
+ const previouslyFocusedElement = useRef<HTMLElement | null>(null);
9
+
10
+ const getFocusableElements = useCallback(() => {
11
+ if (!containerRef.current) return [];
12
+
13
+ return Array.from(
14
+ containerRef.current.querySelectorAll<HTMLElement>(
15
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
16
+ )
17
+ ).filter((el) => {
18
+ // Check if visible
19
+ const style = window.getComputedStyle(el);
20
+ return style.display !== 'none' && style.visibility !== 'hidden';
21
+ });
22
+ }, []);
23
+
24
+ const handleKeyDown = useCallback(
25
+ (event: KeyboardEvent) => {
26
+ if (event.key !== 'Tab') return;
27
+
28
+ const focusableElements = getFocusableElements();
29
+ if (focusableElements.length === 0) {
30
+ return;
31
+ }
32
+
33
+ const firstElement = focusableElements[0] as HTMLElement;
34
+ const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
35
+
36
+ if (event.shiftKey) {
37
+ // Shift + Tab
38
+ if (document.activeElement === firstElement) {
39
+ event.preventDefault();
40
+ lastElement.focus();
41
+ }
42
+ } else {
43
+ // Tab
44
+ if (document.activeElement === lastElement) {
45
+ event.preventDefault();
46
+ firstElement.focus();
47
+ }
48
+ }
49
+ },
50
+ [getFocusableElements]
51
+ );
52
+
53
+ useEffect(() => {
54
+ if (isOpen) {
55
+ previouslyFocusedElement.current = document.activeElement as HTMLElement;
56
+
57
+ const focusableElements = getFocusableElements();
58
+ if (focusableElements.length > 0 && focusableElements[0]) {
59
+ // Delay focus slightly to ensure element is rendered
60
+ const firstEl = focusableElements[0] as HTMLElement;
61
+ setTimeout(() => {
62
+ firstEl.focus();
63
+ }, 0);
64
+ }
65
+
66
+ document.addEventListener('keydown', handleKeyDown);
67
+ } else {
68
+ if (previouslyFocusedElement.current) {
69
+ previouslyFocusedElement.current.focus();
70
+ }
71
+ document.removeEventListener('keydown', handleKeyDown);
72
+ }
73
+
74
+ return () => {
75
+ document.removeEventListener('keydown', handleKeyDown);
76
+ };
77
+ }, [isOpen, getFocusableElements, handleKeyDown]);
78
+
79
+ return containerRef;
80
+ }