@shohojdhara/atomix 0.3.15 → 0.4.1

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 (276) hide show
  1. package/build-tools/index.d.ts +31 -30
  2. package/build-tools/package.json +4 -21
  3. package/dist/atomix.css +20234 -2027
  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 +4 -5
  10. package/dist/charts.js.map +1 -1
  11. package/dist/core.d.ts +87 -10
  12. package/dist/core.js +673 -480
  13. package/dist/core.js.map +1 -1
  14. package/dist/forms.d.ts +15 -3
  15. package/dist/forms.js +530 -97
  16. package/dist/forms.js.map +1 -1
  17. package/dist/heavy.js +5 -6
  18. package/dist/heavy.js.map +1 -1
  19. package/dist/index.d.ts +644 -277
  20. package/dist/index.esm.js +1948 -1347
  21. package/dist/index.esm.js.map +1 -1
  22. package/dist/index.js +3333 -2728
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.min.js +1 -1
  25. package/dist/index.min.js.map +1 -1
  26. package/dist/layout.js.map +1 -1
  27. package/dist/theme.d.ts +9 -9
  28. package/dist/theme.js.map +1 -1
  29. package/package.json +2 -2
  30. package/scripts/atomix-cli.js +10 -1
  31. package/scripts/cli/__tests__/utils.test.js +6 -2
  32. package/scripts/cli/migration-tools.js +2 -2
  33. package/scripts/cli/theme-bridge.js +7 -9
  34. package/scripts/cli/utils.js +2 -1
  35. package/src/components/Accordion/Accordion.stories.tsx +72 -23
  36. package/src/components/Accordion/Accordion.test.tsx +70 -50
  37. package/src/components/Accordion/Accordion.tsx +219 -96
  38. package/src/components/Accordion/AccordionCompound.test.tsx +70 -0
  39. package/src/components/AtomixGlass/AtomixGlass.test.tsx +1 -1
  40. package/src/components/AtomixGlass/GlassFilter.tsx +9 -16
  41. package/src/components/AtomixGlass/glass-utils.ts +4 -3
  42. package/src/components/AtomixGlass/shader-utils.ts +128 -52
  43. package/src/components/AtomixGlass/stories/Playground.stories.tsx +1 -1
  44. package/src/components/AtomixGlass/stories/Shaders.stories.tsx +1 -1
  45. package/src/components/Avatar/Avatar.stories.tsx +45 -62
  46. package/src/components/Avatar/Avatar.tsx +58 -56
  47. package/src/components/Badge/Badge.stories.tsx +20 -9
  48. package/src/components/Badge/Badge.test.tsx +41 -41
  49. package/src/components/Badge/Badge.tsx +64 -62
  50. package/src/components/Block/Block.stories.tsx +14 -4
  51. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +9 -8
  52. package/src/components/Breadcrumb/Breadcrumb.tsx +173 -65
  53. package/src/components/Breadcrumb/BreadcrumbCompound.test.tsx +84 -0
  54. package/src/components/Button/Button.stories.tsx +13 -22
  55. package/src/components/Button/Button.test.tsx +97 -81
  56. package/src/components/Button/Button.tsx +46 -14
  57. package/src/components/Button/ButtonGroup.stories.tsx +37 -32
  58. package/src/components/Button/ButtonGroup.tsx +4 -15
  59. package/src/components/Callout/Callout.stories.tsx +166 -918
  60. package/src/components/Callout/Callout.tsx +196 -84
  61. package/src/components/Callout/CalloutCompound.test.tsx +72 -0
  62. package/src/components/Card/Card.stories.tsx +67 -36
  63. package/src/components/Card/Card.tsx +30 -14
  64. package/src/components/Chart/AreaChart.tsx +1 -1
  65. package/src/components/Chart/CandlestickChart.tsx +23 -16
  66. package/src/components/Chart/Chart.stories.tsx +4 -9
  67. package/src/components/Chart/Chart.tsx +40 -44
  68. package/src/components/Chart/ChartRenderer.tsx +39 -12
  69. package/src/components/Chart/ChartToolbar.tsx +21 -5
  70. package/src/components/Chart/DonutChart.tsx +1 -1
  71. package/src/components/Chart/FunnelChart.tsx +4 -1
  72. package/src/components/Chart/GaugeChart.tsx +3 -1
  73. package/src/components/Chart/HeatmapChart.tsx +50 -37
  74. package/src/components/Chart/LineChart.tsx +3 -2
  75. package/src/components/Chart/MultiAxisChart.tsx +24 -16
  76. package/src/components/Chart/RadarChart.tsx +19 -17
  77. package/src/components/Chart/ScatterChart.tsx +29 -21
  78. package/src/components/ColorModeToggle/ColorModeToggle.stories.tsx +6 -2
  79. package/src/components/ColorModeToggle/ColorModeToggle.tsx +15 -3
  80. package/src/components/Countdown/Countdown.stories.tsx +7 -7
  81. package/src/components/DataTable/DataTable.stories.tsx +43 -38
  82. package/src/components/DataTable/DataTable.test.tsx +26 -148
  83. package/src/components/DataTable/DataTable.tsx +485 -456
  84. package/src/components/DatePicker/DatePicker.stories.tsx +32 -47
  85. package/src/components/DatePicker/DatePicker.tsx +31 -26
  86. package/src/components/Dropdown/Dropdown.stories.tsx +2 -5
  87. package/src/components/Dropdown/Dropdown.tsx +425 -298
  88. package/src/components/Dropdown/DropdownCompound.test.tsx +64 -0
  89. package/src/components/EdgePanel/EdgePanel.stories.tsx +6 -19
  90. package/src/components/EdgePanel/EdgePanel.tsx +163 -113
  91. package/src/components/EdgePanel/EdgePanelCompound.test.tsx +53 -0
  92. package/src/components/Footer/Footer.stories.tsx +21 -16
  93. package/src/components/Footer/Footer.tsx +130 -128
  94. package/src/components/Footer/FooterLink.tsx +2 -2
  95. package/src/components/Form/Checkbox.test.tsx +49 -49
  96. package/src/components/Form/Checkbox.tsx +108 -100
  97. package/src/components/Form/Form.stories.tsx +2 -10
  98. package/src/components/Form/Input.stories.tsx +22 -39
  99. package/src/components/Form/Input.test.tsx +38 -44
  100. package/src/components/Form/Radio.stories.tsx +6 -12
  101. package/src/components/Form/Radio.tsx +68 -66
  102. package/src/components/Form/Select.stories.tsx +23 -0
  103. package/src/components/Form/Select.test.tsx +99 -0
  104. package/src/components/Form/Select.tsx +239 -186
  105. package/src/components/Form/SelectOption.tsx +88 -0
  106. package/src/components/Form/Textarea.test.tsx +27 -32
  107. package/src/components/Hero/Hero.stories.tsx +93 -23
  108. package/src/components/Hero/Hero.test.tsx +142 -0
  109. package/src/components/Hero/Hero.tsx +343 -58
  110. package/src/components/Icon/index.ts +7 -1
  111. package/src/components/List/List.test.tsx +62 -0
  112. package/src/components/List/List.tsx +32 -25
  113. package/src/components/List/ListItem.tsx +20 -0
  114. package/src/components/Modal/Modal.stories.tsx +67 -2
  115. package/src/components/Modal/Modal.tsx +208 -125
  116. package/src/components/Modal/ModalCompound.test.tsx +94 -0
  117. package/src/components/Navigation/Menu/MegaMenu.tsx +70 -70
  118. package/src/components/Navigation/Nav/NavDropdown.tsx +1 -5
  119. package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +128 -28
  120. package/src/components/Navigation/SideMenu/SideMenu.tsx +5 -7
  121. package/src/components/Navigation/SideMenu/SideMenuItem.tsx +4 -5
  122. package/src/components/Pagination/Pagination.stories.tsx +7 -4
  123. package/src/components/Pagination/Pagination.tsx +199 -202
  124. package/src/components/PhotoViewer/PhotoViewer.tsx +4 -1
  125. package/src/components/Popover/Popover.stories.tsx +99 -192
  126. package/src/components/Popover/Popover.tsx +41 -37
  127. package/src/components/Progress/Progress.stories.tsx +35 -44
  128. package/src/components/River/River.stories.tsx +2 -1
  129. package/src/components/SectionIntro/SectionIntro.stories.tsx +71 -71
  130. package/src/components/Slider/Slider.stories.tsx +12 -4
  131. package/src/components/Spinner/Spinner.stories.tsx +3 -1
  132. package/src/components/Spinner/Spinner.test.tsx +23 -23
  133. package/src/components/Spinner/Spinner.tsx +43 -46
  134. package/src/components/Steps/Steps.stories.tsx +8 -6
  135. package/src/components/Steps/Steps.tsx +124 -21
  136. package/src/components/Steps/StepsCompound.test.tsx +81 -0
  137. package/src/components/Tabs/Tabs.stories.tsx +12 -9
  138. package/src/components/Tabs/Tabs.tsx +230 -75
  139. package/src/components/Tabs/TabsCompound.test.tsx +64 -0
  140. package/src/components/Toggle/Toggle.stories.tsx +27 -13
  141. package/src/components/Toggle/Toggle.test.tsx +65 -70
  142. package/src/components/Toggle/Toggle.tsx +4 -1
  143. package/src/components/Tooltip/Tooltip.stories.tsx +24 -20
  144. package/src/components/Tooltip/Tooltip.tsx +104 -106
  145. package/src/components/Upload/Upload.stories.tsx +129 -127
  146. package/src/components/Upload/Upload.tsx +287 -283
  147. package/src/components/VideoPlayer/VideoPlayer.tsx +6 -1
  148. package/src/components/index.ts +13 -2
  149. package/src/layouts/Grid/Grid.stories.tsx +9 -3
  150. package/src/layouts/MasonryGrid/MasonryGrid.tsx +5 -1
  151. package/src/lib/__tests__/theme-tools.test.ts +32 -6
  152. package/src/lib/composables/index.ts +0 -4
  153. package/src/lib/composables/shared-mouse-tracker.ts +13 -14
  154. package/src/lib/composables/useAtomixGlass.ts +102 -60
  155. package/src/lib/composables/useChartExport.ts +1 -1
  156. package/src/lib/composables/useDataTable.ts +29 -17
  157. package/src/lib/composables/useHero.ts +58 -14
  158. package/src/lib/composables/useHeroBackgroundSlider.ts +2 -9
  159. package/src/lib/composables/useInput.ts +10 -8
  160. package/src/lib/composables/useSideMenu.ts +6 -5
  161. package/src/lib/composables/useTooltip.ts +1 -2
  162. package/src/lib/composables/useVideoPlayer.ts +44 -35
  163. package/src/lib/config/index.ts +154 -154
  164. package/src/lib/constants/cssVariables.ts +29 -29
  165. package/src/lib/hooks/__tests__/useComponentCustomization.test.ts +2 -6
  166. package/src/lib/hooks/index.ts +1 -1
  167. package/src/lib/hooks/useComponentCustomization.ts +11 -17
  168. package/src/lib/hooks/usePerformanceMonitor.ts +6 -7
  169. package/src/lib/patterns/__tests__/slots.test.ts +1 -1
  170. package/src/lib/patterns/index.ts +1 -1
  171. package/src/lib/patterns/slots.tsx +8 -13
  172. package/src/lib/storybook/InteractiveDemo.tsx +13 -18
  173. package/src/lib/storybook/PreviewContainer.tsx +1 -1
  174. package/src/lib/storybook/VariantsGrid.tsx +3 -7
  175. package/src/lib/storybook/index.ts +1 -1
  176. package/src/lib/theme/adapters/cssVariableMapper.ts +47 -74
  177. package/src/lib/theme/adapters/index.ts +3 -9
  178. package/src/lib/theme/adapters/themeAdapter.ts +41 -26
  179. package/src/lib/theme/config/index.ts +1 -1
  180. package/src/lib/theme/config/types.ts +2 -2
  181. package/src/lib/theme/config/validator.ts +10 -5
  182. package/src/lib/theme/constants/constants.ts +2 -2
  183. package/src/lib/theme/constants/index.ts +1 -2
  184. package/src/lib/theme/core/__tests__/createTheme.test.ts +20 -22
  185. package/src/lib/theme/core/composeTheme.ts +32 -26
  186. package/src/lib/theme/core/createTheme.ts +1 -1
  187. package/src/lib/theme/core/createThemeObject.ts +308 -301
  188. package/src/lib/theme/core/index.ts +3 -3
  189. package/src/lib/theme/devtools/CLI.ts +105 -111
  190. package/src/lib/theme/devtools/Comparator.tsx +50 -32
  191. package/src/lib/theme/devtools/DesignTokensCustomizer.stories.tsx +50 -48
  192. package/src/lib/theme/devtools/DesignTokensCustomizer.tsx +257 -63
  193. package/src/lib/theme/devtools/Inspector.tsx +75 -60
  194. package/src/lib/theme/devtools/LiveEditor.tsx +97 -76
  195. package/src/lib/theme/devtools/Preview.tsx +150 -106
  196. package/src/lib/theme/devtools/ThemeValidator.ts +29 -21
  197. package/src/lib/theme/devtools/index.ts +3 -9
  198. package/src/lib/theme/devtools/useHistory.ts +23 -21
  199. package/src/lib/theme/errors/errors.ts +12 -11
  200. package/src/lib/theme/errors/index.ts +2 -7
  201. package/src/lib/theme/generators/generateCSS.ts +9 -13
  202. package/src/lib/theme/generators/generateCSSNested.ts +1 -6
  203. package/src/lib/theme/generators/generateCSSVariables.ts +673 -630
  204. package/src/lib/theme/generators/index.ts +1 -4
  205. package/src/lib/theme/i18n/index.ts +1 -1
  206. package/src/lib/theme/i18n/rtl.ts +13 -13
  207. package/src/lib/theme/index.ts +7 -16
  208. package/src/lib/theme/runtime/ThemeApplicator.ts +4 -4
  209. package/src/lib/theme/runtime/ThemeContext.tsx +1 -1
  210. package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +19 -23
  211. package/src/lib/theme/runtime/ThemeProvider.tsx +230 -239
  212. package/src/lib/theme/runtime/__tests__/ThemeProvider.integration.test.tsx +1 -1
  213. package/src/lib/theme/runtime/__tests__/ThemeProvider.test.tsx +24 -29
  214. package/src/lib/theme/runtime/index.ts +2 -5
  215. package/src/lib/theme/runtime/useTheme.ts +18 -18
  216. package/src/lib/theme/runtime/useThemeTokens.ts +22 -22
  217. package/src/lib/theme/test/testTheme.ts +15 -16
  218. package/src/lib/theme/tokens/index.ts +2 -7
  219. package/src/lib/theme/tokens/tokens.ts +25 -24
  220. package/src/lib/theme/types.ts +428 -411
  221. package/src/lib/theme/utils/__tests__/themeValidation.test.ts +3 -3
  222. package/src/lib/theme/utils/componentTheming.ts +18 -18
  223. package/src/lib/theme/utils/domUtils.ts +277 -289
  224. package/src/lib/theme/utils/index.ts +1 -2
  225. package/src/lib/theme/utils/injectCSS.ts +10 -14
  226. package/src/lib/theme/utils/naming.ts +20 -16
  227. package/src/lib/theme/utils/themeHelpers.ts +10 -12
  228. package/src/lib/theme/utils/themeUtils.ts +85 -86
  229. package/src/lib/theme/utils/themeValidation.ts +82 -33
  230. package/src/lib/theme-tools.ts +8 -6
  231. package/src/lib/types/components.ts +180 -73
  232. package/src/lib/types/partProps.ts +1 -1
  233. package/src/lib/utils/__tests__/componentUtils.test.ts +57 -2
  234. package/src/lib/utils/__tests__/csv.test.ts +1 -1
  235. package/src/lib/utils/__tests__/themeNaming.test.ts +117 -0
  236. package/src/lib/utils/componentUtils.ts +8 -12
  237. package/src/lib/utils/csv.ts +3 -1
  238. package/src/lib/utils/dataTableExport.ts +1 -5
  239. package/src/lib/utils/fontPreloader.ts +10 -19
  240. package/src/lib/utils/icons.ts +4 -1
  241. package/src/lib/utils/index.ts +2 -6
  242. package/src/lib/utils/memoryMonitor.ts +10 -8
  243. package/src/lib/utils/themeNaming.ts +3 -3
  244. package/src/styles/01-settings/_index.scss +0 -1
  245. package/src/styles/01-settings/_settings.colors.scss +8 -8
  246. package/src/styles/01-settings/_settings.design-tokens.scss +61 -50
  247. package/src/styles/01-settings/_settings.navbar.scss +1 -1
  248. package/src/styles/01-settings/_settings.spacing.scss +3 -4
  249. package/src/styles/01-settings/_settings.tooltip.scss +1 -1
  250. package/src/styles/01-settings/_settings.typography.scss +1 -1
  251. package/src/styles/02-tools/_tools.breakpoints.scss +1 -1
  252. package/src/styles/02-tools/_tools.button.scss +51 -21
  253. package/src/styles/02-tools/_tools.utility-api.scss +36 -24
  254. package/src/styles/03-generic/_generic.root.scss +4 -3
  255. package/src/styles/06-components/_components.atomix-glass.scss +13 -9
  256. package/src/styles/06-components/_components.button.scss +16 -4
  257. package/src/styles/06-components/_components.callout.scss +27 -21
  258. package/src/styles/06-components/_components.card.scss +5 -14
  259. package/src/styles/06-components/_components.chart.scss +22 -19
  260. package/src/styles/06-components/_components.checkbox.scss +3 -1
  261. package/src/styles/06-components/_components.color-mode-toggle.scss +3 -1
  262. package/src/styles/06-components/_components.edge-panel.scss +9 -2
  263. package/src/styles/06-components/_components.footer.scss +1 -1
  264. package/src/styles/06-components/_components.side-menu.scss +5 -5
  265. package/src/styles/06-components/_components.toggle.scss +18 -0
  266. package/src/styles/06-components/_index.scss +1 -1
  267. package/src/styles/06-components/old.chart.styles.scss +0 -2
  268. package/src/styles/99-utilities/_utilities.border.scss +69 -27
  269. package/src/styles/99-utilities/_utilities.display.scss +1 -1
  270. package/src/styles/99-utilities/_utilities.opacity.scss +10 -0
  271. package/src/styles/99-utilities/_utilities.position.scss +16 -9
  272. package/src/styles/99-utilities/_utilities.scss +1 -1
  273. package/src/styles/99-utilities/_utilities.sizes.scss +47 -18
  274. package/src/styles/99-utilities/_utilities.spacing.scss +118 -66
  275. package/src/styles/99-utilities/_utilities.text-gradient.scss +30 -30
  276. package/src/styles/99-utilities/_utilities.text.scss +67 -47
@@ -1,4 +1,14 @@
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
+ forwardRef,
10
+ ReactNode,
11
+ } from 'react';
2
12
  import { DROPDOWN } from '../../lib/constants/components';
3
13
  import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
4
14
  import type {
@@ -6,6 +16,7 @@ import type {
6
16
  DropdownItemProps,
7
17
  DropdownDividerProps,
8
18
  DropdownHeaderProps,
19
+ AtomixGlassProps,
9
20
  } from '../../lib/types/components';
10
21
 
11
22
  // Context type definition
@@ -24,94 +35,144 @@ const DropdownContext = createContext<DropdownContextType>({
24
35
  trigger: 'click',
25
36
  });
26
37
 
38
+ // Compound Components
39
+
40
+ export const DropdownMenu = forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(
41
+ ({ children, className = '', ...props }, ref) => {
42
+ const { glass } = useContext(DropdownStyleContext); // We need to access glass prop here?
43
+ // Wait, the original code wrapped <ul> in Context Provider.
44
+ // And applied glass wrapper around <ul>.
45
+ // If we use Compound Component, DropdownMenu should be the list.
46
+
47
+ return (
48
+ <ul
49
+ ref={ref}
50
+ className={`c-dropdown__menu ${glass ? 'c-dropdown__menu--glass' : ''} ${className}`.trim()}
51
+ {...props}
52
+ >
53
+ {children}
54
+ </ul>
55
+ );
56
+ }
57
+ );
58
+ DropdownMenu.displayName = 'DropdownMenu';
59
+
60
+ export const DropdownTrigger = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
61
+ ({ children, className = '', onClick, onKeyDown, ...props }, ref) => {
62
+ // We need to inject the trigger logic here.
63
+ // But triggers are usually handled by the parent Dropdown in the original code.
64
+ // The original code wraps children in `c-dropdown__toggle` div.
65
+
66
+ // Ideally, DropdownTrigger allows user to customize the trigger element.
67
+ // For backward compat, Dropdown wraps `children` (legacy) in `c-dropdown__toggle`.
68
+
69
+ // If we use <Dropdown.Trigger><Button/></Dropdown.Trigger>, we want the Button to be the trigger.
70
+
71
+ return (
72
+ <div
73
+ ref={ref}
74
+ className={`c-dropdown__toggle ${className}`.trim()}
75
+ onClick={onClick}
76
+ onKeyDown={onKeyDown}
77
+ {...props}
78
+ >
79
+ {children}
80
+ </div>
81
+ );
82
+ }
83
+ );
84
+ DropdownTrigger.displayName = 'DropdownTrigger';
85
+
27
86
  /**
28
87
  * DropdownItem component for menu items
29
88
  */
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
- }
89
+ export const DropdownItem: React.FC<DropdownItemProps> = memo(
90
+ ({
91
+ children,
92
+ href,
93
+ active = false,
94
+ disabled = false,
95
+ icon,
96
+ onClick,
97
+ className = '',
98
+ LinkComponent,
99
+ ...props
100
+ }) => {
101
+ const { close } = useContext(DropdownContext);
102
+
103
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
104
+ if (disabled) {
105
+ e.preventDefault();
106
+ return;
107
+ }
48
108
 
49
- if (onClick) {
50
- onClick(e);
51
- }
109
+ if (onClick) {
110
+ onClick(e);
111
+ }
52
112
 
53
- // Always close the dropdown when an item is clicked
54
- close();
55
- };
113
+ // Always close the dropdown when an item is clicked
114
+ close();
115
+ };
56
116
 
57
- const itemClasses = [
58
- 'c-dropdown__menu-item',
59
- active ? 'is-active' : '',
60
- disabled ? 'is-disabled' : '',
61
- className,
62
- ]
63
- .filter(Boolean)
64
- .join(' ');
117
+ const itemClasses = [
118
+ 'c-dropdown__menu-item',
119
+ active ? 'is-active' : '',
120
+ disabled ? 'is-disabled' : '',
121
+ className,
122
+ ]
123
+ .filter(Boolean)
124
+ .join(' ');
125
+
126
+ const linkProps = {
127
+ href,
128
+ className: itemClasses,
129
+ onClick: handleClick,
130
+ role: 'menuitem',
131
+ tabIndex: 0,
132
+ ...props,
133
+ };
134
+
135
+ if (href && !disabled) {
136
+ return (
137
+ <li>
138
+ {LinkComponent ? (
139
+ (() => {
140
+ const Component = LinkComponent as React.ComponentType<any>;
141
+ return (
142
+ <Component {...linkProps}>
143
+ {icon && <span className="c-dropdown__menu-item-icon">{icon}</span>}
144
+ {children}
145
+ </Component>
146
+ );
147
+ })()
148
+ ) : (
149
+ <a {...linkProps}>
150
+ {icon && <span className="c-dropdown__menu-item-icon">{icon}</span>}
151
+ {children}
152
+ </a>
153
+ )}
154
+ </li>
155
+ );
156
+ }
65
157
 
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
158
  return (
77
159
  <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
- )}
160
+ <button
161
+ type="button"
162
+ className={itemClasses}
163
+ onClick={handleClick}
164
+ disabled={disabled}
165
+ role="menuitem"
166
+ tabIndex={0}
167
+ {...props}
168
+ >
169
+ {icon && <span className="c-dropdown__menu-item-icon">{icon}</span>}
170
+ {children}
171
+ </button>
94
172
  </li>
95
173
  );
96
174
  }
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
- });
175
+ );
115
176
 
116
177
  /**
117
178
  * DropdownDivider component for separating groups of items
@@ -123,260 +184,326 @@ export const DropdownDivider: React.FC<DropdownDividerProps> = memo(({ className
123
184
  /**
124
185
  * DropdownHeader component for section headers
125
186
  */
126
- export const DropdownHeader: React.FC<DropdownHeaderProps> = memo(({ children, className = '' }) => {
127
- return <li className={`c-dropdown__header ${className}`}>{children}</li>;
128
- });
187
+ export const DropdownHeader: React.FC<DropdownHeaderProps> = memo(
188
+ ({ children, className = '' }) => {
189
+ return <li className={`c-dropdown__header ${className}`}>{children}</li>;
190
+ }
191
+ );
192
+
193
+ // Helper context to pass glass prop to DropdownMenu
194
+ const DropdownStyleContext = createContext<{ glass?: AtomixGlassProps | boolean }>({});
129
195
 
130
196
  /**
131
197
  * Dropdown component for creating dropdown menus
132
198
  */
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
- };
199
+ type DropdownComponent = React.FC<DropdownProps> & {
200
+ Trigger: typeof DropdownTrigger;
201
+ Menu: typeof DropdownMenu;
202
+ Item: typeof DropdownItem;
203
+ Divider: typeof DropdownDivider;
204
+ Header: typeof DropdownHeader;
205
+ };
196
206
 
197
- document.addEventListener('mousedown', handleClickOutside);
198
- return () => document.removeEventListener('mousedown', handleClickOutside);
199
- }, [isOpen, closeOnClickOutside, close]);
207
+ export const Dropdown: DropdownComponent = memo(
208
+ ({
209
+ children,
210
+ menu,
211
+ placement = 'bottom-start',
212
+ trigger = 'click',
213
+ offset = DROPDOWN.DEFAULTS.OFFSET,
214
+ isOpen: controlledIsOpen,
215
+ onOpenChange,
216
+ closeOnClickOutside = true,
217
+ closeOnEscape = true,
218
+ maxHeight,
219
+ minWidth = DROPDOWN.DEFAULTS.MIN_WIDTH,
220
+ variant,
221
+ className = '',
222
+ style,
223
+ glass,
224
+ ...props
225
+ }: DropdownProps) => {
226
+ // Set up controlled vs uncontrolled state
227
+ const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState(false);
228
+ const isControlled = controlledIsOpen !== undefined;
229
+ const isOpen = isControlled ? controlledIsOpen : uncontrolledIsOpen;
230
+
231
+ // Create refs
232
+ const dropdownRef = useRef<HTMLDivElement>(null);
233
+ const toggleRef = useRef<HTMLDivElement>(null);
234
+ const menuRef = useRef<HTMLDivElement>(null);
235
+
236
+ // Generate unique ID
237
+ const dropdownId = useRef(`dropdown-${Math.random().toString(36).substring(2, 9)}`).current;
238
+
239
+ // State change handlers
240
+ const setIsOpen = useCallback(
241
+ (nextIsOpen: boolean) => {
242
+ if (!isControlled) {
243
+ setUncontrolledIsOpen(nextIsOpen);
244
+ }
245
+ if (onOpenChange) {
246
+ onOpenChange(nextIsOpen);
247
+ }
248
+ },
249
+ [isControlled, onOpenChange]
250
+ );
200
251
 
201
- // Escape key handler
202
- useEffect(() => {
203
- if (!isOpen || !closeOnEscape) return undefined;
252
+ const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen, setIsOpen]);
204
253
 
205
- const handleKeyDown = (e: KeyboardEvent) => {
206
- if (e.key === 'Escape') {
207
- close();
208
- }
209
- };
254
+ const close = useCallback(() => {
255
+ setIsOpen(false);
256
+ // Return focus to the toggle button after closing
257
+ setTimeout(() => {
258
+ toggleRef.current?.focus();
259
+ }, 0);
260
+ }, [setIsOpen]);
210
261
 
211
- document.addEventListener('keydown', handleKeyDown);
212
- return () => document.removeEventListener('keydown', handleKeyDown);
213
- }, [isOpen, closeOnEscape, close]);
262
+ // Click outside handler
263
+ useEffect(() => {
264
+ if (!isOpen || !closeOnClickOutside) return undefined;
214
265
 
215
- // Keyboard navigation
216
- const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
217
- if (!menuRef.current) return;
266
+ const handleClickOutside = (e: MouseEvent) => {
267
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
268
+ close();
269
+ }
270
+ };
218
271
 
219
- const focusableItems = menuRef.current.querySelectorAll<HTMLElement>(
220
- '[role="menuitem"]:not([disabled])'
221
- );
222
- if (!focusableItems.length) return;
272
+ document.addEventListener('mousedown', handleClickOutside);
273
+ return () => document.removeEventListener('mousedown', handleClickOutside);
274
+ }, [isOpen, closeOnClickOutside, close]);
223
275
 
224
- const currentIndex = Array.from(focusableItems).findIndex(
225
- item => item === document.activeElement
226
- );
276
+ // Escape key handler
277
+ useEffect(() => {
278
+ if (!isOpen || !closeOnEscape) return undefined;
227
279
 
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();
280
+ const handleKeyDown = (e: KeyboardEvent) => {
281
+ if (e.key === 'Escape') {
282
+ close();
235
283
  }
236
- break;
284
+ };
285
+
286
+ document.addEventListener('keydown', handleKeyDown);
287
+ return () => document.removeEventListener('keydown', handleKeyDown);
288
+ }, [isOpen, closeOnEscape, close]);
289
+
290
+ // Keyboard navigation
291
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
292
+ if (!menuRef.current) return;
293
+
294
+ const focusableItems = menuRef.current.querySelectorAll<HTMLElement>(
295
+ '[role="menuitem"]:not([disabled])'
296
+ );
297
+ if (!focusableItems.length) return;
298
+
299
+ const currentIndex = Array.from(focusableItems).findIndex(
300
+ item => item === document.activeElement
301
+ );
302
+
303
+ switch (e.key) {
304
+ case 'ArrowDown':
305
+ e.preventDefault();
306
+ if (currentIndex < focusableItems.length - 1) {
307
+ focusableItems[currentIndex + 1]?.focus();
308
+ } else {
309
+ focusableItems[0]?.focus();
310
+ }
311
+ break;
312
+
313
+ case 'ArrowUp':
314
+ e.preventDefault();
315
+ if (currentIndex > 0) {
316
+ focusableItems[currentIndex - 1]?.focus();
317
+ } else {
318
+ focusableItems[focusableItems.length - 1]?.focus();
319
+ }
320
+ break;
321
+
322
+ case 'Home':
323
+ e.preventDefault();
324
+ focusableItems[0]?.focus();
325
+ break;
237
326
 
238
- case 'ArrowUp':
239
- e.preventDefault();
240
- if (currentIndex > 0) {
241
- focusableItems[currentIndex - 1]?.focus();
242
- } else {
327
+ case 'End':
328
+ e.preventDefault();
243
329
  focusableItems[focusableItems.length - 1]?.focus();
330
+ break;
331
+ }
332
+ }, []);
333
+
334
+ // Event handlers
335
+ const handleToggleClick = useCallback(
336
+ (e: React.MouseEvent) => {
337
+ if (trigger === 'click') {
338
+ e.preventDefault();
339
+ e.stopPropagation();
340
+ toggle();
244
341
  }
245
- break;
342
+ },
343
+ [trigger, toggle]
344
+ );
246
345
 
247
- case 'Home':
248
- e.preventDefault();
249
- focusableItems[0]?.focus();
250
- break;
346
+ const handleToggleKeyDown = useCallback(
347
+ (e: React.KeyboardEvent) => {
348
+ if ((e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') && !isOpen) {
349
+ e.preventDefault();
350
+ setIsOpen(true);
351
+
352
+ // Only focus the first menu item when using keyboard navigation
353
+ if (e.key === 'ArrowDown' && menuRef.current) {
354
+ setTimeout(() => {
355
+ const firstItem = menuRef.current?.querySelector<HTMLElement>('[role="menuitem"]');
356
+ firstItem?.focus();
357
+ }, 100);
358
+ }
359
+ } else if (e.key === 'Escape' && isOpen) {
360
+ e.preventDefault();
361
+ close();
362
+ }
363
+ },
364
+ [isOpen, setIsOpen, close]
365
+ );
251
366
 
252
- case 'End':
253
- e.preventDefault();
254
- focusableItems[focusableItems.length - 1]?.focus();
255
- break;
367
+ // Hover handlers for trigger="hover"
368
+ const handleHoverOpen = useCallback(() => {
369
+ if (trigger === 'hover') {
370
+ setIsOpen(true);
371
+ }
372
+ }, [trigger, setIsOpen]);
373
+
374
+ // Build class names
375
+ const dropdownClasses = [
376
+ 'c-dropdown',
377
+ trigger === 'click' ? 'c-dropdown--onclick' : '',
378
+ variant ? `c-dropdown--${variant}` : '',
379
+ isOpen ? 'is-open' : '',
380
+ glass ? 'c-dropdown--glass' : '',
381
+ className,
382
+ ]
383
+ .filter(Boolean)
384
+ .join(' ');
385
+
386
+ // Menu styles
387
+ const menuStyleProps: React.CSSProperties = {};
388
+ if (maxHeight) menuStyleProps.maxHeight = maxHeight;
389
+ if (minWidth !== undefined) {
390
+ menuStyleProps.minWidth = typeof minWidth === 'number' ? `${minWidth}px` : minWidth;
256
391
  }
257
- }, []);
258
392
 
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
- );
393
+ // Determine content structure
394
+ // Legacy: menu prop + children as trigger
395
+ // Compound: children contains Trigger and Menu
270
396
 
271
- const handleToggleKeyDown = useCallback(
272
- (e: React.KeyboardEvent) => {
273
- if ((e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') && !isOpen) {
274
- e.preventDefault();
275
- setIsOpen(true);
397
+ const hasCompoundComponents = React.Children.toArray(children).some((child) =>
398
+ React.isValidElement(child) &&
399
+ ['DropdownTrigger', 'DropdownMenu'].includes((child.type as any).displayName)
400
+ );
276
401
 
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);
402
+ let triggerContent: ReactNode;
403
+ let menuContentNode: ReactNode;
404
+
405
+ if (hasCompoundComponents) {
406
+ // Find Trigger and Menu in children
407
+ React.Children.forEach(children, (child) => {
408
+ if (React.isValidElement(child)) {
409
+ if ((child.type as any).displayName === 'DropdownTrigger') {
410
+ triggerContent = React.cloneElement(child, {
411
+ ref: toggleRef,
412
+ onClick: (e: React.MouseEvent) => {
413
+ handleToggleClick(e);
414
+ (child.props as any).onClick?.(e);
415
+ },
416
+ onKeyDown: (e: React.KeyboardEvent) => {
417
+ handleToggleKeyDown(e);
418
+ (child.props as any).onKeyDown?.(e);
419
+ },
420
+ 'aria-haspopup': 'menu',
421
+ 'aria-expanded': isOpen,
422
+ 'aria-controls': dropdownId,
423
+ tabIndex: 0,
424
+ } as any);
425
+ } else if ((child.type as any).displayName === 'DropdownMenu') {
426
+ menuContentNode = child;
427
+ }
283
428
  }
284
- } else if (e.key === 'Escape' && isOpen) {
285
- e.preventDefault();
286
- close();
287
- }
288
- },
289
- [isOpen, setIsOpen, close]
290
- );
291
-
292
- // Hover handlers for trigger="hover"
293
- const handleHoverOpen = useCallback(() => {
294
- if (trigger === 'hover') {
295
- setIsOpen(true);
429
+ });
430
+ } else {
431
+ // Legacy mode
432
+ triggerContent = (
433
+ <div
434
+ ref={toggleRef}
435
+ className="c-dropdown__toggle"
436
+ onClick={handleToggleClick}
437
+ onKeyDown={handleToggleKeyDown}
438
+ aria-haspopup="menu"
439
+ aria-expanded={isOpen}
440
+ aria-controls={dropdownId}
441
+ tabIndex={0}
442
+ >
443
+ {children}
444
+ </div>
445
+ );
446
+ menuContentNode = (
447
+ <ul className={`c-dropdown__menu ${glass ? 'c-dropdown__menu--glass' : ''}`}>{menu}</ul>
448
+ );
296
449
  }
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
450
 
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}
451
+ const menuContent = (
452
+ <div className="c-dropdown__menu-inner" style={menuStyleProps}>
453
+ <DropdownStyleContext.Provider value={{ glass }}>
454
+ <DropdownContext.Provider value={{ isOpen, close, id: dropdownId, trigger }}>
455
+ {menuContentNode}
456
+ </DropdownContext.Provider>
457
+ </DropdownStyleContext.Provider>
345
458
  </div>
459
+ );
346
460
 
461
+ return (
347
462
  <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}
463
+ ref={dropdownRef}
464
+ className={dropdownClasses}
465
+ style={style}
466
+ onMouseEnter={trigger === 'hover' ? handleHoverOpen : undefined}
467
+ {...props}
355
468
  >
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}
469
+ {triggerContent}
470
+
471
+ <div
472
+ ref={menuRef}
473
+ id={dropdownId}
474
+ className={`c-dropdown__menu-wrapper c-dropdown__menu-wrapper--${placement} ${isOpen ? 'is-open' : ''} ${glass ? 'is-glass' : ''}`}
475
+ role="menu"
476
+ aria-orientation="vertical"
477
+ aria-hidden={!isOpen}
478
+ onKeyDown={handleKeyDown}
479
+ >
480
+ {glass
481
+ ? // Default glass settings for dropdowns
482
+ (() => {
483
+ const defaultGlassProps = {
484
+ displacementScale: 20,
485
+ elasticity: 0,
486
+ };
487
+
488
+ const glassProps =
489
+ glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
490
+
491
+ return <AtomixGlass {...glassProps}>{menuContent}</AtomixGlass>;
492
+ })()
493
+ : menuContent}
494
+ </div>
370
495
  </div>
371
- </div>
372
- );
373
- });
496
+ );
497
+ }
498
+ ) as unknown as DropdownComponent;
374
499
 
375
500
  export type { DropdownProps, DropdownItemProps, DropdownDividerProps, DropdownHeaderProps };
376
501
 
377
502
  Dropdown.displayName = 'Dropdown';
378
- DropdownItem.displayName = 'DropdownItem';
379
- DropdownDivider.displayName = 'DropdownDivider';
380
- DropdownHeader.displayName = 'DropdownHeader';
503
+ Dropdown.Trigger = DropdownTrigger;
504
+ Dropdown.Menu = DropdownMenu;
505
+ Dropdown.Item = DropdownItem;
506
+ Dropdown.Divider = DropdownDivider;
507
+ Dropdown.Header = DropdownHeader;
381
508
 
382
509
  export default Dropdown;