@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.
- package/dist/atomix.css +0 -14
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +4 -4
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +12 -19
- package/dist/charts.js +555 -359
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +98 -28
- package/dist/core.js +1082 -733
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +26 -21
- package/dist/forms.js +937 -350
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +14 -21
- package/dist/heavy.js +409 -256
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +518 -284
- package/dist/index.esm.js +1993 -1237
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1994 -1237
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +2 -2
- package/scripts/atomix-cli.js +43 -1
- package/scripts/cli/__tests__/utils.test.js +6 -2
- package/scripts/cli/migration-tools.js +2 -2
- package/scripts/cli/theme-bridge.js +7 -9
- package/scripts/cli/utils.js +2 -1
- package/src/components/Accordion/Accordion.stories.tsx +40 -0
- package/src/components/Accordion/Accordion.tsx +174 -56
- package/src/components/Accordion/AccordionCompound.test.tsx +70 -0
- package/src/components/AtomixGlass/AtomixGlass.tsx +82 -54
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +17 -18
- package/src/components/AtomixGlass/README.md +5 -5
- package/src/components/AtomixGlass/stories/Customization.stories.tsx +2 -2
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +42 -42
- package/src/components/AtomixGlass/stories/Modes.stories.tsx +5 -5
- package/src/components/AtomixGlass/stories/Overview.stories.tsx +3 -3
- package/src/components/AtomixGlass/stories/Performance.stories.tsx +2 -2
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +45 -45
- package/src/components/AtomixGlass/stories/Shaders.stories.tsx +3 -3
- package/src/components/Badge/Badge.stories.tsx +1 -1
- package/src/components/Badge/Badge.tsx +1 -1
- package/src/components/Breadcrumb/Breadcrumb.tsx +185 -65
- package/src/components/Breadcrumb/BreadcrumbCompound.test.tsx +84 -0
- package/src/components/Breadcrumb/index.ts +2 -2
- package/src/components/Button/Button.stories.tsx +1 -1
- package/src/components/Button/README.md +2 -2
- package/src/components/Callout/Callout.stories.tsx +166 -1011
- package/src/components/Callout/Callout.test.tsx +3 -3
- package/src/components/Callout/Callout.tsx +196 -84
- package/src/components/Callout/CalloutCompound.test.tsx +72 -0
- package/src/components/Callout/README.md +2 -2
- package/src/components/Chart/Chart.stories.tsx +1 -1
- package/src/components/Chart/Chart.tsx +5 -5
- package/src/components/Chart/TreemapChart.tsx +37 -29
- package/src/components/DatePicker/readme.md +3 -3
- package/src/components/Dropdown/Dropdown.stories.tsx +1 -1
- package/src/components/Dropdown/Dropdown.tsx +133 -20
- package/src/components/Dropdown/DropdownCompound.test.tsx +64 -0
- package/src/components/EdgePanel/EdgePanel.stories.tsx +7 -7
- package/src/components/EdgePanel/EdgePanel.tsx +164 -112
- package/src/components/EdgePanel/EdgePanelCompound.test.tsx +53 -0
- package/src/components/Form/Checkbox.stories.tsx +1 -1
- package/src/components/Form/Checkbox.tsx +1 -1
- package/src/components/Form/Input.stories.tsx +1 -1
- package/src/components/Form/Input.tsx +1 -1
- package/src/components/Form/Radio.stories.tsx +1 -1
- package/src/components/Form/Radio.tsx +1 -1
- package/src/components/Form/Select.stories.tsx +24 -1
- package/src/components/Form/Select.test.tsx +99 -0
- package/src/components/Form/Select.tsx +145 -94
- package/src/components/Form/SelectOption.tsx +88 -0
- package/src/components/Form/Textarea.stories.tsx +1 -1
- package/src/components/Form/Textarea.tsx +1 -1
- package/src/components/Hero/Hero.stories.tsx +39 -2
- package/src/components/Hero/Hero.test.tsx +142 -0
- package/src/components/Hero/Hero.tsx +143 -4
- package/src/components/List/List.test.tsx +62 -0
- package/src/components/List/List.tsx +16 -5
- package/src/components/List/ListItem.tsx +20 -0
- package/src/components/Messages/Messages.stories.tsx +1 -1
- package/src/components/Messages/Messages.tsx +2 -2
- package/src/components/Modal/Modal.stories.tsx +66 -2
- package/src/components/Modal/Modal.tsx +115 -35
- package/src/components/Modal/ModalCompound.test.tsx +94 -0
- package/src/components/Navigation/Nav/Nav.stories.tsx +2 -2
- package/src/components/Navigation/Nav/Nav.tsx +1 -1
- package/src/components/Navigation/Navbar/Navbar.stories.tsx +3 -3
- package/src/components/Navigation/Navbar/Navbar.tsx +1 -1
- package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +2 -2
- package/src/components/Navigation/SideMenu/SideMenu.tsx +1 -1
- package/src/components/Pagination/Pagination.stories.tsx +1 -1
- package/src/components/Pagination/Pagination.tsx +1 -1
- package/src/components/Popover/Popover.stories.tsx +1 -1
- package/src/components/Popover/Popover.tsx +1 -1
- package/src/components/Progress/Progress.tsx +1 -1
- package/src/components/Rating/Rating.stories.tsx +1 -1
- package/src/components/Rating/Rating.test.tsx +73 -0
- package/src/components/Rating/Rating.tsx +25 -37
- package/src/components/Spinner/Spinner.tsx +1 -1
- package/src/components/Steps/Steps.stories.tsx +1 -1
- package/src/components/Steps/Steps.tsx +125 -22
- package/src/components/Steps/StepsCompound.test.tsx +81 -0
- package/src/components/Tabs/Tabs.stories.tsx +1 -1
- package/src/components/Tabs/Tabs.tsx +198 -45
- package/src/components/Tabs/TabsCompound.test.tsx +64 -0
- package/src/components/Todo/Todo.tsx +0 -1
- package/src/components/Toggle/Toggle.stories.tsx +1 -1
- package/src/components/Toggle/Toggle.tsx +1 -1
- package/src/components/Tooltip/Tooltip.stories.tsx +1 -1
- package/src/components/VideoPlayer/VideoPlayer.stories.tsx +2 -2
- package/src/lib/composables/__tests__/useAtomixGlassPerf.test.tsx +88 -0
- package/src/lib/composables/__tests__/useChart.test.ts +50 -0
- package/src/lib/composables/__tests__/useChart.test.tsx +139 -0
- package/src/lib/composables/__tests__/useHeroBackgroundSlider.test.tsx +59 -0
- package/src/lib/composables/__tests__/useSliderAutoplay.test.tsx +68 -0
- package/src/lib/composables/atomix-glass/useGlassBackgroundDetection.ts +329 -0
- package/src/lib/composables/atomix-glass/useGlassCornerRadius.ts +82 -0
- package/src/lib/composables/atomix-glass/useGlassMouseTracking.ts +153 -0
- package/src/lib/composables/atomix-glass/useGlassOverLight.ts +198 -0
- package/src/lib/composables/atomix-glass/useGlassSize.ts +117 -0
- package/src/lib/composables/atomix-glass/useGlassState.ts +112 -0
- package/src/lib/composables/atomix-glass/useGlassTransforms.ts +160 -0
- package/src/lib/composables/glass-styles.ts +302 -0
- package/src/lib/composables/index.ts +0 -8
- package/src/lib/composables/useAtomixGlass.ts +331 -537
- package/src/lib/composables/useAtomixGlassStyles.ts +307 -0
- package/src/lib/composables/useBarChart.ts +1 -1
- package/src/lib/composables/useBreadcrumb.ts +6 -6
- package/src/lib/composables/useChart.ts +104 -21
- package/src/lib/composables/useHeroBackgroundSlider.ts +16 -7
- package/src/lib/composables/useSlider.ts +66 -34
- package/src/lib/theme/devtools/CLI.ts +2 -10
- package/src/lib/theme/utils/__tests__/themeUtils.test.ts +213 -0
- package/src/lib/types/components.ts +21 -23
- package/src/lib/utils/__tests__/componentUtils.test.ts +57 -2
- package/src/lib/utils/__tests__/dom.test.ts +100 -0
- package/src/lib/utils/__tests__/fontPreloader.test.ts +102 -0
- package/src/lib/utils/__tests__/themeNaming.test.ts +117 -0
- package/src/lib/utils/themeNaming.ts +1 -1
- package/src/styles/06-components/_components.accordion.scss +0 -2
- package/src/styles/06-components/_components.chart.scss +0 -1
- package/src/styles/06-components/_components.dropdown.scss +0 -1
- package/src/styles/06-components/_components.edge-panel.scss +0 -2
- package/src/styles/06-components/_components.photoviewer.scss +0 -1
- package/src/styles/06-components/_components.river.scss +0 -1
- package/src/styles/06-components/_components.slider.scss +0 -3
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
+
});
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
aria-
|
|
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.
|
|
97
|
-
</
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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}>{
|
|
286
|
+
return <AtomixGlass {...glassProps}>{wrapper}</AtomixGlass>;
|
|
138
287
|
}
|
|
139
288
|
|
|
140
|
-
return
|
|
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> = ({
|