@shohojdhara/atomix 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/atomix.css +0 -14
  2. package/dist/atomix.css.map +1 -1
  3. package/dist/atomix.min.css +4 -4
  4. package/dist/atomix.min.css.map +1 -1
  5. package/dist/charts.d.ts +12 -19
  6. package/dist/charts.js +555 -359
  7. package/dist/charts.js.map +1 -1
  8. package/dist/core.d.ts +98 -28
  9. package/dist/core.js +1082 -733
  10. package/dist/core.js.map +1 -1
  11. package/dist/forms.d.ts +26 -21
  12. package/dist/forms.js +937 -350
  13. package/dist/forms.js.map +1 -1
  14. package/dist/heavy.d.ts +14 -21
  15. package/dist/heavy.js +409 -256
  16. package/dist/heavy.js.map +1 -1
  17. package/dist/index.d.ts +518 -284
  18. package/dist/index.esm.js +1993 -1237
  19. package/dist/index.esm.js.map +1 -1
  20. package/dist/index.js +1994 -1237
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.min.js +1 -1
  23. package/dist/index.min.js.map +1 -1
  24. package/package.json +2 -2
  25. package/scripts/atomix-cli.js +43 -1
  26. package/scripts/cli/__tests__/utils.test.js +6 -2
  27. package/scripts/cli/migration-tools.js +2 -2
  28. package/scripts/cli/theme-bridge.js +7 -9
  29. package/scripts/cli/utils.js +2 -1
  30. package/src/components/Accordion/Accordion.stories.tsx +40 -0
  31. package/src/components/Accordion/Accordion.tsx +174 -56
  32. package/src/components/Accordion/AccordionCompound.test.tsx +70 -0
  33. package/src/components/AtomixGlass/AtomixGlass.tsx +82 -54
  34. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +17 -18
  35. package/src/components/AtomixGlass/README.md +5 -5
  36. package/src/components/AtomixGlass/stories/Customization.stories.tsx +2 -2
  37. package/src/components/AtomixGlass/stories/Examples.stories.tsx +42 -42
  38. package/src/components/AtomixGlass/stories/Modes.stories.tsx +5 -5
  39. package/src/components/AtomixGlass/stories/Overview.stories.tsx +3 -3
  40. package/src/components/AtomixGlass/stories/Performance.stories.tsx +2 -2
  41. package/src/components/AtomixGlass/stories/Playground.stories.tsx +45 -45
  42. package/src/components/AtomixGlass/stories/Shaders.stories.tsx +3 -3
  43. package/src/components/Badge/Badge.stories.tsx +1 -1
  44. package/src/components/Badge/Badge.tsx +1 -1
  45. package/src/components/Breadcrumb/Breadcrumb.tsx +185 -65
  46. package/src/components/Breadcrumb/BreadcrumbCompound.test.tsx +84 -0
  47. package/src/components/Breadcrumb/index.ts +2 -2
  48. package/src/components/Button/Button.stories.tsx +1 -1
  49. package/src/components/Button/README.md +2 -2
  50. package/src/components/Callout/Callout.stories.tsx +166 -1011
  51. package/src/components/Callout/Callout.test.tsx +3 -3
  52. package/src/components/Callout/Callout.tsx +196 -84
  53. package/src/components/Callout/CalloutCompound.test.tsx +72 -0
  54. package/src/components/Callout/README.md +2 -2
  55. package/src/components/Chart/Chart.stories.tsx +1 -1
  56. package/src/components/Chart/Chart.tsx +5 -5
  57. package/src/components/Chart/TreemapChart.tsx +37 -29
  58. package/src/components/DatePicker/readme.md +3 -3
  59. package/src/components/Dropdown/Dropdown.stories.tsx +1 -1
  60. package/src/components/Dropdown/Dropdown.tsx +133 -20
  61. package/src/components/Dropdown/DropdownCompound.test.tsx +64 -0
  62. package/src/components/EdgePanel/EdgePanel.stories.tsx +7 -7
  63. package/src/components/EdgePanel/EdgePanel.tsx +164 -112
  64. package/src/components/EdgePanel/EdgePanelCompound.test.tsx +53 -0
  65. package/src/components/Form/Checkbox.stories.tsx +1 -1
  66. package/src/components/Form/Checkbox.tsx +1 -1
  67. package/src/components/Form/Input.stories.tsx +1 -1
  68. package/src/components/Form/Input.tsx +1 -1
  69. package/src/components/Form/Radio.stories.tsx +1 -1
  70. package/src/components/Form/Radio.tsx +1 -1
  71. package/src/components/Form/Select.stories.tsx +24 -1
  72. package/src/components/Form/Select.test.tsx +99 -0
  73. package/src/components/Form/Select.tsx +145 -94
  74. package/src/components/Form/SelectOption.tsx +88 -0
  75. package/src/components/Form/Textarea.stories.tsx +1 -1
  76. package/src/components/Form/Textarea.tsx +1 -1
  77. package/src/components/Hero/Hero.stories.tsx +39 -2
  78. package/src/components/Hero/Hero.test.tsx +142 -0
  79. package/src/components/Hero/Hero.tsx +143 -4
  80. package/src/components/List/List.test.tsx +62 -0
  81. package/src/components/List/List.tsx +16 -5
  82. package/src/components/List/ListItem.tsx +20 -0
  83. package/src/components/Messages/Messages.stories.tsx +1 -1
  84. package/src/components/Messages/Messages.tsx +2 -2
  85. package/src/components/Modal/Modal.stories.tsx +66 -2
  86. package/src/components/Modal/Modal.tsx +115 -35
  87. package/src/components/Modal/ModalCompound.test.tsx +94 -0
  88. package/src/components/Navigation/Nav/Nav.stories.tsx +2 -2
  89. package/src/components/Navigation/Nav/Nav.tsx +1 -1
  90. package/src/components/Navigation/Navbar/Navbar.stories.tsx +3 -3
  91. package/src/components/Navigation/Navbar/Navbar.tsx +1 -1
  92. package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +2 -2
  93. package/src/components/Navigation/SideMenu/SideMenu.tsx +1 -1
  94. package/src/components/Pagination/Pagination.stories.tsx +1 -1
  95. package/src/components/Pagination/Pagination.tsx +1 -1
  96. package/src/components/Popover/Popover.stories.tsx +1 -1
  97. package/src/components/Popover/Popover.tsx +1 -1
  98. package/src/components/Progress/Progress.tsx +1 -1
  99. package/src/components/Rating/Rating.stories.tsx +1 -1
  100. package/src/components/Rating/Rating.test.tsx +73 -0
  101. package/src/components/Rating/Rating.tsx +25 -37
  102. package/src/components/Spinner/Spinner.tsx +1 -1
  103. package/src/components/Steps/Steps.stories.tsx +1 -1
  104. package/src/components/Steps/Steps.tsx +125 -22
  105. package/src/components/Steps/StepsCompound.test.tsx +81 -0
  106. package/src/components/Tabs/Tabs.stories.tsx +1 -1
  107. package/src/components/Tabs/Tabs.tsx +198 -45
  108. package/src/components/Tabs/TabsCompound.test.tsx +64 -0
  109. package/src/components/Todo/Todo.tsx +0 -1
  110. package/src/components/Toggle/Toggle.stories.tsx +1 -1
  111. package/src/components/Toggle/Toggle.tsx +1 -1
  112. package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
  113. package/src/components/VideoPlayer/VideoPlayer.stories.tsx +2 -2
  114. package/src/lib/composables/__tests__/useAtomixGlassPerf.test.tsx +88 -0
  115. package/src/lib/composables/__tests__/useChart.test.ts +50 -0
  116. package/src/lib/composables/__tests__/useChart.test.tsx +139 -0
  117. package/src/lib/composables/__tests__/useHeroBackgroundSlider.test.tsx +59 -0
  118. package/src/lib/composables/__tests__/useSliderAutoplay.test.tsx +68 -0
  119. package/src/lib/composables/atomix-glass/useGlassBackgroundDetection.ts +329 -0
  120. package/src/lib/composables/atomix-glass/useGlassCornerRadius.ts +82 -0
  121. package/src/lib/composables/atomix-glass/useGlassMouseTracking.ts +153 -0
  122. package/src/lib/composables/atomix-glass/useGlassOverLight.ts +198 -0
  123. package/src/lib/composables/atomix-glass/useGlassSize.ts +117 -0
  124. package/src/lib/composables/atomix-glass/useGlassState.ts +112 -0
  125. package/src/lib/composables/atomix-glass/useGlassTransforms.ts +160 -0
  126. package/src/lib/composables/glass-styles.ts +302 -0
  127. package/src/lib/composables/index.ts +0 -8
  128. package/src/lib/composables/useAtomixGlass.ts +331 -537
  129. package/src/lib/composables/useAtomixGlassStyles.ts +307 -0
  130. package/src/lib/composables/useBarChart.ts +1 -1
  131. package/src/lib/composables/useBreadcrumb.ts +6 -6
  132. package/src/lib/composables/useChart.ts +104 -21
  133. package/src/lib/composables/useHeroBackgroundSlider.ts +16 -7
  134. package/src/lib/composables/useSlider.ts +66 -34
  135. package/src/lib/theme/devtools/CLI.ts +2 -10
  136. package/src/lib/theme/utils/__tests__/themeUtils.test.ts +213 -0
  137. package/src/lib/types/components.ts +21 -23
  138. package/src/lib/utils/__tests__/componentUtils.test.ts +57 -2
  139. package/src/lib/utils/__tests__/dom.test.ts +100 -0
  140. package/src/lib/utils/__tests__/fontPreloader.test.ts +102 -0
  141. package/src/lib/utils/__tests__/themeNaming.test.ts +117 -0
  142. package/src/lib/utils/themeNaming.ts +1 -1
  143. package/src/styles/06-components/_components.accordion.scss +0 -2
  144. package/src/styles/06-components/_components.chart.scss +0 -1
  145. package/src/styles/06-components/_components.dropdown.scss +0 -1
  146. package/src/styles/06-components/_components.edge-panel.scss +0 -2
  147. package/src/styles/06-components/_components.photoviewer.scss +0 -1
  148. package/src/styles/06-components/_components.river.scss +0 -1
  149. package/src/styles/06-components/_components.slider.scss +0 -3
  150. package/src/styles/99-utilities/_utilities.glass-fixes.scss +0 -1
@@ -6,6 +6,8 @@ import React, {
6
6
  useContext,
7
7
  useEffect,
8
8
  memo,
9
+ forwardRef,
10
+ ReactNode,
9
11
  } from 'react';
10
12
  import { DROPDOWN } from '../../lib/constants/components';
11
13
  import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
@@ -14,6 +16,7 @@ import type {
14
16
  DropdownItemProps,
15
17
  DropdownDividerProps,
16
18
  DropdownHeaderProps,
19
+ AtomixGlassProps,
17
20
  } from '../../lib/types/components';
18
21
 
19
22
  // Context type definition
@@ -32,6 +35,54 @@ const DropdownContext = createContext<DropdownContextType>({
32
35
  trigger: 'click',
33
36
  });
34
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
+
35
86
  /**
36
87
  * DropdownItem component for menu items
37
88
  */
@@ -139,10 +190,21 @@ export const DropdownHeader: React.FC<DropdownHeaderProps> = memo(
139
190
  }
140
191
  );
141
192
 
193
+ // Helper context to pass glass prop to DropdownMenu
194
+ const DropdownStyleContext = createContext<{ glass?: AtomixGlassProps | boolean }>({});
195
+
142
196
  /**
143
197
  * Dropdown component for creating dropdown menus
144
198
  */
145
- export const Dropdown: React.FC<DropdownProps> = memo(
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
+ };
206
+
207
+ export const Dropdown: DropdownComponent = memo(
146
208
  ({
147
209
  children,
148
210
  menu,
@@ -160,7 +222,7 @@ export const Dropdown: React.FC<DropdownProps> = memo(
160
222
  style,
161
223
  glass,
162
224
  ...props
163
- }) => {
225
+ }: DropdownProps) => {
164
226
  // Set up controlled vs uncontrolled state
165
227
  const [uncontrolledIsOpen, setUncontrolledIsOpen] = useState(false);
166
228
  const isControlled = controlledIsOpen !== undefined;
@@ -328,22 +390,46 @@ export const Dropdown: React.FC<DropdownProps> = memo(
328
390
  menuStyleProps.minWidth = typeof minWidth === 'number' ? `${minWidth}px` : minWidth;
329
391
  }
330
392
 
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>
336
- </div>
393
+ // Determine content structure
394
+ // Legacy: menu prop + children as trigger
395
+ // Compound: children contains Trigger and Menu
396
+
397
+ const hasCompoundComponents = React.Children.toArray(children).some((child) =>
398
+ React.isValidElement(child) &&
399
+ ['DropdownTrigger', 'DropdownMenu'].includes((child.type as any).displayName)
337
400
  );
338
401
 
339
- return (
340
- <div
341
- ref={dropdownRef}
342
- className={dropdownClasses}
343
- style={style}
344
- onMouseEnter={trigger === 'hover' ? handleHoverOpen : undefined}
345
- {...props}
346
- >
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
+ }
428
+ }
429
+ });
430
+ } else {
431
+ // Legacy mode
432
+ triggerContent = (
347
433
  <div
348
434
  ref={toggleRef}
349
435
  className="c-dropdown__toggle"
@@ -356,6 +442,31 @@ export const Dropdown: React.FC<DropdownProps> = memo(
356
442
  >
357
443
  {children}
358
444
  </div>
445
+ );
446
+ menuContentNode = (
447
+ <ul className={`c-dropdown__menu ${glass ? 'c-dropdown__menu--glass' : ''}`}>{menu}</ul>
448
+ );
449
+ }
450
+
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>
458
+ </div>
459
+ );
460
+
461
+ return (
462
+ <div
463
+ ref={dropdownRef}
464
+ className={dropdownClasses}
465
+ style={style}
466
+ onMouseEnter={trigger === 'hover' ? handleHoverOpen : undefined}
467
+ {...props}
468
+ >
469
+ {triggerContent}
359
470
 
360
471
  <div
361
472
  ref={menuRef}
@@ -384,13 +495,15 @@ export const Dropdown: React.FC<DropdownProps> = memo(
384
495
  </div>
385
496
  );
386
497
  }
387
- );
498
+ ) as unknown as DropdownComponent;
388
499
 
389
500
  export type { DropdownProps, DropdownItemProps, DropdownDividerProps, DropdownHeaderProps };
390
501
 
391
502
  Dropdown.displayName = 'Dropdown';
392
- DropdownItem.displayName = 'DropdownItem';
393
- DropdownDivider.displayName = 'DropdownDivider';
394
- DropdownHeader.displayName = 'DropdownHeader';
503
+ Dropdown.Trigger = DropdownTrigger;
504
+ Dropdown.Menu = DropdownMenu;
505
+ Dropdown.Item = DropdownItem;
506
+ Dropdown.Divider = DropdownDivider;
507
+ Dropdown.Header = DropdownHeader;
395
508
 
396
509
  export default Dropdown;
@@ -0,0 +1,64 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { Dropdown } from './Dropdown';
4
+ import React from 'react';
5
+
6
+ describe('Dropdown Component', () => {
7
+ it('renders correctly with legacy props', () => {
8
+ // In legacy mode, `menu` prop content is rendered inside the dropdown wrapper.
9
+ // The dropdown wrapper uses CSS visibility/opacity/display to hide the menu when not open.
10
+ // `toBeVisible` checks if the element is visible to the user.
11
+ // However, if the menu is just visually hidden via CSS classes (e.g. opacity: 0),
12
+ // jest-dom might consider it visible if display is not none and visibility is not hidden.
13
+ // Let's check if the wrapper has `is-open` class.
14
+
15
+ const { container } = render(
16
+ <Dropdown menu={<Dropdown.Item>Item 1</Dropdown.Item>}>
17
+ <button>Trigger</button>
18
+ </Dropdown>
19
+ );
20
+
21
+ expect(screen.getByText('Trigger')).toBeInTheDocument();
22
+
23
+ // Check if the menu wrapper exists but does not have 'is-open' class
24
+ const menuWrapper = container.querySelector('.c-dropdown__menu-wrapper');
25
+ expect(menuWrapper).not.toHaveClass('is-open');
26
+ });
27
+
28
+ it('renders correctly with compound components', () => {
29
+ render(
30
+ <Dropdown>
31
+ <Dropdown.Trigger>
32
+ <button>Compound Trigger</button>
33
+ </Dropdown.Trigger>
34
+ <Dropdown.Menu>
35
+ <Dropdown.Item>Item 1</Dropdown.Item>
36
+ </Dropdown.Menu>
37
+ </Dropdown>
38
+ );
39
+
40
+ expect(screen.getByText('Compound Trigger')).toBeInTheDocument();
41
+ });
42
+
43
+ it('toggles menu in compound mode', () => {
44
+ const { container } = render(
45
+ <Dropdown>
46
+ <Dropdown.Trigger>
47
+ <button>Trigger</button>
48
+ </Dropdown.Trigger>
49
+ <Dropdown.Menu>
50
+ <Dropdown.Item>Item 1</Dropdown.Item>
51
+ </Dropdown.Menu>
52
+ </Dropdown>
53
+ );
54
+
55
+ fireEvent.click(screen.getByText('Trigger'));
56
+
57
+ // Check if open class is applied or aria-expanded
58
+ const trigger = screen.getByText('Trigger').closest('.c-dropdown__toggle');
59
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
60
+
61
+ const menuWrapper = container.querySelector('.c-dropdown__menu-wrapper');
62
+ expect(menuWrapper).toHaveClass('is-open');
63
+ });
64
+ });
@@ -627,7 +627,7 @@ export const GlassCustom: Story = {
627
627
  displacementScale: 70,
628
628
  blurAmount: 1.8,
629
629
  saturation: 170,
630
- cornerRadius: 0,
630
+ borderRadius: 0,
631
631
  } as any
632
632
  }
633
633
  >
@@ -693,7 +693,7 @@ export const GlassPremium: Story = {
693
693
  displacementScale: 180,
694
694
  blurAmount: 1,
695
695
  saturation: 60,
696
- cornerRadius: 0,
696
+ borderRadius: 0,
697
697
  } as any
698
698
  }
699
699
  >
@@ -786,7 +786,7 @@ export const GlassShowcase: Story = {
786
786
  displacementScale: 25,
787
787
  blurAmount: 1.2,
788
788
  saturation: 140,
789
- cornerRadius: 0,
789
+ borderRadius: 0,
790
790
  } as any
791
791
  }
792
792
  >
@@ -815,7 +815,7 @@ export const GlassShowcase: Story = {
815
815
  displacementScale: 35,
816
816
  blurAmount: 1.5,
817
817
  saturation: 160,
818
- cornerRadius: 0,
818
+ borderRadius: 0,
819
819
  } as any
820
820
  }
821
821
  >
@@ -844,7 +844,7 @@ export const GlassShowcase: Story = {
844
844
  displacementScale: 50,
845
845
  blurAmount: 2,
846
846
  saturation: 180,
847
- cornerRadius: 0,
847
+ borderRadius: 0,
848
848
  } as any
849
849
  }
850
850
  >
@@ -874,7 +874,7 @@ export const GlassShowcase: Story = {
874
874
  displacementScale: 70,
875
875
  blurAmount: 1.8,
876
876
  saturation: 170,
877
- cornerRadius: 0,
877
+ borderRadius: 0,
878
878
  } as any
879
879
  }
880
880
  >
@@ -907,7 +907,7 @@ export const GlassShowcase: Story = {
907
907
  displacementScale: 180,
908
908
  blurAmount: 1,
909
909
  saturation: 60,
910
- cornerRadius: 0,
910
+ borderRadius: 0,
911
911
  } as any
912
912
  }
913
913
  >
@@ -1,10 +1,53 @@
1
- import React, { useRef, useEffect } from 'react';
1
+ import React, { useRef, useEffect, memo, forwardRef } from 'react';
2
2
  import { EdgePanelProps } from '../../lib/types/components';
3
3
  import { useEdgePanel } from '../../lib/composables/useEdgePanel';
4
4
  import { EDGE_PANEL } from '../../lib/constants/components';
5
5
  import { Icon } from '../Icon/Icon';
6
6
  import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
7
7
 
8
+ // Subcomponents
9
+ export const EdgePanelHeader = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
10
+ ({ children, className = '', ...props }, ref) => (
11
+ <div ref={ref} className={`c-edge-panel__header ${className}`.trim()} {...props}>
12
+ {children}
13
+ </div>
14
+ )
15
+ );
16
+ EdgePanelHeader.displayName = 'EdgePanelHeader';
17
+
18
+ export const EdgePanelBody = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
19
+ ({ children, className = '', ...props }, ref) => (
20
+ <div ref={ref} className={`c-edge-panel__body ${className}`.trim()} {...props}>
21
+ {children}
22
+ </div>
23
+ )
24
+ );
25
+ EdgePanelBody.displayName = 'EdgePanelBody';
26
+
27
+ export const EdgePanelFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
28
+ ({ children, className = '', ...props }, ref) => (
29
+ <div ref={ref} className={`c-edge-panel__footer ${className}`.trim()} {...props}>
30
+ {children}
31
+ </div>
32
+ )
33
+ );
34
+ EdgePanelFooter.displayName = 'EdgePanelFooter';
35
+
36
+ export const EdgePanelCloseButton = forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
37
+ ({ className = '', onClick, ...props }, ref) => (
38
+ <button
39
+ ref={ref}
40
+ className={`c-edge-panel__close c-btn c-btn--icon ${className}`.trim()}
41
+ onClick={onClick}
42
+ aria-label="Close panel"
43
+ {...props}
44
+ >
45
+ <Icon name="X" />
46
+ </button>
47
+ )
48
+ );
49
+ EdgePanelCloseButton.displayName = 'EdgePanelCloseButton';
50
+
8
51
  /**
9
52
  * EdgePanel - A sliding panel component that appears from any screen edge
10
53
  *
@@ -21,129 +64,138 @@ import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
21
64
  * <p>Panel content</p>
22
65
  * </EdgePanel>
23
66
  *
24
- * // With glass effect
25
- * <EdgePanel
26
- * title="Glass Panel"
27
- * isOpen={isOpen}
28
- * onOpenChange={setIsOpen}
29
- * position="end"
30
- * glass={true}
31
- * >
32
- * <p>Panel with glass morphism</p>
33
- * </EdgePanel>
34
- *
35
- * // With custom glass configuration
36
- * <EdgePanel
37
- * title="Custom Glass"
38
- * isOpen={isOpen}
39
- * onOpenChange={setIsOpen}
40
- * position="start"
41
- * glass={{
42
- * mode: 'shader',
43
- * shaderVariant: 'liquidGlass',
44
- * displacementScale: 70,
45
- * blurAmount: 1.8,
46
- * saturation: 170,
47
- * }}
48
- * >
49
- * <p>Panel with custom glass effect</p>
67
+ * // Compound Usage
68
+ * <EdgePanel isOpen={isOpen} onOpenChange={setIsOpen}>
69
+ * <EdgePanel.Header>
70
+ * <h4>Title</h4>
71
+ * <EdgePanel.CloseButton onClick={() => setIsOpen(false)} />
72
+ * </EdgePanel.Header>
73
+ * <EdgePanel.Body>Content</EdgePanel.Body>
74
+ * <EdgePanel.Footer>Footer</EdgePanel.Footer>
50
75
  * </EdgePanel>
51
76
  * ```
52
77
  */
53
- export const EdgePanel: React.FC<EdgePanelProps> = ({
54
- title,
55
- children,
56
- position = 'start',
57
- mode = 'slide',
58
- isOpen = false,
59
- onOpenChange,
60
- backdrop = true,
61
- closeOnBackdropClick = true,
62
- closeOnEscape = true,
63
- className = '',
64
- style,
65
- glass,
66
- }) => {
67
- const {
68
- isOpen: isOpenState,
69
- containerRef,
70
- backdropRef,
71
- generateEdgePanelClass,
72
- closePanel,
73
- handleBackdropClick,
74
- } = useEdgePanel({
75
- position,
76
- mode,
77
- isOpen,
78
+ type EdgePanelComponent = React.FC<EdgePanelProps> & {
79
+ Header: typeof EdgePanelHeader;
80
+ Body: typeof EdgePanelBody;
81
+ Footer: typeof EdgePanelFooter;
82
+ CloseButton: typeof EdgePanelCloseButton;
83
+ };
84
+
85
+ export const EdgePanel: EdgePanelComponent = memo(
86
+ ({
87
+ title,
88
+ children,
89
+ position = 'start',
90
+ mode = 'slide',
91
+ isOpen = false,
78
92
  onOpenChange,
79
- backdrop,
80
- closeOnBackdropClick,
81
- closeOnEscape,
93
+ backdrop = true,
94
+ closeOnBackdropClick = true,
95
+ closeOnEscape = true,
96
+ className = '',
97
+ style,
82
98
  glass,
83
- });
99
+ }: EdgePanelProps) => {
100
+ const {
101
+ isOpen: isOpenState,
102
+ containerRef,
103
+ backdropRef,
104
+ generateEdgePanelClass,
105
+ closePanel,
106
+ handleBackdropClick,
107
+ } = useEdgePanel({
108
+ position,
109
+ mode,
110
+ isOpen,
111
+ onOpenChange,
112
+ backdrop,
113
+ closeOnBackdropClick,
114
+ closeOnEscape,
115
+ glass,
116
+ });
84
117
 
85
- // Moved useRef outside of conditional rendering to fix hook order issue
86
- const glassContentRef = useRef<HTMLDivElement>(null);
118
+ // Moved useRef outside of conditional rendering to fix hook order issue
119
+ const glassContentRef = useRef<HTMLDivElement>(null);
87
120
 
88
- const panelClass = generateEdgePanelClass({
89
- position,
90
- isOpen,
91
- className: glass ? `${className} c-edge-panel--glass` : className,
92
- });
121
+ const panelClass = generateEdgePanelClass({
122
+ position,
123
+ isOpen,
124
+ className: glass ? `${className} c-edge-panel--glass` : className,
125
+ });
93
126
 
94
- // If not open and not controlled by parent, don't render
95
- if (!isOpenState && isOpen === false) {
96
- return null;
97
- }
127
+ // If not open and not controlled by parent, don't render
128
+ // Note: useEdgePanel manages internal state if onOpenChange is not provided?
129
+ // Looking at useEdgePanel (implied): it seems to return isOpenState.
130
+ // If we return null here, animations might be cut off.
131
+ // Usually EdgePanel/Drawer should stay mounted but hidden or conditionally mounted.
132
+ // The original code returned null if !isOpenState && isOpen === false.
133
+ // Let's keep that logic.
134
+ if (!isOpenState && isOpen === false) {
135
+ return null;
136
+ }
98
137
 
99
- const defaultGlassProps = {
100
- elasticity: 0,
101
- };
102
-
103
- const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
104
-
105
- const panelContent = (
106
- <>
107
- <div className="c-edge-panel__header">
108
- <h4>{title}</h4>
109
- <button
110
- className="c-edge-panel__close c-btn c-btn--icon"
111
- onClick={() => closePanel()}
112
- aria-label="Close panel"
113
- >
114
- <Icon name="X" />
115
- </button>
116
- </div>
117
- <div className="c-edge-panel__body">{children}</div>
118
- </>
119
- );
120
-
121
- return (
122
- <div className={panelClass} data-position={position} data-mode={mode} style={style}>
123
- {backdrop && (
124
- <div ref={backdropRef} className="c-edge-panel__backdrop" onClick={handleBackdropClick} />
125
- )}
126
- <div ref={containerRef} className="c-edge-panel__container">
127
- {glass ? (
128
- <AtomixGlass {...glassProps}>
129
- <div
130
- ref={glassContentRef}
131
- className="c-edge-panel__glass-content"
132
- style={{ borderRadius: containerRef.current?.style.borderRadius }}
133
- >
134
- {panelContent}
135
- </div>
136
- </AtomixGlass>
137
- ) : (
138
- panelContent
138
+ const defaultGlassProps = {
139
+ elasticity: 0,
140
+ };
141
+
142
+ const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
143
+
144
+ // Check for compound components
145
+ const hasCompoundComponents = React.Children.toArray(children).some((child) =>
146
+ React.isValidElement(child) &&
147
+ ['EdgePanelHeader', 'EdgePanelBody', 'EdgePanelFooter'].includes((child.type as any).displayName)
148
+ );
149
+
150
+ const panelContent = hasCompoundComponents ? (
151
+ children
152
+ ) : (
153
+ <>
154
+ <div className="c-edge-panel__header">
155
+ <h4>{title}</h4>
156
+ <button
157
+ className="c-edge-panel__close c-btn c-btn--icon"
158
+ onClick={() => closePanel()}
159
+ aria-label="Close panel"
160
+ >
161
+ <Icon name="X" />
162
+ </button>
163
+ </div>
164
+ <div className="c-edge-panel__body">{children}</div>
165
+ </>
166
+ );
167
+
168
+ return (
169
+ <div className={panelClass} data-position={position} data-mode={mode} style={style}>
170
+ {backdrop && (
171
+ <div ref={backdropRef} className="c-edge-panel__backdrop" onClick={handleBackdropClick} />
139
172
  )}
173
+ <div ref={containerRef} className="c-edge-panel__container">
174
+ {glass ? (
175
+ <AtomixGlass {...glassProps}>
176
+ <div
177
+ ref={glassContentRef}
178
+ className="c-edge-panel__glass-content"
179
+ style={{ borderRadius: containerRef.current?.style.borderRadius }}
180
+ >
181
+ {panelContent}
182
+ </div>
183
+ </AtomixGlass>
184
+ ) : (
185
+ panelContent
186
+ )}
187
+ </div>
140
188
  </div>
141
- </div>
142
- );
143
- };
144
-
145
- export type { EdgePanelProps };
189
+ );
190
+ }
191
+ ) as unknown as EdgePanelComponent;
146
192
 
147
193
  EdgePanel.displayName = 'EdgePanel';
194
+ EdgePanel.Header = EdgePanelHeader;
195
+ EdgePanel.Body = EdgePanelBody;
196
+ EdgePanel.Footer = EdgePanelFooter;
197
+ EdgePanel.CloseButton = EdgePanelCloseButton;
198
+
199
+ export type { EdgePanelProps };
148
200
 
149
201
  export default EdgePanel;