@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
@@ -1,9 +1,10 @@
1
- import React, { useEffect, useState, ReactNode } from 'react';
1
+ import React, { useEffect, useState, ReactNode, forwardRef, Children, cloneElement, isValidElement } from 'react';
2
2
  import { STEPS } from '../../lib/constants/components';
3
3
  import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
4
4
  import { AtomixGlassProps } from '../../lib/types/components';
5
5
 
6
- export interface StepItem {
6
+ // Legacy Item Interface
7
+ export interface StepItemData {
7
8
  /**
8
9
  * The number for the step
9
10
  */
@@ -20,11 +21,71 @@ export interface StepItem {
20
21
  content?: React.ReactNode;
21
22
  }
22
23
 
24
+ export type { StepItemData as StepItem };
25
+
26
+ // Compound Component Props
27
+ export interface StepsItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
28
+ /**
29
+ * The number or icon for the step
30
+ */
31
+ number?: number | string | ReactNode;
32
+
33
+ /**
34
+ * The text label/title for the step
35
+ */
36
+ title?: ReactNode;
37
+
38
+ /**
39
+ * Whether the step is active
40
+ */
41
+ active?: boolean;
42
+
43
+ /**
44
+ * Whether the step is completed
45
+ */
46
+ completed?: boolean;
47
+
48
+ /**
49
+ * Index of the step (injected by parent)
50
+ */
51
+ index?: number;
52
+ }
53
+
54
+ export const StepsItem = forwardRef<HTMLDivElement, StepsItemProps>(
55
+ ({ children, className = '', number, title, active, completed, index, ...props }, ref) => {
56
+ const itemClasses = [
57
+ 'c-steps__item',
58
+ active ? STEPS.CLASSES.ACTIVE : '',
59
+ completed ? STEPS.CLASSES.COMPLETED : '',
60
+ className
61
+ ].filter(Boolean).join(' ');
62
+
63
+ return (
64
+ <div
65
+ ref={ref}
66
+ className={itemClasses}
67
+ aria-current={active ? 'step' : undefined}
68
+ data-index={index}
69
+ {...props}
70
+ >
71
+ <div className="c-steps__line"></div>
72
+ <div className="c-steps__content">
73
+ {(number !== undefined && number !== null) && <div className="c-steps__number">{number}</div>}
74
+ {title && <div className="c-steps__text">{title}</div>}
75
+ {children && <div className="c-steps__custom-content">{children}</div>}
76
+ </div>
77
+ </div>
78
+ );
79
+ }
80
+ );
81
+
82
+ StepsItem.displayName = 'StepsItem';
83
+
23
84
  export interface StepsProps {
24
85
  /**
25
- * Array of step items
86
+ * Array of step items (Legacy)
26
87
  */
27
- items: StepItem[];
88
+ items?: StepItemData[];
28
89
 
29
90
  /**
30
91
  * Current active step index (0-based)
@@ -56,12 +117,22 @@ export interface StepsProps {
56
117
  * Can be a boolean to enable with default settings, or an object with AtomixGlassProps to customize the effect
57
118
  */
58
119
  glass?: AtomixGlassProps | boolean;
120
+
121
+ /**
122
+ * Children (Compound)
123
+ */
124
+ children?: ReactNode;
59
125
  }
60
126
 
127
+ type StepsComponent = React.FC<StepsProps> & {
128
+ Item: typeof StepsItem;
129
+ Step: typeof StepsItem; // Alias for convenience
130
+ };
131
+
61
132
  /**
62
133
  * Steps component for displaying a sequence of steps
63
134
  */
64
- export const Steps: React.FC<StepsProps> = ({
135
+ const StepsComp: React.FC<StepsProps> = ({
65
136
  items,
66
137
  activeIndex = 0,
67
138
  vertical = false,
@@ -69,6 +140,7 @@ export const Steps: React.FC<StepsProps> = ({
69
140
  className = '',
70
141
  style,
71
142
  glass,
143
+ children,
72
144
  }) => {
73
145
  const [currentStep, setCurrentStep] = useState(activeIndex);
74
146
 
@@ -79,10 +151,11 @@ export const Steps: React.FC<StepsProps> = ({
79
151
  }
80
152
  }, [activeIndex]);
81
153
 
82
- // Method to go to next step
154
+ // Method to go to next step (Internal helper)
83
155
  const goToNextStep = () => {
84
156
  const nextIndex = currentStep + 1;
85
- if (nextIndex < items.length) {
157
+ const maxIndex = items ? items.length : Children.count(children);
158
+ if (nextIndex < maxIndex) {
86
159
  setCurrentStep(nextIndex);
87
160
  if (onStepChange) {
88
161
  onStepChange(nextIndex);
@@ -101,6 +174,45 @@ export const Steps: React.FC<StepsProps> = ({
101
174
  }
102
175
  };
103
176
 
177
+ let content: ReactNode;
178
+
179
+ if (items && items.length > 0) {
180
+ // Legacy rendering
181
+ content = items.map((item, index) => (
182
+ <StepsItem
183
+ key={`step-${index}`}
184
+ index={index}
185
+ number={item.number}
186
+ title={item.text}
187
+ active={index <= currentStep}
188
+ completed={index < currentStep}
189
+ >
190
+ {item.content}
191
+ </StepsItem>
192
+ ));
193
+ } else {
194
+ // Compound rendering
195
+ content = Children.map(children, (child, index) => {
196
+ if (isValidElement(child)) {
197
+ const childProps = child.props as any;
198
+ // Inject active/completed based on index if not explicitly provided
199
+ const isActive = childProps.active ?? index <= currentStep;
200
+ const isCompleted = childProps.completed ?? index < currentStep;
201
+
202
+ // If number is not provided, default to index + 1
203
+ const number = childProps.number ?? (index + 1);
204
+
205
+ return cloneElement(child, {
206
+ index,
207
+ active: isActive,
208
+ completed: isCompleted,
209
+ number,
210
+ } as any);
211
+ }
212
+ return child;
213
+ });
214
+ }
215
+
104
216
  const stepsContent = (
105
217
  <div
106
218
  className={`c-steps ${vertical ? STEPS.CLASSES.VERTICAL : ''} ${className}`}
@@ -108,20 +220,7 @@ export const Steps: React.FC<StepsProps> = ({
108
220
  role="navigation"
109
221
  aria-label="Steps"
110
222
  >
111
- {items.map((item, index) => (
112
- <div
113
- key={`step-${index}`}
114
- className={`c-steps__item ${index <= currentStep ? STEPS.CLASSES.ACTIVE : ''} ${index < currentStep ? STEPS.CLASSES.COMPLETED : ''}`}
115
- aria-current={index === currentStep ? 'step' : undefined}
116
- >
117
- <div className="c-steps__line"></div>
118
- <div className="c-steps__content">
119
- <div className="c-steps__number">{item.number}</div>
120
- <div className="c-steps__text">{item.text}</div>
121
- {item.content && <div className="c-steps__custom-content">{item.content}</div>}
122
- </div>
123
- </div>
124
- ))}
223
+ {content}
125
224
  </div>
126
225
  );
127
226
 
@@ -132,7 +231,7 @@ export const Steps: React.FC<StepsProps> = ({
132
231
  blurAmount: 1,
133
232
  saturation: 160,
134
233
  aberrationIntensity: 0.5,
135
- cornerRadius: 8,
234
+ borderRadius: 8,
136
235
  mode: 'shader' as const,
137
236
  };
138
237
 
@@ -144,6 +243,10 @@ export const Steps: React.FC<StepsProps> = ({
144
243
  return stepsContent;
145
244
  };
146
245
 
246
+ export const Steps = StepsComp as StepsComponent;
247
+
147
248
  Steps.displayName = 'Steps';
249
+ Steps.Item = StepsItem;
250
+ Steps.Step = StepsItem;
148
251
 
149
252
  export default Steps;
@@ -0,0 +1,81 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { Steps } from './Steps';
4
+ import React from 'react';
5
+
6
+ describe('Steps Component', () => {
7
+ it('renders correctly with legacy items prop', () => {
8
+ const items = [
9
+ { number: 1, text: 'Step 1' },
10
+ { number: 2, text: 'Step 2' },
11
+ { number: 3, text: 'Step 3' },
12
+ ];
13
+ render(<Steps items={items} activeIndex={1} />);
14
+
15
+ // Step 1: active (<= 1) and completed (< 1)
16
+ const step1 = screen.getByText('Step 1').closest('.c-steps__item');
17
+ expect(step1).toHaveClass('is-active');
18
+ expect(step1).toHaveClass('is-completed');
19
+
20
+ // Step 2: active (<= 1) and NOT completed (>= 1)
21
+ const step2 = screen.getByText('Step 2').closest('.c-steps__item');
22
+ expect(step2).toHaveClass('is-active');
23
+ expect(step2).not.toHaveClass('is-completed');
24
+
25
+ // Step 3: NOT active (> 1)
26
+ const step3 = screen.getByText('Step 3').closest('.c-steps__item');
27
+ expect(step3).not.toHaveClass('is-active');
28
+ });
29
+
30
+ it('renders correctly with compound components', () => {
31
+ render(
32
+ <Steps activeIndex={1}>
33
+ <Steps.Item title="Step 1">Content 1</Steps.Item>
34
+ <Steps.Item title="Step 2">Content 2</Steps.Item>
35
+ <Steps.Item title="Step 3">Content 3</Steps.Item>
36
+ </Steps>
37
+ );
38
+
39
+ // Verify titles
40
+ expect(screen.getByText('Step 1')).toBeInTheDocument();
41
+ expect(screen.getByText('Step 2')).toBeInTheDocument();
42
+ expect(screen.getByText('Step 3')).toBeInTheDocument();
43
+
44
+ // Verify content
45
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
46
+
47
+ // Step 1: active and completed (inferred from activeIndex 1)
48
+ const step1 = screen.getByText('Step 1').closest('.c-steps__item');
49
+ expect(step1).toHaveClass('is-active');
50
+ expect(step1).toHaveClass('is-completed');
51
+
52
+ // Step 2: active and NOT completed
53
+ const step2 = screen.getByText('Step 2').closest('.c-steps__item');
54
+ expect(step2).toHaveClass('is-active');
55
+ expect(step2).not.toHaveClass('is-completed');
56
+
57
+ // Step 3: NOT active
58
+ const step3 = screen.getByText('Step 3').closest('.c-steps__item');
59
+ expect(step3).not.toHaveClass('is-active');
60
+
61
+ // Verify automatic numbering
62
+ expect(step1?.querySelector('.c-steps__number')).toHaveTextContent('1');
63
+ expect(step2?.querySelector('.c-steps__number')).toHaveTextContent('2');
64
+ expect(step3?.querySelector('.c-steps__number')).toHaveTextContent('3');
65
+ });
66
+
67
+ it('supports explicit props on Steps.Item', () => {
68
+ render(
69
+ <Steps>
70
+ <Steps.Item title="Custom Step" number="A" active completed>
71
+ Custom Content
72
+ </Steps.Item>
73
+ </Steps>
74
+ );
75
+
76
+ const step = screen.getByText('Custom Step').closest('.c-steps__item');
77
+ expect(step).toHaveClass('is-active');
78
+ expect(step).toHaveClass('is-completed');
79
+ expect(step?.querySelector('.c-steps__number')).toHaveTextContent('A');
80
+ });
81
+ });
@@ -372,7 +372,7 @@ export const GlassCustom: Story = {
372
372
  blurAmount: 2,
373
373
  saturation: 200,
374
374
  aberrationIntensity: 0.8,
375
- cornerRadius: 12,
375
+ borderRadius: 12,
376
376
  } as GlassProps,
377
377
  },
378
378
  render: args => (
@@ -1,4 +1,4 @@
1
- import React, { useState, ReactNode, memo } from 'react';
1
+ import React, { useState, ReactNode, memo, createContext, useContext, forwardRef } from 'react';
2
2
  import { TAB } from '../../lib/constants/components';
3
3
  import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
4
4
  import { AtomixGlassProps } from '../../lib/types/components';
@@ -27,9 +27,9 @@ export interface TabsItemProps {
27
27
 
28
28
  export interface TabsProps {
29
29
  /**
30
- * Array of tab items
30
+ * Array of tab items (Legacy mode)
31
31
  */
32
- items: TabsItemProps[];
32
+ items?: TabsItemProps[];
33
33
 
34
34
  /**
35
35
  * Initial active tab index
@@ -56,12 +56,140 @@ export interface TabsProps {
56
56
  * Can be a boolean to enable with default settings, or an object with AtomixGlassProps to customize the effect
57
57
  */
58
58
  glass?: AtomixGlassProps | boolean;
59
+
60
+ /**
61
+ * Children (Compound mode)
62
+ */
63
+ children?: ReactNode;
64
+ }
65
+
66
+ // Context for compound usage
67
+ const TabsContext = createContext<{
68
+ currentTab: number;
69
+ handleTabClick: (index: number) => void;
70
+ }>({
71
+ currentTab: 0,
72
+ handleTabClick: () => {},
73
+ });
74
+
75
+ // Compound components
76
+ export const TabsList = forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(
77
+ ({ children, className = '', ...props }, ref) => {
78
+ return (
79
+ <ul ref={ref} className={`c-tabs__nav ${className}`.trim()} {...props}>
80
+ {React.Children.map(children, (child, index) => {
81
+ if (React.isValidElement(child)) {
82
+ // Inject index into TabsTrigger
83
+ return React.cloneElement(child, { index } as any);
84
+ }
85
+ return child;
86
+ })}
87
+ </ul>
88
+ );
89
+ }
90
+ );
91
+ TabsList.displayName = 'TabsList';
92
+
93
+ export interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
94
+ index?: number; // Injected by TabsList or passed explicitly
59
95
  }
60
96
 
97
+ export const TabsTrigger = forwardRef<HTMLButtonElement, TabsTriggerProps>(
98
+ ({ children, className = '', index, onClick, ...props }, ref) => {
99
+ const { currentTab, handleTabClick } = useContext(TabsContext);
100
+
101
+ // Safety check if used outside context or without index
102
+ if (index === undefined) {
103
+ console.warn('TabsTrigger requires an index prop or must be a direct child of TabsList');
104
+ }
105
+
106
+ const isActive = index !== undefined && currentTab === index;
107
+
108
+ return (
109
+ <li className="c-tabs__nav-item">
110
+ <button
111
+ ref={ref}
112
+ className={`c-tabs__nav-btn ${isActive ? TAB.CLASSES.ACTIVE : ''} ${className}`.trim()}
113
+ onClick={(e) => {
114
+ if (index !== undefined) handleTabClick(index);
115
+ onClick?.(e);
116
+ }}
117
+ data-tabindex={index}
118
+ role="tab"
119
+ aria-selected={isActive}
120
+ aria-controls={`tab-panel-${index}`}
121
+ type="button"
122
+ {...props}
123
+ >
124
+ {children}
125
+ </button>
126
+ </li>
127
+ );
128
+ }
129
+ );
130
+ TabsTrigger.displayName = 'TabsTrigger';
131
+
132
+ export const TabsPanels = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
133
+ ({ children, className = '', ...props }, ref) => {
134
+ return (
135
+ <div ref={ref} className={`c-tabs__panels ${className}`.trim()} {...props}>
136
+ {React.Children.map(children, (child, index) => {
137
+ if (React.isValidElement(child)) {
138
+ return React.cloneElement(child, { index } as any);
139
+ }
140
+ return child;
141
+ })}
142
+ </div>
143
+ );
144
+ }
145
+ );
146
+ TabsPanels.displayName = 'TabsPanels';
147
+
148
+ export interface TabsPanelProps extends React.HTMLAttributes<HTMLDivElement> {
149
+ index?: number;
150
+ }
151
+
152
+ export const TabsPanel = forwardRef<HTMLDivElement, TabsPanelProps>(
153
+ ({ children, className = '', index, style, ...props }, ref) => {
154
+ const { currentTab } = useContext(TabsContext);
155
+ const isActive = index !== undefined && currentTab === index;
156
+
157
+ return (
158
+ <div
159
+ ref={ref}
160
+ className={`c-tabs__panel ${isActive ? TAB.CLASSES.ACTIVE : ''} ${className}`.trim()}
161
+ data-tabindex={index}
162
+ id={`tab-panel-${index}`}
163
+ role="tabpanel"
164
+ aria-labelledby={`tab-nav-${index}`}
165
+ style={{
166
+ height: isActive ? 'auto' : '0px',
167
+ opacity: isActive ? 1 : 0,
168
+ overflow: 'hidden',
169
+ transition: 'height 0.3s ease, opacity 0.3s ease',
170
+ ...style,
171
+ }}
172
+ {...props}
173
+ >
174
+ <div className="c-tabs__panel-body">{children}</div>
175
+ </div>
176
+ );
177
+ }
178
+ );
179
+ TabsPanel.displayName = 'TabsPanel';
180
+
181
+
61
182
  /**
62
183
  * Tabs component for switching between different content panels
63
184
  */
64
- export const Tabs: React.FC<TabsProps> = memo(
185
+ type TabsComponent = React.FC<TabsProps> & {
186
+ List: typeof TabsList;
187
+ Trigger: typeof TabsTrigger;
188
+ Panels: typeof TabsPanels;
189
+ Panel: typeof TabsPanel;
190
+ };
191
+
192
+ export const Tabs: TabsComponent = memo(
65
193
  ({
66
194
  items,
67
195
  activeIndex = TAB.DEFAULTS.ACTIVE_INDEX,
@@ -69,7 +197,8 @@ export const Tabs: React.FC<TabsProps> = memo(
69
197
  className = '',
70
198
  style,
71
199
  glass,
72
- }) => {
200
+ children,
201
+ }: TabsProps) => {
73
202
  const [currentTab, setCurrentTab] = useState(activeIndex);
74
203
 
75
204
  // Handle tab change
@@ -80,44 +209,64 @@ export const Tabs: React.FC<TabsProps> = memo(
80
209
  }
81
210
  };
82
211
 
83
- const tabContent = (
84
- <div className={`c-tabs js-atomix-tab ${className}`} style={style}>
85
- <ul className="c-tabs__nav">
86
- {items.map((item, index) => (
87
- <li className="c-tabs__nav-item" key={`tab-nav-${index}`}>
88
- <button
89
- className={`c-tabs__nav-btn ${index === currentTab ? TAB.CLASSES.ACTIVE : ''}`}
90
- onClick={() => handleTabClick(index)}
212
+ // Determine content based on mode (legacy items vs compound children)
213
+ let content: ReactNode;
214
+
215
+ // Use items prop if provided
216
+ if (items && items.length > 0) {
217
+ // Legacy mode
218
+ content = (
219
+ <>
220
+ <ul className="c-tabs__nav">
221
+ {items.map((item, index) => (
222
+ <li className="c-tabs__nav-item" key={`tab-nav-${index}`}>
223
+ <button
224
+ className={`c-tabs__nav-btn ${index === currentTab ? TAB.CLASSES.ACTIVE : ''}`}
225
+ onClick={() => handleTabClick(index)}
226
+ data-tabindex={index}
227
+ role="tab"
228
+ aria-selected={index === currentTab}
229
+ aria-controls={`tab-panel-${index}`}
230
+ >
231
+ {item.label}
232
+ </button>
233
+ </li>
234
+ ))}
235
+ </ul>
236
+ <div className="c-tabs__panels">
237
+ {items.map((item, index) => (
238
+ <div
239
+ className={`c-tabs__panel ${index === currentTab ? TAB.CLASSES.ACTIVE : ''}`}
240
+ key={`tab-panel-${index}`}
91
241
  data-tabindex={index}
92
- role="tab"
93
- aria-selected={index === currentTab}
94
- aria-controls={`tab-panel-${index}`}
242
+ id={`tab-panel-${index}`}
243
+ role="tabpanel"
244
+ aria-labelledby={`tab-nav-${index}`}
245
+ style={{
246
+ height: index === currentTab ? 'auto' : '0px',
247
+ opacity: index === currentTab ? 1 : 0,
248
+ overflow: 'hidden',
249
+ transition: 'height 0.3s ease, opacity 0.3s ease',
250
+ }}
95
251
  >
96
- {item.label}
97
- </button>
98
- </li>
99
- ))}
100
- </ul>
101
- <div className="c-tabs__panels">
102
- {items.map((item, index) => (
103
- <div
104
- className={`c-tabs__panel ${index === currentTab ? TAB.CLASSES.ACTIVE : ''}`}
105
- key={`tab-panel-${index}`}
106
- data-tabindex={index}
107
- id={`tab-panel-${index}`}
108
- role="tabpanel"
109
- aria-labelledby={`tab-nav-${index}`}
110
- style={{
111
- height: index === currentTab ? 'auto' : '0px',
112
- opacity: index === currentTab ? 1 : 0,
113
- overflow: 'hidden',
114
- transition: 'height 0.3s ease, opacity 0.3s ease',
115
- }}
116
- >
117
- <div className="c-tabs__panel-body">{item.content}</div>
118
- </div>
119
- ))}
120
- </div>
252
+ <div className="c-tabs__panel-body">{item.content}</div>
253
+ </div>
254
+ ))}
255
+ </div>
256
+ </>
257
+ );
258
+ } else {
259
+ // Compound mode
260
+ content = (
261
+ <TabsContext.Provider value={{ currentTab, handleTabClick }}>
262
+ {children}
263
+ </TabsContext.Provider>
264
+ );
265
+ }
266
+
267
+ const wrapper = (
268
+ <div className={`c-tabs js-atomix-tab ${className}`} style={style}>
269
+ {content}
121
270
  </div>
122
271
  );
123
272
 
@@ -128,19 +277,23 @@ export const Tabs: React.FC<TabsProps> = memo(
128
277
  blurAmount: 1,
129
278
  saturation: 160,
130
279
  aberrationIntensity: 0.5,
131
- cornerRadius: 8,
280
+ borderRadius: 8,
132
281
  mode: 'shader' as const,
133
282
  };
134
283
 
135
284
  const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
136
285
 
137
- return <AtomixGlass {...glassProps}>{tabContent}</AtomixGlass>;
286
+ return <AtomixGlass {...glassProps}>{wrapper}</AtomixGlass>;
138
287
  }
139
288
 
140
- return tabContent;
289
+ return wrapper;
141
290
  }
142
- );
291
+ ) as unknown as TabsComponent;
143
292
 
144
293
  Tabs.displayName = 'Tabs';
294
+ Tabs.List = TabsList;
295
+ Tabs.Trigger = TabsTrigger;
296
+ Tabs.Panels = TabsPanels;
297
+ Tabs.Panel = TabsPanel;
145
298
 
146
299
  export default Tabs;
@@ -0,0 +1,64 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { Tabs } from './Tabs';
4
+ import React from 'react';
5
+
6
+ describe('Tabs Component', () => {
7
+ it('renders correctly with legacy props', () => {
8
+ const items = [
9
+ { label: 'Tab 1', content: 'Content 1' },
10
+ { label: 'Tab 2', content: 'Content 2' },
11
+ ];
12
+ render(<Tabs items={items} />);
13
+
14
+ expect(screen.getByText('Tab 1')).toBeInTheDocument();
15
+ expect(screen.getByText('Tab 2')).toBeInTheDocument();
16
+ expect(screen.getByText('Content 1')).toBeVisible();
17
+
18
+ // Content 2 is rendered but hidden
19
+ const content2 = screen.getByText('Content 2').closest('.c-tabs__panel');
20
+ expect(content2).toHaveStyle({ height: '0px', opacity: '0' });
21
+ });
22
+
23
+ it('renders correctly with compound components', () => {
24
+ render(
25
+ <Tabs>
26
+ <Tabs.List>
27
+ <Tabs.Trigger index={0}>Tab 1</Tabs.Trigger>
28
+ <Tabs.Trigger index={1}>Tab 2</Tabs.Trigger>
29
+ </Tabs.List>
30
+ <Tabs.Panels>
31
+ <Tabs.Panel index={0}>Content 1</Tabs.Panel>
32
+ <Tabs.Panel index={1}>Content 2</Tabs.Panel>
33
+ </Tabs.Panels>
34
+ </Tabs>
35
+ );
36
+
37
+ expect(screen.getByText('Tab 1')).toBeInTheDocument();
38
+ expect(screen.getByText('Tab 2')).toBeInTheDocument();
39
+ expect(screen.getByText('Content 1')).toBeVisible();
40
+ });
41
+
42
+ it('switches tabs in compound mode', () => {
43
+ render(
44
+ <Tabs>
45
+ <Tabs.List>
46
+ <Tabs.Trigger index={0}>Tab 1</Tabs.Trigger>
47
+ <Tabs.Trigger index={1}>Tab 2</Tabs.Trigger>
48
+ </Tabs.List>
49
+ <Tabs.Panels>
50
+ <Tabs.Panel index={0}>Content 1</Tabs.Panel>
51
+ <Tabs.Panel index={1}>Content 2</Tabs.Panel>
52
+ </Tabs.Panels>
53
+ </Tabs>
54
+ );
55
+
56
+ fireEvent.click(screen.getByText('Tab 2'));
57
+
58
+ const content1 = screen.getByText('Content 1').closest('.c-tabs__panel');
59
+ const content2 = screen.getByText('Content 2').closest('.c-tabs__panel');
60
+
61
+ expect(content1).toHaveStyle({ height: '0px', opacity: '0' });
62
+ expect(content2).toHaveStyle({ height: 'auto', opacity: '1' });
63
+ });
64
+ });
@@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
2
2
  import { TodoProps } from '../../lib/types/components';
3
3
  import { useTodo } from '../../lib/composables/useTodo';
4
4
  import { Icon } from '../Icon/Icon';
5
- import { TODO } from '../../lib/constants/components';
6
5
  import { generateUUID } from '../../lib/utils';
7
6
 
8
7
  export const Todo: React.FC<TodoProps> = ({
@@ -266,7 +266,7 @@ export const GlassCustom: Story = {
266
266
  blurAmount: 2,
267
267
  saturation: 200,
268
268
  aberrationIntensity: 0.8,
269
- cornerRadius: 12,
269
+ borderRadius: 12,
270
270
  children: <div>Custom glass</div>,
271
271
  },
272
272
  },