@shohojdhara/atomix 0.3.15 → 0.4.0

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 (245) hide show
  1. package/build-tools/index.d.ts +31 -30
  2. package/build-tools/package.json +4 -21
  3. package/dist/atomix.css +20924 -2611
  4. package/dist/atomix.css.map +1 -1
  5. package/dist/atomix.min.css +76 -2
  6. package/dist/atomix.min.css.map +1 -1
  7. package/dist/build-tools/index.d.ts +31 -30
  8. package/dist/build-tools/package.json +4 -21
  9. package/dist/charts.js.map +1 -1
  10. package/dist/core.js.map +1 -1
  11. package/dist/forms.js.map +1 -1
  12. package/dist/heavy.js.map +1 -1
  13. package/dist/index.d.ts +144 -18
  14. package/dist/index.esm.js +110 -55
  15. package/dist/index.esm.js.map +1 -1
  16. package/dist/index.js +110 -55
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.min.js +1 -1
  19. package/dist/index.min.js.map +1 -1
  20. package/dist/layout.js.map +1 -1
  21. package/dist/theme.d.ts +9 -9
  22. package/dist/theme.js.map +1 -1
  23. package/package.json +1 -1
  24. package/src/components/Accordion/Accordion.stories.tsx +32 -23
  25. package/src/components/Accordion/Accordion.test.tsx +70 -50
  26. package/src/components/Accordion/Accordion.tsx +99 -94
  27. package/src/components/AtomixGlass/AtomixGlass.test.tsx +1 -1
  28. package/src/components/AtomixGlass/GlassFilter.tsx +9 -16
  29. package/src/components/AtomixGlass/glass-utils.ts +4 -3
  30. package/src/components/AtomixGlass/shader-utils.ts +128 -52
  31. package/src/components/AtomixGlass/stories/Playground.stories.tsx +1 -1
  32. package/src/components/AtomixGlass/stories/Shaders.stories.tsx +1 -1
  33. package/src/components/Avatar/Avatar.stories.tsx +45 -62
  34. package/src/components/Avatar/Avatar.tsx +58 -56
  35. package/src/components/Badge/Badge.stories.tsx +20 -9
  36. package/src/components/Badge/Badge.test.tsx +41 -41
  37. package/src/components/Badge/Badge.tsx +64 -62
  38. package/src/components/Block/Block.stories.tsx +14 -4
  39. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +9 -8
  40. package/src/components/Breadcrumb/Breadcrumb.tsx +62 -60
  41. package/src/components/Button/Button.stories.tsx +13 -22
  42. package/src/components/Button/Button.test.tsx +97 -81
  43. package/src/components/Button/Button.tsx +46 -14
  44. package/src/components/Button/ButtonGroup.stories.tsx +37 -32
  45. package/src/components/Button/ButtonGroup.tsx +4 -15
  46. package/src/components/Callout/Callout.stories.tsx +109 -16
  47. package/src/components/Card/Card.stories.tsx +67 -36
  48. package/src/components/Card/Card.tsx +30 -14
  49. package/src/components/Chart/AreaChart.tsx +1 -1
  50. package/src/components/Chart/CandlestickChart.tsx +23 -16
  51. package/src/components/Chart/Chart.stories.tsx +4 -9
  52. package/src/components/Chart/Chart.tsx +40 -44
  53. package/src/components/Chart/ChartRenderer.tsx +39 -12
  54. package/src/components/Chart/ChartToolbar.tsx +21 -5
  55. package/src/components/Chart/DonutChart.tsx +1 -1
  56. package/src/components/Chart/FunnelChart.tsx +4 -1
  57. package/src/components/Chart/GaugeChart.tsx +3 -1
  58. package/src/components/Chart/HeatmapChart.tsx +50 -37
  59. package/src/components/Chart/LineChart.tsx +3 -2
  60. package/src/components/Chart/MultiAxisChart.tsx +24 -16
  61. package/src/components/Chart/RadarChart.tsx +19 -17
  62. package/src/components/Chart/ScatterChart.tsx +29 -21
  63. package/src/components/ColorModeToggle/ColorModeToggle.stories.tsx +6 -2
  64. package/src/components/ColorModeToggle/ColorModeToggle.tsx +15 -3
  65. package/src/components/Countdown/Countdown.stories.tsx +7 -7
  66. package/src/components/DataTable/DataTable.stories.tsx +43 -38
  67. package/src/components/DataTable/DataTable.test.tsx +26 -148
  68. package/src/components/DataTable/DataTable.tsx +485 -456
  69. package/src/components/DatePicker/DatePicker.stories.tsx +32 -47
  70. package/src/components/DatePicker/DatePicker.tsx +31 -26
  71. package/src/components/Dropdown/Dropdown.stories.tsx +2 -5
  72. package/src/components/Dropdown/Dropdown.tsx +313 -299
  73. package/src/components/EdgePanel/EdgePanel.stories.tsx +6 -19
  74. package/src/components/EdgePanel/EdgePanel.tsx +1 -3
  75. package/src/components/Footer/Footer.stories.tsx +21 -16
  76. package/src/components/Footer/Footer.tsx +130 -128
  77. package/src/components/Footer/FooterLink.tsx +2 -2
  78. package/src/components/Form/Checkbox.test.tsx +49 -49
  79. package/src/components/Form/Checkbox.tsx +108 -100
  80. package/src/components/Form/Form.stories.tsx +2 -10
  81. package/src/components/Form/Input.stories.tsx +22 -39
  82. package/src/components/Form/Input.test.tsx +38 -44
  83. package/src/components/Form/Radio.stories.tsx +6 -12
  84. package/src/components/Form/Radio.tsx +68 -66
  85. package/src/components/Form/Select.tsx +184 -182
  86. package/src/components/Form/Textarea.test.tsx +27 -32
  87. package/src/components/Hero/Hero.stories.tsx +56 -23
  88. package/src/components/Hero/Hero.tsx +201 -55
  89. package/src/components/Icon/index.ts +7 -1
  90. package/src/components/List/List.tsx +19 -23
  91. package/src/components/Modal/Modal.stories.tsx +2 -1
  92. package/src/components/Modal/Modal.tsx +130 -127
  93. package/src/components/Navigation/Menu/MegaMenu.tsx +70 -70
  94. package/src/components/Navigation/Nav/NavDropdown.tsx +1 -5
  95. package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +128 -28
  96. package/src/components/Navigation/SideMenu/SideMenu.tsx +5 -7
  97. package/src/components/Navigation/SideMenu/SideMenuItem.tsx +4 -5
  98. package/src/components/Pagination/Pagination.stories.tsx +7 -4
  99. package/src/components/Pagination/Pagination.tsx +199 -202
  100. package/src/components/PhotoViewer/PhotoViewer.tsx +4 -1
  101. package/src/components/Popover/Popover.stories.tsx +99 -192
  102. package/src/components/Popover/Popover.tsx +41 -37
  103. package/src/components/Progress/Progress.stories.tsx +35 -44
  104. package/src/components/River/River.stories.tsx +2 -1
  105. package/src/components/SectionIntro/SectionIntro.stories.tsx +71 -71
  106. package/src/components/Slider/Slider.stories.tsx +12 -4
  107. package/src/components/Spinner/Spinner.stories.tsx +3 -1
  108. package/src/components/Spinner/Spinner.test.tsx +23 -23
  109. package/src/components/Spinner/Spinner.tsx +43 -46
  110. package/src/components/Steps/Steps.stories.tsx +8 -6
  111. package/src/components/Tabs/Tabs.stories.tsx +12 -9
  112. package/src/components/Tabs/Tabs.tsx +74 -72
  113. package/src/components/Toggle/Toggle.stories.tsx +27 -13
  114. package/src/components/Toggle/Toggle.test.tsx +65 -70
  115. package/src/components/Toggle/Toggle.tsx +4 -1
  116. package/src/components/Tooltip/Tooltip.stories.tsx +24 -20
  117. package/src/components/Tooltip/Tooltip.tsx +104 -106
  118. package/src/components/Upload/Upload.stories.tsx +129 -127
  119. package/src/components/Upload/Upload.tsx +287 -283
  120. package/src/components/VideoPlayer/VideoPlayer.tsx +6 -1
  121. package/src/components/index.ts +13 -2
  122. package/src/layouts/Grid/Grid.stories.tsx +9 -3
  123. package/src/layouts/MasonryGrid/MasonryGrid.tsx +5 -1
  124. package/src/lib/__tests__/theme-tools.test.ts +32 -6
  125. package/src/lib/composables/shared-mouse-tracker.ts +13 -14
  126. package/src/lib/composables/useAtomixGlass.ts +106 -49
  127. package/src/lib/composables/useChartExport.ts +1 -1
  128. package/src/lib/composables/useDataTable.ts +29 -17
  129. package/src/lib/composables/useHero.ts +58 -14
  130. package/src/lib/composables/useHeroBackgroundSlider.ts +2 -9
  131. package/src/lib/composables/useInput.ts +10 -8
  132. package/src/lib/composables/useSideMenu.ts +6 -5
  133. package/src/lib/composables/useTooltip.ts +1 -2
  134. package/src/lib/composables/useVideoPlayer.ts +44 -35
  135. package/src/lib/config/index.ts +154 -154
  136. package/src/lib/constants/cssVariables.ts +29 -29
  137. package/src/lib/hooks/__tests__/useComponentCustomization.test.ts +2 -6
  138. package/src/lib/hooks/index.ts +1 -1
  139. package/src/lib/hooks/useComponentCustomization.ts +11 -17
  140. package/src/lib/hooks/usePerformanceMonitor.ts +6 -7
  141. package/src/lib/patterns/__tests__/slots.test.ts +1 -1
  142. package/src/lib/patterns/index.ts +1 -1
  143. package/src/lib/patterns/slots.tsx +8 -13
  144. package/src/lib/storybook/InteractiveDemo.tsx +13 -18
  145. package/src/lib/storybook/PreviewContainer.tsx +1 -1
  146. package/src/lib/storybook/VariantsGrid.tsx +3 -7
  147. package/src/lib/storybook/index.ts +1 -1
  148. package/src/lib/theme/adapters/cssVariableMapper.ts +47 -74
  149. package/src/lib/theme/adapters/index.ts +3 -9
  150. package/src/lib/theme/adapters/themeAdapter.ts +41 -26
  151. package/src/lib/theme/config/index.ts +1 -1
  152. package/src/lib/theme/config/types.ts +2 -2
  153. package/src/lib/theme/config/validator.ts +10 -5
  154. package/src/lib/theme/constants/constants.ts +2 -2
  155. package/src/lib/theme/constants/index.ts +1 -2
  156. package/src/lib/theme/core/__tests__/createTheme.test.ts +20 -22
  157. package/src/lib/theme/core/composeTheme.ts +32 -26
  158. package/src/lib/theme/core/createTheme.ts +1 -1
  159. package/src/lib/theme/core/createThemeObject.ts +308 -301
  160. package/src/lib/theme/core/index.ts +3 -3
  161. package/src/lib/theme/devtools/CLI.ts +106 -104
  162. package/src/lib/theme/devtools/Comparator.tsx +50 -32
  163. package/src/lib/theme/devtools/DesignTokensCustomizer.stories.tsx +50 -48
  164. package/src/lib/theme/devtools/DesignTokensCustomizer.tsx +257 -63
  165. package/src/lib/theme/devtools/Inspector.tsx +75 -60
  166. package/src/lib/theme/devtools/LiveEditor.tsx +97 -76
  167. package/src/lib/theme/devtools/Preview.tsx +150 -106
  168. package/src/lib/theme/devtools/ThemeValidator.ts +29 -21
  169. package/src/lib/theme/devtools/index.ts +3 -9
  170. package/src/lib/theme/devtools/useHistory.ts +23 -21
  171. package/src/lib/theme/errors/errors.ts +12 -11
  172. package/src/lib/theme/errors/index.ts +2 -7
  173. package/src/lib/theme/generators/generateCSS.ts +9 -13
  174. package/src/lib/theme/generators/generateCSSNested.ts +1 -6
  175. package/src/lib/theme/generators/generateCSSVariables.ts +673 -630
  176. package/src/lib/theme/generators/index.ts +1 -4
  177. package/src/lib/theme/i18n/index.ts +1 -1
  178. package/src/lib/theme/i18n/rtl.ts +13 -13
  179. package/src/lib/theme/index.ts +7 -16
  180. package/src/lib/theme/runtime/ThemeApplicator.ts +4 -4
  181. package/src/lib/theme/runtime/ThemeContext.tsx +1 -1
  182. package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +19 -23
  183. package/src/lib/theme/runtime/ThemeProvider.tsx +230 -239
  184. package/src/lib/theme/runtime/__tests__/ThemeProvider.integration.test.tsx +1 -1
  185. package/src/lib/theme/runtime/__tests__/ThemeProvider.test.tsx +24 -29
  186. package/src/lib/theme/runtime/index.ts +2 -5
  187. package/src/lib/theme/runtime/useTheme.ts +18 -18
  188. package/src/lib/theme/runtime/useThemeTokens.ts +22 -22
  189. package/src/lib/theme/test/testTheme.ts +15 -16
  190. package/src/lib/theme/tokens/index.ts +2 -7
  191. package/src/lib/theme/tokens/tokens.ts +25 -24
  192. package/src/lib/theme/types.ts +428 -411
  193. package/src/lib/theme/utils/__tests__/themeValidation.test.ts +3 -3
  194. package/src/lib/theme/utils/componentTheming.ts +18 -18
  195. package/src/lib/theme/utils/domUtils.ts +277 -289
  196. package/src/lib/theme/utils/index.ts +1 -2
  197. package/src/lib/theme/utils/injectCSS.ts +10 -14
  198. package/src/lib/theme/utils/naming.ts +20 -16
  199. package/src/lib/theme/utils/themeHelpers.ts +10 -12
  200. package/src/lib/theme/utils/themeUtils.ts +85 -86
  201. package/src/lib/theme/utils/themeValidation.ts +82 -33
  202. package/src/lib/theme-tools.ts +8 -6
  203. package/src/lib/types/components.ts +172 -71
  204. package/src/lib/types/partProps.ts +1 -1
  205. package/src/lib/utils/__tests__/csv.test.ts +1 -1
  206. package/src/lib/utils/componentUtils.ts +8 -12
  207. package/src/lib/utils/csv.ts +3 -1
  208. package/src/lib/utils/dataTableExport.ts +1 -5
  209. package/src/lib/utils/fontPreloader.ts +10 -19
  210. package/src/lib/utils/icons.ts +4 -1
  211. package/src/lib/utils/index.ts +2 -6
  212. package/src/lib/utils/memoryMonitor.ts +10 -8
  213. package/src/lib/utils/themeNaming.ts +2 -2
  214. package/src/styles/01-settings/_index.scss +0 -1
  215. package/src/styles/01-settings/_settings.colors.scss +8 -8
  216. package/src/styles/01-settings/_settings.design-tokens.scss +61 -50
  217. package/src/styles/01-settings/_settings.navbar.scss +1 -1
  218. package/src/styles/01-settings/_settings.spacing.scss +3 -4
  219. package/src/styles/01-settings/_settings.tooltip.scss +1 -1
  220. package/src/styles/01-settings/_settings.typography.scss +1 -1
  221. package/src/styles/02-tools/_tools.button.scss +51 -21
  222. package/src/styles/02-tools/_tools.utility-api.scss +30 -18
  223. package/src/styles/03-generic/_generic.root.scss +4 -3
  224. package/src/styles/06-components/_components.atomix-glass.scss +13 -9
  225. package/src/styles/06-components/_components.button.scss +16 -4
  226. package/src/styles/06-components/_components.callout.scss +27 -21
  227. package/src/styles/06-components/_components.card.scss +5 -14
  228. package/src/styles/06-components/_components.chart.scss +22 -19
  229. package/src/styles/06-components/_components.checkbox.scss +3 -1
  230. package/src/styles/06-components/_components.color-mode-toggle.scss +3 -1
  231. package/src/styles/06-components/_components.edge-panel.scss +9 -2
  232. package/src/styles/06-components/_components.footer.scss +1 -1
  233. package/src/styles/06-components/_components.side-menu.scss +5 -5
  234. package/src/styles/06-components/_components.toggle.scss +18 -0
  235. package/src/styles/06-components/_index.scss +1 -1
  236. package/src/styles/06-components/old.chart.styles.scss +0 -2
  237. package/src/styles/99-utilities/_utilities.border.scss +69 -27
  238. package/src/styles/99-utilities/_utilities.display.scss +1 -1
  239. package/src/styles/99-utilities/_utilities.opacity.scss +10 -0
  240. package/src/styles/99-utilities/_utilities.position.scss +16 -9
  241. package/src/styles/99-utilities/_utilities.scss +1 -1
  242. package/src/styles/99-utilities/_utilities.sizes.scss +47 -18
  243. package/src/styles/99-utilities/_utilities.spacing.scss +118 -66
  244. package/src/styles/99-utilities/_utilities.text-gradient.scss +30 -30
  245. package/src/styles/99-utilities/_utilities.text.scss +67 -46
@@ -1,4 +1,12 @@
1
- import React, { useRef, useState, useCallback, createContext, useContext, useEffect, memo } from 'react';
1
+ import React, {
2
+ useRef,
3
+ useState,
4
+ useCallback,
5
+ createContext,
6
+ useContext,
7
+ useEffect,
8
+ memo,
9
+ } from 'react';
2
10
  import { DROPDOWN } from '../../lib/constants/components';
3
11
  import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
4
12
  import type {
@@ -27,91 +35,93 @@ const DropdownContext = createContext<DropdownContextType>({
27
35
  /**
28
36
  * DropdownItem component for menu items
29
37
  */
30
- export const DropdownItem: React.FC<DropdownItemProps> = memo(({
31
- children,
32
- href,
33
- active = false,
34
- disabled = false,
35
- icon,
36
- onClick,
37
- className = '',
38
- LinkComponent,
39
- ...props
40
- }) => {
41
- const { close } = useContext(DropdownContext);
42
-
43
- const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
44
- if (disabled) {
45
- e.preventDefault();
46
- return;
47
- }
38
+ export const DropdownItem: React.FC<DropdownItemProps> = memo(
39
+ ({
40
+ children,
41
+ href,
42
+ active = false,
43
+ disabled = false,
44
+ icon,
45
+ onClick,
46
+ className = '',
47
+ LinkComponent,
48
+ ...props
49
+ }) => {
50
+ const { close } = useContext(DropdownContext);
51
+
52
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
53
+ if (disabled) {
54
+ e.preventDefault();
55
+ return;
56
+ }
48
57
 
49
- if (onClick) {
50
- onClick(e);
51
- }
58
+ if (onClick) {
59
+ onClick(e);
60
+ }
61
+
62
+ // Always close the dropdown when an item is clicked
63
+ close();
64
+ };
52
65
 
53
- // Always close the dropdown when an item is clicked
54
- close();
55
- };
66
+ const itemClasses = [
67
+ 'c-dropdown__menu-item',
68
+ active ? 'is-active' : '',
69
+ disabled ? 'is-disabled' : '',
70
+ className,
71
+ ]
72
+ .filter(Boolean)
73
+ .join(' ');
74
+
75
+ const linkProps = {
76
+ href,
77
+ className: itemClasses,
78
+ onClick: handleClick,
79
+ role: 'menuitem',
80
+ tabIndex: 0,
81
+ ...props,
82
+ };
56
83
 
57
- const itemClasses = [
58
- 'c-dropdown__menu-item',
59
- active ? 'is-active' : '',
60
- disabled ? 'is-disabled' : '',
61
- className,
62
- ]
63
- .filter(Boolean)
64
- .join(' ');
84
+ if (href && !disabled) {
85
+ return (
86
+ <li>
87
+ {LinkComponent ? (
88
+ (() => {
89
+ const Component = LinkComponent as React.ComponentType<any>;
90
+ return (
91
+ <Component {...linkProps}>
92
+ {icon && <span className="c-dropdown__menu-item-icon">{icon}</span>}
93
+ {children}
94
+ </Component>
95
+ );
96
+ })()
97
+ ) : (
98
+ <a {...linkProps}>
99
+ {icon && <span className="c-dropdown__menu-item-icon">{icon}</span>}
100
+ {children}
101
+ </a>
102
+ )}
103
+ </li>
104
+ );
105
+ }
65
106
 
66
- const linkProps = {
67
- href,
68
- className: itemClasses,
69
- onClick: handleClick,
70
- role: 'menuitem',
71
- tabIndex: 0,
72
- ...props,
73
- };
74
-
75
- if (href && !disabled) {
76
107
  return (
77
108
  <li>
78
- {LinkComponent ? (
79
- (() => {
80
- const Component = LinkComponent as React.ComponentType<any>;
81
- return (
82
- <Component {...linkProps}>
83
- {icon && <span className="c-dropdown__menu-item-icon">{icon}</span>}
84
- {children}
85
- </Component>
86
- );
87
- })()
88
- ) : (
89
- <a {...linkProps}>
90
- {icon && <span className="c-dropdown__menu-item-icon">{icon}</span>}
91
- {children}
92
- </a>
93
- )}
109
+ <button
110
+ type="button"
111
+ className={itemClasses}
112
+ onClick={handleClick}
113
+ disabled={disabled}
114
+ role="menuitem"
115
+ tabIndex={0}
116
+ {...props}
117
+ >
118
+ {icon && <span className="c-dropdown__menu-item-icon">{icon}</span>}
119
+ {children}
120
+ </button>
94
121
  </li>
95
122
  );
96
123
  }
97
-
98
- return (
99
- <li>
100
- <button
101
- type="button"
102
- className={itemClasses}
103
- onClick={handleClick}
104
- disabled={disabled}
105
- role="menuitem"
106
- tabIndex={0}
107
- {...props}
108
- >
109
- {icon && <span className="c-dropdown__menu-item-icon">{icon}</span>}
110
- {children}
111
- </button>
112
- </li>
113
- );
114
- });
124
+ );
115
125
 
116
126
  /**
117
127
  * DropdownDivider component for separating groups of items
@@ -123,254 +133,258 @@ export const DropdownDivider: React.FC<DropdownDividerProps> = memo(({ className
123
133
  /**
124
134
  * DropdownHeader component for section headers
125
135
  */
126
- export const DropdownHeader: React.FC<DropdownHeaderProps> = memo(({ children, className = '' }) => {
127
- return <li className={`c-dropdown__header ${className}`}>{children}</li>;
128
- });
136
+ export const DropdownHeader: React.FC<DropdownHeaderProps> = memo(
137
+ ({ children, className = '' }) => {
138
+ return <li className={`c-dropdown__header ${className}`}>{children}</li>;
139
+ }
140
+ );
129
141
 
130
142
  /**
131
143
  * Dropdown component for creating dropdown menus
132
144
  */
133
- export const Dropdown: React.FC<DropdownProps> = memo(({
134
- children,
135
- menu,
136
- placement = 'bottom-start',
137
- trigger = 'click',
138
- offset = DROPDOWN.DEFAULTS.OFFSET,
139
- isOpen: controlledIsOpen,
140
- onOpenChange,
141
- closeOnClickOutside = true,
142
- closeOnEscape = true,
143
- maxHeight,
144
- minWidth = DROPDOWN.DEFAULTS.MIN_WIDTH,
145
- variant,
146
- className = '',
147
- style,
148
- glass,
149
- ...props
150
- }) => {
151
- // Set up controlled vs uncontrolled state
152
- const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState(false);
153
- const isControlled = controlledIsOpen !== undefined;
154
- const isOpen = isControlled ? controlledIsOpen : uncontrolledIsOpen;
155
-
156
- // Create refs
157
- const dropdownRef = useRef<HTMLDivElement>(null);
158
- const toggleRef = useRef<HTMLDivElement>(null);
159
- const menuRef = useRef<HTMLDivElement>(null);
160
-
161
- // Generate unique ID
162
- const dropdownId = useRef(`dropdown-${Math.random().toString(36).substring(2, 9)}`).current;
163
-
164
- // State change handlers
165
- const setIsOpen = useCallback(
166
- (nextIsOpen: boolean) => {
167
- if (!isControlled) {
168
- setUncontrolledIsOpen(nextIsOpen);
169
- }
170
- if (onOpenChange) {
171
- onOpenChange(nextIsOpen);
172
- }
173
- },
174
- [isControlled, onOpenChange]
175
- );
176
-
177
- const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen, setIsOpen]);
178
-
179
- const close = useCallback(() => {
180
- setIsOpen(false);
181
- // Return focus to the toggle button after closing
182
- setTimeout(() => {
183
- toggleRef.current?.focus();
184
- }, 0);
185
- }, [setIsOpen]);
186
-
187
- // Click outside handler
188
- useEffect(() => {
189
- if (!isOpen || !closeOnClickOutside) return undefined;
190
-
191
- const handleClickOutside = (e: MouseEvent) => {
192
- if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
193
- close();
194
- }
195
- };
196
-
197
- document.addEventListener('mousedown', handleClickOutside);
198
- return () => document.removeEventListener('mousedown', handleClickOutside);
199
- }, [isOpen, closeOnClickOutside, close]);
145
+ export const Dropdown: React.FC<DropdownProps> = memo(
146
+ ({
147
+ children,
148
+ menu,
149
+ placement = 'bottom-start',
150
+ trigger = 'click',
151
+ offset = DROPDOWN.DEFAULTS.OFFSET,
152
+ isOpen: controlledIsOpen,
153
+ onOpenChange,
154
+ closeOnClickOutside = true,
155
+ closeOnEscape = true,
156
+ maxHeight,
157
+ minWidth = DROPDOWN.DEFAULTS.MIN_WIDTH,
158
+ variant,
159
+ className = '',
160
+ style,
161
+ glass,
162
+ ...props
163
+ }) => {
164
+ // Set up controlled vs uncontrolled state
165
+ const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState(false);
166
+ const isControlled = controlledIsOpen !== undefined;
167
+ const isOpen = isControlled ? controlledIsOpen : uncontrolledIsOpen;
168
+
169
+ // Create refs
170
+ const dropdownRef = useRef<HTMLDivElement>(null);
171
+ const toggleRef = useRef<HTMLDivElement>(null);
172
+ const menuRef = useRef<HTMLDivElement>(null);
173
+
174
+ // Generate unique ID
175
+ const dropdownId = useRef(`dropdown-${Math.random().toString(36).substring(2, 9)}`).current;
176
+
177
+ // State change handlers
178
+ const setIsOpen = useCallback(
179
+ (nextIsOpen: boolean) => {
180
+ if (!isControlled) {
181
+ setUncontrolledIsOpen(nextIsOpen);
182
+ }
183
+ if (onOpenChange) {
184
+ onOpenChange(nextIsOpen);
185
+ }
186
+ },
187
+ [isControlled, onOpenChange]
188
+ );
200
189
 
201
- // Escape key handler
202
- useEffect(() => {
203
- if (!isOpen || !closeOnEscape) return undefined;
190
+ const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen, setIsOpen]);
204
191
 
205
- const handleKeyDown = (e: KeyboardEvent) => {
206
- if (e.key === 'Escape') {
207
- close();
208
- }
209
- };
192
+ const close = useCallback(() => {
193
+ setIsOpen(false);
194
+ // Return focus to the toggle button after closing
195
+ setTimeout(() => {
196
+ toggleRef.current?.focus();
197
+ }, 0);
198
+ }, [setIsOpen]);
210
199
 
211
- document.addEventListener('keydown', handleKeyDown);
212
- return () => document.removeEventListener('keydown', handleKeyDown);
213
- }, [isOpen, closeOnEscape, close]);
200
+ // Click outside handler
201
+ useEffect(() => {
202
+ if (!isOpen || !closeOnClickOutside) return undefined;
214
203
 
215
- // Keyboard navigation
216
- const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
217
- if (!menuRef.current) return;
204
+ const handleClickOutside = (e: MouseEvent) => {
205
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
206
+ close();
207
+ }
208
+ };
218
209
 
219
- const focusableItems = menuRef.current.querySelectorAll<HTMLElement>(
220
- '[role="menuitem"]:not([disabled])'
221
- );
222
- if (!focusableItems.length) return;
210
+ document.addEventListener('mousedown', handleClickOutside);
211
+ return () => document.removeEventListener('mousedown', handleClickOutside);
212
+ }, [isOpen, closeOnClickOutside, close]);
223
213
 
224
- const currentIndex = Array.from(focusableItems).findIndex(
225
- item => item === document.activeElement
226
- );
214
+ // Escape key handler
215
+ useEffect(() => {
216
+ if (!isOpen || !closeOnEscape) return undefined;
227
217
 
228
- switch (e.key) {
229
- case 'ArrowDown':
230
- e.preventDefault();
231
- if (currentIndex < focusableItems.length - 1) {
232
- focusableItems[currentIndex + 1]?.focus();
233
- } else {
234
- focusableItems[0]?.focus();
218
+ const handleKeyDown = (e: KeyboardEvent) => {
219
+ if (e.key === 'Escape') {
220
+ close();
235
221
  }
236
- break;
222
+ };
223
+
224
+ document.addEventListener('keydown', handleKeyDown);
225
+ return () => document.removeEventListener('keydown', handleKeyDown);
226
+ }, [isOpen, closeOnEscape, close]);
227
+
228
+ // Keyboard navigation
229
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
230
+ if (!menuRef.current) return;
231
+
232
+ const focusableItems = menuRef.current.querySelectorAll<HTMLElement>(
233
+ '[role="menuitem"]:not([disabled])'
234
+ );
235
+ if (!focusableItems.length) return;
236
+
237
+ const currentIndex = Array.from(focusableItems).findIndex(
238
+ item => item === document.activeElement
239
+ );
240
+
241
+ switch (e.key) {
242
+ case 'ArrowDown':
243
+ e.preventDefault();
244
+ if (currentIndex < focusableItems.length - 1) {
245
+ focusableItems[currentIndex + 1]?.focus();
246
+ } else {
247
+ focusableItems[0]?.focus();
248
+ }
249
+ break;
250
+
251
+ case 'ArrowUp':
252
+ e.preventDefault();
253
+ if (currentIndex > 0) {
254
+ focusableItems[currentIndex - 1]?.focus();
255
+ } else {
256
+ focusableItems[focusableItems.length - 1]?.focus();
257
+ }
258
+ break;
259
+
260
+ case 'Home':
261
+ e.preventDefault();
262
+ focusableItems[0]?.focus();
263
+ break;
237
264
 
238
- case 'ArrowUp':
239
- e.preventDefault();
240
- if (currentIndex > 0) {
241
- focusableItems[currentIndex - 1]?.focus();
242
- } else {
265
+ case 'End':
266
+ e.preventDefault();
243
267
  focusableItems[focusableItems.length - 1]?.focus();
268
+ break;
269
+ }
270
+ }, []);
271
+
272
+ // Event handlers
273
+ const handleToggleClick = useCallback(
274
+ (e: React.MouseEvent) => {
275
+ if (trigger === 'click') {
276
+ e.preventDefault();
277
+ e.stopPropagation();
278
+ toggle();
244
279
  }
245
- break;
246
-
247
- case 'Home':
248
- e.preventDefault();
249
- focusableItems[0]?.focus();
250
- break;
251
-
252
- case 'End':
253
- e.preventDefault();
254
- focusableItems[focusableItems.length - 1]?.focus();
255
- break;
256
- }
257
- }, []);
280
+ },
281
+ [trigger, toggle]
282
+ );
258
283
 
259
- // Event handlers
260
- const handleToggleClick = useCallback(
261
- (e: React.MouseEvent) => {
262
- if (trigger === 'click') {
263
- e.preventDefault();
264
- e.stopPropagation();
265
- toggle();
266
- }
267
- },
268
- [trigger, toggle]
269
- );
284
+ const handleToggleKeyDown = useCallback(
285
+ (e: React.KeyboardEvent) => {
286
+ if ((e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') && !isOpen) {
287
+ e.preventDefault();
288
+ setIsOpen(true);
289
+
290
+ // Only focus the first menu item when using keyboard navigation
291
+ if (e.key === 'ArrowDown' && menuRef.current) {
292
+ setTimeout(() => {
293
+ const firstItem = menuRef.current?.querySelector<HTMLElement>('[role="menuitem"]');
294
+ firstItem?.focus();
295
+ }, 100);
296
+ }
297
+ } else if (e.key === 'Escape' && isOpen) {
298
+ e.preventDefault();
299
+ close();
300
+ }
301
+ },
302
+ [isOpen, setIsOpen, close]
303
+ );
270
304
 
271
- const handleToggleKeyDown = useCallback(
272
- (e: React.KeyboardEvent) => {
273
- if ((e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') && !isOpen) {
274
- e.preventDefault();
305
+ // Hover handlers for trigger="hover"
306
+ const handleHoverOpen = useCallback(() => {
307
+ if (trigger === 'hover') {
275
308
  setIsOpen(true);
276
-
277
- // Only focus the first menu item when using keyboard navigation
278
- if (e.key === 'ArrowDown' && menuRef.current) {
279
- setTimeout(() => {
280
- const firstItem = menuRef.current?.querySelector<HTMLElement>('[role="menuitem"]');
281
- firstItem?.focus();
282
- }, 100);
283
- }
284
- } else if (e.key === 'Escape' && isOpen) {
285
- e.preventDefault();
286
- close();
287
309
  }
288
- },
289
- [isOpen, setIsOpen, close]
290
- );
291
-
292
- // Hover handlers for trigger="hover"
293
- const handleHoverOpen = useCallback(() => {
294
- if (trigger === 'hover') {
295
- setIsOpen(true);
310
+ }, [trigger, setIsOpen]);
311
+
312
+ // Build class names
313
+ const dropdownClasses = [
314
+ 'c-dropdown',
315
+ trigger === 'click' ? 'c-dropdown--onclick' : '',
316
+ variant ? `c-dropdown--${variant}` : '',
317
+ isOpen ? 'is-open' : '',
318
+ glass ? 'c-dropdown--glass' : '',
319
+ className,
320
+ ]
321
+ .filter(Boolean)
322
+ .join(' ');
323
+
324
+ // Menu styles
325
+ const menuStyleProps: React.CSSProperties = {};
326
+ if (maxHeight) menuStyleProps.maxHeight = maxHeight;
327
+ if (minWidth !== undefined) {
328
+ menuStyleProps.minWidth = typeof minWidth === 'number' ? `${minWidth}px` : minWidth;
296
329
  }
297
- }, [trigger, setIsOpen]);
298
-
299
- // Build class names
300
- const dropdownClasses = [
301
- 'c-dropdown',
302
- trigger === 'click' ? 'c-dropdown--onclick' : '',
303
- variant ? `c-dropdown--${variant}` : '',
304
- isOpen ? 'is-open' : '',
305
- glass ? 'c-dropdown--glass' : '',
306
- className,
307
- ]
308
- .filter(Boolean)
309
- .join(' ');
310
-
311
- // Menu styles
312
- const menuStyleProps: React.CSSProperties = {};
313
- if (maxHeight) menuStyleProps.maxHeight = maxHeight;
314
- if (minWidth !== undefined) {
315
- menuStyleProps.minWidth = typeof minWidth === 'number' ? `${minWidth}px` : minWidth;
316
- }
317
330
 
318
- const menuContent = (
319
- <div className="c-dropdown__menu-inner" style={menuStyleProps}>
320
- <DropdownContext.Provider value={{ isOpen, close, id: dropdownId, trigger }}>
321
- <ul className={`c-dropdown__menu ${glass ? 'c-dropdown__menu--glass' : ''}`}>{menu}</ul>
322
- </DropdownContext.Provider>
323
- </div>
324
- );
325
-
326
- return (
327
- <div
328
- ref={dropdownRef}
329
- className={dropdownClasses}
330
- style={style}
331
- onMouseEnter={trigger === 'hover' ? handleHoverOpen : undefined}
332
- {...props}
333
- >
334
- <div
335
- ref={toggleRef}
336
- className="c-dropdown__toggle"
337
- onClick={handleToggleClick}
338
- onKeyDown={handleToggleKeyDown}
339
- aria-haspopup="menu"
340
- aria-expanded={isOpen}
341
- aria-controls={dropdownId}
342
- tabIndex={0}
343
- >
344
- {children}
331
+ const menuContent = (
332
+ <div className="c-dropdown__menu-inner" style={menuStyleProps}>
333
+ <DropdownContext.Provider value={{ isOpen, close, id: dropdownId, trigger }}>
334
+ <ul className={`c-dropdown__menu ${glass ? 'c-dropdown__menu--glass' : ''}`}>{menu}</ul>
335
+ </DropdownContext.Provider>
345
336
  </div>
337
+ );
346
338
 
339
+ return (
347
340
  <div
348
- ref={menuRef}
349
- id={dropdownId}
350
- className={`c-dropdown__menu-wrapper c-dropdown__menu-wrapper--${placement} ${isOpen ? 'is-open' : ''} ${glass ? 'is-glass' : ''}`}
351
- role="menu"
352
- aria-orientation="vertical"
353
- aria-hidden={!isOpen}
354
- onKeyDown={handleKeyDown}
341
+ ref={dropdownRef}
342
+ className={dropdownClasses}
343
+ style={style}
344
+ onMouseEnter={trigger === 'hover' ? handleHoverOpen : undefined}
345
+ {...props}
355
346
  >
356
- {glass
357
- ? // Default glass settings for dropdowns
358
- (() => {
359
- const defaultGlassProps = {
360
- displacementScale: 20,
361
- elasticity: 0,
362
- };
363
-
364
- const glassProps =
365
- glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
366
-
367
- return <AtomixGlass {...glassProps}>{menuContent}</AtomixGlass>;
368
- })()
369
- : menuContent}
347
+ <div
348
+ ref={toggleRef}
349
+ className="c-dropdown__toggle"
350
+ onClick={handleToggleClick}
351
+ onKeyDown={handleToggleKeyDown}
352
+ aria-haspopup="menu"
353
+ aria-expanded={isOpen}
354
+ aria-controls={dropdownId}
355
+ tabIndex={0}
356
+ >
357
+ {children}
358
+ </div>
359
+
360
+ <div
361
+ ref={menuRef}
362
+ id={dropdownId}
363
+ className={`c-dropdown__menu-wrapper c-dropdown__menu-wrapper--${placement} ${isOpen ? 'is-open' : ''} ${glass ? 'is-glass' : ''}`}
364
+ role="menu"
365
+ aria-orientation="vertical"
366
+ aria-hidden={!isOpen}
367
+ onKeyDown={handleKeyDown}
368
+ >
369
+ {glass
370
+ ? // Default glass settings for dropdowns
371
+ (() => {
372
+ const defaultGlassProps = {
373
+ displacementScale: 20,
374
+ elasticity: 0,
375
+ };
376
+
377
+ const glassProps =
378
+ glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
379
+
380
+ return <AtomixGlass {...glassProps}>{menuContent}</AtomixGlass>;
381
+ })()
382
+ : menuContent}
383
+ </div>
370
384
  </div>
371
- </div>
372
- );
373
- });
385
+ );
386
+ }
387
+ );
374
388
 
375
389
  export type { DropdownProps, DropdownItemProps, DropdownDividerProps, DropdownHeaderProps };
376
390