@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,36 +1,43 @@
1
1
  import React, { memo } from 'react';
2
2
  import { ListProps } from '../../lib/types/components';
3
3
  import { LIST } from '../../lib/constants/components';
4
+ import { ListItem } from './ListItem';
4
5
 
5
- export const List: React.FC<ListProps> = memo(({
6
- children,
7
- variant = 'default',
8
- className = '',
9
- style,
10
- ...props
11
- }) => {
12
- // Generate CSS classes
13
- const listClasses = [LIST.BASE_CLASS, variant !== 'default' && `c-list--${variant}`, className]
14
- .filter(Boolean)
15
- .join(' ');
6
+ export type { ListProps };
16
7
 
17
- // Determine the HTML element based on variant
18
- const ListElement = ['number', 'text'].includes(variant) ? 'ol' : 'ul';
8
+ export type ListComponent = React.FC<ListProps> & {
9
+ Item: typeof ListItem;
10
+ };
19
11
 
20
- return (
21
- <ListElement className={listClasses} style={style} {...props}>
22
- {React.Children.map(children, child => {
23
- if (React.isValidElement(child)) {
24
- return <li className="c-list__item">{child}</li>;
25
- }
26
- return <li className="c-list__item">{child}</li>;
27
- })}
28
- </ListElement>
29
- );
30
- });
12
+ export const List: ListComponent = memo(
13
+ ({ children, variant = 'default', className = '', style, ...props }: ListProps) => {
14
+ // Generate CSS classes
15
+ const listClasses = [LIST.BASE_CLASS, variant !== 'default' && `c-list--${variant}`, className]
16
+ .filter(Boolean)
17
+ .join(' ');
31
18
 
32
- export type { ListProps };
19
+ // Determine the HTML element based on variant
20
+ const ListElement = ['number', 'text'].includes(variant) ? 'ol' : 'ul';
21
+
22
+ return (
23
+ <ListElement className={listClasses} style={style} {...props}>
24
+ {React.Children.map(children, child => {
25
+ if (React.isValidElement(child)) {
26
+ // Check if child is a ListItem
27
+ if (child.type === ListItem) {
28
+ return child;
29
+ }
30
+ // Legacy behavior: wrap in li
31
+ return <li className="c-list__item">{child}</li>;
32
+ }
33
+ return <li className="c-list__item">{child}</li>;
34
+ })}
35
+ </ListElement>
36
+ );
37
+ }
38
+ ) as unknown as ListComponent;
33
39
 
34
40
  List.displayName = 'List';
41
+ List.Item = ListItem;
35
42
 
36
43
  export default List;
@@ -0,0 +1,20 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { LIST } from '../../lib/constants/components';
3
+
4
+ export interface ListItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
5
+ children?: React.ReactNode;
6
+ }
7
+
8
+ export const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
9
+ ({ children, className = '', ...props }, ref) => {
10
+ return (
11
+ <li ref={ref} className={`${LIST.ITEM_CLASS} ${className}`.trim()} {...props}>
12
+ {children}
13
+ </li>
14
+ );
15
+ }
16
+ );
17
+
18
+ ListItem.displayName = 'ListItem';
19
+
20
+ export default ListItem;
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
2
2
  import { fn } from '@storybook/test';
3
3
  import { useState } from 'react';
4
4
  import type { AtomixGlassProps } from '../../lib/types/components';
5
- import Modal from './Modal';
5
+ import { Modal } from './Modal';
6
6
 
7
7
  // Helper type for glass props in stories (without children requirement)
8
8
  type GlassProps = boolean | Omit<AtomixGlassProps, 'children'>;
@@ -31,6 +31,7 @@ Modal displays content in a focused overlay dialog. It provides a way to present
31
31
  - Header and footer sections
32
32
  - Accessible design
33
33
  - Responsive behavior
34
+ - **Compound Component Pattern** (new)
34
35
 
35
36
  ## Accessibility
36
37
 
@@ -53,6 +54,20 @@ Modal displays content in a focused overlay dialog. It provides a way to present
53
54
  </Modal>
54
55
  \`\`\`
55
56
 
57
+ ### Compound Component Usage
58
+
59
+ \`\`\`tsx
60
+ <Modal isOpen={isOpen} onOpenChange={setIsOpen}>
61
+ <Modal.Header closeButton title="Custom Header" />
62
+ <Modal.Body>
63
+ <p>Flexible body content</p>
64
+ </Modal.Body>
65
+ <Modal.Footer>
66
+ <button>Action</button>
67
+ </Modal.Footer>
68
+ </Modal>
69
+ \`\`\`
70
+
56
71
  ### With Glass Effect
57
72
 
58
73
  \`\`\`tsx
@@ -284,6 +299,55 @@ export const WithGlassEffect: Story = {
284
299
  },
285
300
  };
286
301
 
302
+ export const CompoundUsage: Story = {
303
+ render: args => {
304
+ const [isOpen, setIsOpen] = useState(false);
305
+
306
+ return (
307
+ <>
308
+ <div
309
+ className="c-btn c-btn--primary"
310
+ onClick={() => setIsOpen(true)}
311
+ style={{ cursor: 'pointer', padding: '8px 16px', display: 'inline-block' }}
312
+ >
313
+ Open Compound Modal
314
+ </div>
315
+
316
+ <Modal
317
+ {...args}
318
+ isOpen={isOpen}
319
+ onOpenChange={setIsOpen}
320
+ >
321
+ <Modal.Header
322
+ title="Compound Component Pattern"
323
+ subtitle="Fully customizable header"
324
+ closeButton
325
+ />
326
+ <Modal.Body>
327
+ <p>
328
+ This modal uses the Compound Component pattern (Modal.Header, Modal.Body, Modal.Footer).
329
+ This allows for greater flexibility in content arrangement.
330
+ </p>
331
+ <div style={{ marginTop: '1rem', padding: '1rem', background: '#f5f5f5', borderRadius: '4px' }}>
332
+ Custom content structure inside Body
333
+ </div>
334
+ </Modal.Body>
335
+ <Modal.Footer>
336
+ <button className="c-btn c-btn--outline-secondary" onClick={() => setIsOpen(false)}>Custom Footer Button</button>
337
+ </Modal.Footer>
338
+ </Modal>
339
+ </>
340
+ );
341
+ },
342
+ parameters: {
343
+ docs: {
344
+ description: {
345
+ story: 'Demonstrates the Compound Component usage pattern.',
346
+ },
347
+ },
348
+ },
349
+ };
350
+
287
351
  /**
288
352
  * Small size modal variant.
289
353
  */
@@ -649,7 +713,8 @@ export const GlassModalSizes: Story = {
649
713
  The glass effect adapts to different modal sizes while maintaining its visual appeal.
650
714
  </p>
651
715
  <p>
652
- The glass effect enhances the modal's appearance, making it visually appealing and easier to read.
716
+ The glass effect enhances the modal's appearance, making it visually appealing and
717
+ easier to read.
653
718
  </p>
654
719
  </Modal>
655
720
  </div>
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useRef, useState, useCallback, memo } from 'react';
1
+ import React, { useEffect, useRef, useState, useCallback, memo, forwardRef, ReactNode } from 'react';
2
2
  import { ModalProps } from '../../lib/types/components';
3
3
  import { MODAL } from '../../lib/constants/components';
4
4
  import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
@@ -73,149 +73,232 @@ function useModal({
73
73
  };
74
74
  }
75
75
 
76
+ // Modal Subcomponents
77
+
78
+ export interface ModalHeaderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
79
+ title?: ReactNode;
80
+ subtitle?: ReactNode;
81
+ closeButton?: boolean;
82
+ onClose?: () => void;
83
+ }
84
+
85
+ export const ModalHeader = forwardRef<HTMLDivElement, ModalHeaderProps>(
86
+ ({ title, subtitle, closeButton, onClose, children, className = '', ...props }, ref) => {
87
+ return (
88
+ <div ref={ref} className={`c-modal__header ${className}`.trim()} {...props}>
89
+ <div className="c-modal__header-content">
90
+ {title && <h3 className="c-modal__title">{title}</h3>}
91
+ {subtitle && <p className="c-modal__sub">{subtitle}</p>}
92
+ {children}
93
+ </div>
94
+ {closeButton && (
95
+ <button
96
+ type="button"
97
+ className="c-modal__close c-btn js-modal-close"
98
+ onClick={onClose}
99
+ aria-label="Close modal"
100
+ >
101
+ <svg
102
+ width="20"
103
+ height="20"
104
+ viewBox="0 0 20 20"
105
+ fill="none"
106
+ xmlns="http://www.w3.org/2000/svg"
107
+ >
108
+ <path
109
+ d="M16.0672 15.1828C16.1253 15.2409 16.1713 15.3098 16.2028 15.3857C16.2342 15.4615 16.2504 15.5429 16.2504 15.625C16.2504 15.7071 16.2342 15.7884 16.2028 15.8643C16.1713 15.9402 16.1253 16.0091 16.0672 16.0672C16.0091 16.1252 15.9402 16.1713 15.8643 16.2027C15.7885 16.2342 15.7071 16.2503 15.625 16.2503C15.5429 16.2503 15.4616 16.2342 15.3857 16.2027C15.3098 16.1713 15.2409 16.1252 15.1828 16.0672L10 10.8836L4.8172 16.0672C4.69992 16.1844 4.54086 16.2503 4.37501 16.2503C4.20916 16.2503 4.0501 16.1844 3.93282 16.0672C3.81555 15.9499 3.74966 15.7908 3.74966 15.625C3.74966 15.4591 3.81555 15.3001 3.93282 15.1828L9.11642 9.99998L3.93282 4.81717C3.81555 4.69989 3.74966 4.54083 3.74966 4.37498C3.74966 4.20913 3.81555 4.05007 3.93282 3.93279C4.0501 3.81552 4.20916 3.74963 4.37501 3.74963C4.54086 3.74963 4.69992 3.81552 4.8172 3.93279L10 9.11639L15.1828 3.93279C15.3001 3.81552 15.4592 3.74963 15.625 3.74963C15.7909 3.74963 15.9499 3.81552 16.0672 3.93279C16.1845 4.05007 16.2504 4.20913 16.2504 4.37498C16.2504 4.54083 16.1845 4.69989 16.0672 4.81717L10.8836 9.99998L16.0672 15.1828Z"
110
+ fill="#141414"
111
+ />
112
+ </svg>
113
+ </button>
114
+ )}
115
+ </div>
116
+ );
117
+ }
118
+ );
119
+ ModalHeader.displayName = 'ModalHeader';
120
+
121
+ export const ModalBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
122
+ ({ children, className = '', ...props }, ref) => {
123
+ return (
124
+ <div ref={ref} className={`c-modal__body ${className}`.trim()} {...props}>
125
+ {children}
126
+ </div>
127
+ );
128
+ }
129
+ );
130
+ ModalBody.displayName = 'ModalBody';
131
+
132
+ export const ModalFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
133
+ ({ children, className = '', ...props }, ref) => {
134
+ return (
135
+ <div ref={ref} className={`c-modal__footer ${className}`.trim()} {...props}>
136
+ {children}
137
+ </div>
138
+ );
139
+ }
140
+ );
141
+ ModalFooter.displayName = 'ModalFooter';
142
+
76
143
  /**
77
144
  * Modal component for displaying overlay content
78
145
  */
79
- export const Modal: React.FC<ModalProps> = memo(({
80
- children,
81
- isOpen = false,
82
- onOpenChange,
83
- onClose,
84
- onOpen,
85
- title,
86
- subtitle,
87
- size = 'md',
88
- backdrop = true,
89
- keyboard = true,
90
- className = '',
91
- style,
92
- closeButton = true,
93
- footer,
94
- glass,
95
- ...props
96
- }) => {
97
- const modalRef = useRef<HTMLDivElement>(null);
98
- const dialogRef = useRef<HTMLDivElement>(null);
99
- const backdropRef = useRef<HTMLDivElement>(null);
100
-
101
- const {
102
- isOpen: isOpenState,
103
- open,
104
- close,
105
- } = useModal({
106
- isOpen,
146
+ type ModalComponent = React.FC<ModalProps> & {
147
+ Header: typeof ModalHeader;
148
+ Body: typeof ModalBody;
149
+ Footer: typeof ModalFooter;
150
+ };
151
+
152
+ const ModalImpl = memo(
153
+ ({
154
+ children,
155
+ isOpen = false,
107
156
  onOpenChange,
108
157
  onClose,
109
158
  onOpen,
110
- });
159
+ title,
160
+ subtitle,
161
+ size = 'md',
162
+ backdrop = true,
163
+ keyboard = true,
164
+ className = '',
165
+ style,
166
+ closeButton = true,
167
+ footer,
168
+ glass,
169
+ ...props
170
+ }: ModalProps) => {
171
+ const modalRef = useRef<HTMLDivElement>(null);
172
+ const dialogRef = useRef<HTMLDivElement>(null);
173
+ const backdropRef = useRef<HTMLDivElement>(null);
111
174
 
112
- // Handle keyboard events for Escape key
113
- useEffect(() => {
114
- if (!keyboard) return undefined;
175
+ const {
176
+ isOpen: isOpenState,
177
+ open,
178
+ close,
179
+ } = useModal({
180
+ isOpen,
181
+ onOpenChange,
182
+ onClose,
183
+ onOpen,
184
+ });
185
+
186
+ // Handle keyboard events for Escape key
187
+ useEffect(() => {
188
+ if (!keyboard) return undefined;
189
+
190
+ const handleKeydown = (event: KeyboardEvent) => {
191
+ if (event.key === 'Escape' && isOpenState) {
192
+ close();
193
+ }
194
+ };
115
195
 
116
- const handleKeydown = (event: KeyboardEvent) => {
117
- if (event.key === 'Escape' && isOpenState) {
196
+ document.addEventListener('keydown', handleKeydown);
197
+ return () => {
198
+ document.removeEventListener('keydown', handleKeydown);
199
+ };
200
+ }, [isOpenState, close, keyboard]);
201
+
202
+ // Handle backdrop click
203
+ const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
204
+ if (backdrop && event.target === event.currentTarget) {
118
205
  close();
119
206
  }
120
207
  };
121
208
 
122
- document.addEventListener('keydown', handleKeydown);
123
- return () => {
124
- document.removeEventListener('keydown', handleKeydown);
125
- };
126
- }, [isOpenState, close, keyboard]);
209
+ // Assemble classes
210
+ const modalClasses = [
211
+ 'c-modal',
212
+ isOpenState ? MODAL.CLASSES.IS_OPEN : '',
213
+ size ? `c-modal--${size}` : '',
214
+ glass ? 'c-modal--glass' : '',
215
+ className,
216
+ ]
217
+ .filter(Boolean)
218
+ .join(' ');
127
219
 
128
- // Handle backdrop click
129
- const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
130
- if (backdrop && event.target === event.currentTarget) {
131
- close();
132
- }
133
- };
220
+ // Check for compound components usage
221
+ const hasCompoundComponents = React.Children.toArray(children).some((child) =>
222
+ React.isValidElement(child) &&
223
+ ['ModalHeader', 'ModalBody', 'ModalFooter'].includes((child.type as any).displayName)
224
+ );
134
225
 
135
- // Assemble classes
136
- const modalClasses = [
137
- 'c-modal',
138
- isOpenState ? MODAL.CLASSES.IS_OPEN : '',
139
- size ? `c-modal--${size}` : '',
140
- glass ? 'c-modal--glass' : '',
141
- className,
142
- ]
143
- .filter(Boolean)
144
- .join(' ');
145
-
146
- const modalContent = (
147
- <div className="c-modal__content">
148
- {(title || closeButton) && (
149
- <div className="c-modal__header">
150
- <div className="c-modal__header-content">
151
- {title && <h3 className="c-modal__title">{title}</h3>}
152
- {subtitle && <p className="c-modal__sub">{subtitle}</p>}
153
- </div>
154
- {closeButton && (
155
- <button
156
- type="button"
157
- className="c-modal__close c-btn js-modal-close"
158
- onClick={close}
159
- aria-label="Close modal"
160
- >
161
- <svg
162
- width="20"
163
- height="20"
164
- viewBox="0 0 20 20"
165
- fill="none"
166
- xmlns="http://www.w3.org/2000/svg"
167
- >
168
- <path
169
- d="M16.0672 15.1828C16.1253 15.2409 16.1713 15.3098 16.2028 15.3857C16.2342 15.4615 16.2504 15.5429 16.2504 15.625C16.2504 15.7071 16.2342 15.7884 16.2028 15.8643C16.1713 15.9402 16.1253 16.0091 16.0672 16.0672C16.0091 16.1252 15.9402 16.1713 15.8643 16.2027C15.7885 16.2342 15.7071 16.2503 15.625 16.2503C15.5429 16.2503 15.4616 16.2342 15.3857 16.2027C15.3098 16.1713 15.2409 16.1252 15.1828 16.0672L10 10.8836L4.8172 16.0672C4.69992 16.1844 4.54086 16.2503 4.37501 16.2503C4.20916 16.2503 4.0501 16.1844 3.93282 16.0672C3.81555 15.9499 3.74966 15.7908 3.74966 15.625C3.74966 15.4591 3.81555 15.3001 3.93282 15.1828L9.11642 9.99998L3.93282 4.81717C3.81555 4.69989 3.74966 4.54083 3.74966 4.37498C3.74966 4.20913 3.81555 4.05007 3.93282 3.93279C4.0501 3.81552 4.20916 3.74963 4.37501 3.74963C4.54086 3.74963 4.69992 3.81552 4.8172 3.93279L10 9.11639L15.1828 3.93279C15.3001 3.81552 15.4592 3.74963 15.625 3.74963C15.7909 3.74963 15.9499 3.81552 16.0672 3.93279C16.1845 4.05007 16.2504 4.20913 16.2504 4.37498C16.2504 4.54083 16.1845 4.69989 16.0672 4.81717L10.8836 9.99998L16.0672 15.1828Z"
170
- fill="#141414"
171
- />
172
- </svg>
173
- </button>
174
- )}
175
- </div>
176
- )}
226
+ const modalContent = (
227
+ <div className="c-modal__content">
228
+ {hasCompoundComponents ? (
229
+ React.Children.map(children, child => {
230
+ if (
231
+ React.isValidElement(child) &&
232
+ (child.type as any).displayName === 'ModalHeader'
233
+ ) {
234
+ return React.cloneElement(child, {
235
+ onClose: (child.props as any).onClose || close,
236
+ } as any);
237
+ }
238
+ return child;
239
+ })
240
+ ) : (
241
+ <>
242
+ {(title || closeButton) && (
243
+ <ModalHeader
244
+ title={title}
245
+ subtitle={subtitle}
246
+ closeButton={closeButton}
247
+ onClose={close}
248
+ />
249
+ )}
250
+ <ModalBody>{children}</ModalBody>
251
+ {footer && <ModalFooter>{footer}</ModalFooter>}
252
+ </>
253
+ )}
254
+ </div>
255
+ );
177
256
 
178
- <div className="c-modal__body">{children}</div>
257
+ return (
258
+ <div
259
+ ref={modalRef}
260
+ className={modalClasses}
261
+ style={{ display: isOpenState ? 'block' : 'none', ...style }}
262
+ role="dialog"
263
+ aria-modal="true"
264
+ aria-hidden={!isOpenState}
265
+ {...props}
266
+ >
267
+ <div ref={backdropRef} className="c-modal__backdrop" onClick={handleBackdropClick} />
268
+ <div ref={dialogRef} className="c-modal__dialog">
269
+ {glass
270
+ ? // Default glass settings for modals
271
+ (() => {
272
+ const defaultGlassProps = {
273
+ displacementScale: document.querySelector('.c-modal---glass .c-modal__content')
274
+ ?.clientHeight,
275
+ blurAmount: 2.2,
276
+ elasticity: 0,
277
+ mode: 'shader' as const,
278
+ shaderMode: 'premiumGlass',
279
+ };
179
280
 
180
- {footer && <div className="c-modal__footer">{footer}</div>}
181
- </div>
182
- );
281
+ const glassProps =
282
+ glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
183
283
 
184
- return (
185
- <div
186
- ref={modalRef}
187
- className={modalClasses}
188
- style={{ display: isOpenState ? 'block' : 'none', ...style }}
189
- role="dialog"
190
- aria-modal="true"
191
- aria-hidden={!isOpenState}
192
- {...props}
193
- >
194
- <div ref={backdropRef} className="c-modal__backdrop" onClick={handleBackdropClick} />
195
- <div ref={dialogRef} className="c-modal__dialog">
196
- {glass
197
- ? // Default glass settings for modals
198
- (() => {
199
- const defaultGlassProps = {
200
- displacementScale: document.querySelector('.c-modal---glass .c-modal__content')?.clientHeight,
201
- blurAmount: 2.2,
202
- elasticity: 0,
203
- mode: 'shader' as const,
204
- shaderMode: 'premiumGlass'
205
- };
206
-
207
- const glassProps =
208
- glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
209
-
210
- return <AtomixGlass {...glassProps}>{modalContent}</AtomixGlass>;
211
- })()
212
- : modalContent}
284
+ return <AtomixGlass {...glassProps}>{modalContent}</AtomixGlass>;
285
+ })()
286
+ : modalContent}
287
+ </div>
213
288
  </div>
214
- </div>
215
- );
216
- });
289
+ );
290
+ }
291
+ );
292
+
293
+ ModalImpl.displayName = 'Modal';
294
+
295
+ // Attach subcomponents
296
+ const ModalWithSubcomponents = ModalImpl as unknown as ModalComponent;
297
+ ModalWithSubcomponents.Header = ModalHeader;
298
+ ModalWithSubcomponents.Body = ModalBody;
299
+ ModalWithSubcomponents.Footer = ModalFooter;
217
300
 
218
- Modal.displayName = 'Modal';
301
+ export const Modal = ModalWithSubcomponents;
219
302
 
220
303
  export type { ModalProps };
221
304
 
@@ -0,0 +1,94 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { Modal } from './Modal';
4
+ import React from 'react';
5
+
6
+ describe('Modal Component', () => {
7
+ it('renders correctly with legacy props', () => {
8
+ render(
9
+ <Modal isOpen={true} title="Legacy Title" footer="Legacy Footer">
10
+ Legacy Content
11
+ </Modal>
12
+ );
13
+
14
+ expect(screen.getByText('Legacy Title')).toBeInTheDocument();
15
+ expect(screen.getByText('Legacy Content')).toBeInTheDocument();
16
+ expect(screen.getByText('Legacy Footer')).toBeInTheDocument();
17
+
18
+ // Check structure classes
19
+ expect(document.querySelector('.c-modal__header')).toBeInTheDocument();
20
+ expect(document.querySelector('.c-modal__body')).toBeInTheDocument();
21
+ expect(document.querySelector('.c-modal__footer')).toBeInTheDocument();
22
+ });
23
+
24
+ it('renders correctly with compound components', () => {
25
+ render(
26
+ <Modal isOpen={true}>
27
+ <Modal.Header title="Compound Header" />
28
+ <Modal.Body>Compound Body</Modal.Body>
29
+ <Modal.Footer>Compound Footer</Modal.Footer>
30
+ </Modal>
31
+ );
32
+
33
+ expect(screen.getByText('Compound Header')).toBeInTheDocument();
34
+ expect(screen.getByText('Compound Body')).toBeInTheDocument();
35
+ expect(screen.getByText('Compound Footer')).toBeInTheDocument();
36
+
37
+ // Verify no double wrapping
38
+ // If double wrapping occurred, we might see nested .c-modal__body or similar issues,
39
+ // or the header inside the body if logic failed.
40
+
41
+ const header = document.querySelector('.c-modal__header');
42
+ const body = document.querySelector('.c-modal__body');
43
+ const footer = document.querySelector('.c-modal__footer');
44
+
45
+ // Header should be a direct child of .c-modal__content (or close to it)
46
+ expect(header?.parentElement).toHaveClass('c-modal__content');
47
+ expect(body?.parentElement).toHaveClass('c-modal__content');
48
+ expect(footer?.parentElement).toHaveClass('c-modal__content');
49
+ });
50
+
51
+ it('injects onClose into Modal.Header when used in compound mode', () => {
52
+ const onClose = vi.fn();
53
+ render(
54
+ <Modal isOpen={true} onClose={onClose}>
55
+ <Modal.Header closeButton data-testid="header" />
56
+ <Modal.Body>Content</Modal.Body>
57
+ </Modal>
58
+ );
59
+
60
+ const closeBtn = screen.getByLabelText('Close modal');
61
+ fireEvent.click(closeBtn);
62
+ expect(onClose).toHaveBeenCalled();
63
+ });
64
+
65
+ it('allows custom onClose in Modal.Header', () => {
66
+ const modalOnClose = vi.fn();
67
+ const headerOnClose = vi.fn();
68
+
69
+ render(
70
+ <Modal isOpen={true} onClose={modalOnClose}>
71
+ <Modal.Header closeButton onClose={headerOnClose} />
72
+ <Modal.Body>Content</Modal.Body>
73
+ </Modal>
74
+ );
75
+
76
+ const closeBtn = screen.getByLabelText('Close modal');
77
+ fireEvent.click(closeBtn);
78
+
79
+ expect(headerOnClose).toHaveBeenCalled();
80
+ expect(modalOnClose).not.toHaveBeenCalled();
81
+ });
82
+
83
+ it('prioritizes compound components over legacy props', () => {
84
+ render(
85
+ <Modal isOpen={true} title="Legacy Title">
86
+ <Modal.Header title="Compound Header" />
87
+ <Modal.Body>Compound Body</Modal.Body>
88
+ </Modal>
89
+ );
90
+
91
+ expect(screen.getByText('Compound Header')).toBeInTheDocument();
92
+ expect(screen.queryByText('Legacy Title')).not.toBeInTheDocument();
93
+ });
94
+ });