@fabio.caffarello/react-design-system 1.6.0 → 1.7.0
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/index.cjs +29 -4
- package/dist/index.js +1866 -716
- package/dist/ui/atoms/Button/Button.d.ts +28 -5
- package/dist/ui/atoms/Button/Button.stories.d.ts +11 -3
- package/dist/ui/atoms/Checkbox/Checkbox.d.ts +24 -0
- package/dist/ui/atoms/Checkbox/Checkbox.stories.d.ts +10 -0
- package/dist/ui/atoms/Checkbox/Checkbox.test.d.ts +1 -0
- package/dist/ui/atoms/Collapsible/Collapsible.d.ts +29 -0
- package/dist/ui/atoms/Collapsible/Collapsible.stories.d.ts +9 -0
- package/dist/ui/atoms/Collapsible/Collapsible.test.d.ts +1 -0
- package/dist/ui/atoms/Input/Input.d.ts +28 -4
- package/dist/ui/atoms/Input/Input.stories.d.ts +8 -3
- package/dist/ui/atoms/Radio/Radio.d.ts +26 -0
- package/dist/ui/atoms/Radio/Radio.stories.d.ts +10 -0
- package/dist/ui/atoms/Radio/Radio.test.d.ts +1 -0
- package/dist/ui/atoms/SidebarItem/SidebarItem.d.ts +3 -1
- package/dist/ui/atoms/SidebarItem/SidebarItem.stories.d.ts +3 -0
- package/dist/ui/atoms/index.d.ts +7 -0
- package/dist/ui/hooks/useCollapsible.d.ts +27 -0
- package/dist/ui/index.d.ts +13 -0
- package/dist/ui/molecules/InputWithLabel/InputWithLabel.d.ts +4 -2
- package/dist/ui/molecules/SidebarGroup/SidebarGroup.d.ts +8 -1
- package/dist/ui/molecules/SidebarGroup/SidebarGroup.stories.d.ts +11 -0
- package/dist/ui/molecules/SidebarGroup/SidebarGroup.test.d.ts +1 -0
- package/dist/ui/providers/ThemeProvider.d.ts +34 -0
- package/dist/ui/tokens/breakpoints.d.ts +36 -0
- package/dist/ui/tokens/colors.d.ts +89 -0
- package/dist/ui/tokens/sidebar.d.ts +48 -0
- package/dist/ui/tokens/spacing.d.ts +53 -0
- package/dist/ui/tokens/themes/dark.d.ts +38 -0
- package/dist/ui/tokens/themes/light.d.ts +38 -0
- package/dist/ui/tokens/tokens.factory.d.ts +57 -0
- package/dist/ui/tokens/typography.d.ts +90 -0
- package/package.json +3 -2
- package/src/ui/atoms/Button/Button.stories.tsx +77 -7
- package/src/ui/atoms/Button/Button.tsx +176 -28
- package/src/ui/atoms/Checkbox/Checkbox.stories.tsx +61 -0
- package/src/ui/atoms/Checkbox/Checkbox.test.tsx +32 -0
- package/src/ui/atoms/Checkbox/Checkbox.tsx +103 -0
- package/src/ui/atoms/Collapsible/Collapsible.tsx +2 -2
- package/src/ui/atoms/Input/Input.stories.tsx +67 -6
- package/src/ui/atoms/Input/Input.tsx +117 -14
- package/src/ui/atoms/Radio/Radio.stories.tsx +72 -0
- package/src/ui/atoms/Radio/Radio.test.tsx +32 -0
- package/src/ui/atoms/Radio/Radio.tsx +104 -0
- package/src/ui/atoms/index.ts +7 -0
- package/src/ui/index.ts +14 -0
- package/src/ui/molecules/InputWithLabel/InputWithLabel.tsx +5 -4
- package/src/ui/molecules/SidebarGroup/SidebarGroup.tsx +30 -38
- package/src/ui/providers/ThemeProvider.tsx +105 -0
- package/src/ui/tokens/breakpoints.ts +71 -0
- package/src/ui/tokens/colors.ts +250 -0
- package/src/ui/tokens/sidebar.ts +9 -3
- package/src/ui/tokens/spacing.ts +127 -0
- package/src/ui/tokens/themes/dark.ts +18 -0
- package/src/ui/tokens/themes/light.ts +18 -0
- package/src/ui/tokens/tokens.factory.ts +117 -0
- package/src/ui/tokens/typography.ts +191 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dark Theme Tokens
|
|
3
|
+
*
|
|
4
|
+
* Dark theme color palette and tokens.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DARK_THEME: {
|
|
7
|
+
readonly colors: Record<import("../colors").ColorRole, import("../colors").SemanticColor>;
|
|
8
|
+
readonly spacing: {
|
|
9
|
+
readonly none: import("../spacing").SpacingToken;
|
|
10
|
+
readonly xs: import("../spacing").SpacingToken;
|
|
11
|
+
readonly sm: import("../spacing").SpacingToken;
|
|
12
|
+
readonly md: import("../spacing").SpacingToken;
|
|
13
|
+
readonly base: import("../spacing").SpacingToken;
|
|
14
|
+
readonly lg: import("../spacing").SpacingToken;
|
|
15
|
+
readonly xl: import("../spacing").SpacingToken;
|
|
16
|
+
readonly '2xl': import("../spacing").SpacingToken;
|
|
17
|
+
readonly '3xl': import("../spacing").SpacingToken;
|
|
18
|
+
readonly '4xl': import("../spacing").SpacingToken;
|
|
19
|
+
readonly '5xl': import("../spacing").SpacingToken;
|
|
20
|
+
readonly '6xl': import("../spacing").SpacingToken;
|
|
21
|
+
};
|
|
22
|
+
readonly typography: {
|
|
23
|
+
readonly h1: import("../typography").TypographyToken;
|
|
24
|
+
readonly h2: import("../typography").TypographyToken;
|
|
25
|
+
readonly h3: import("../typography").TypographyToken;
|
|
26
|
+
readonly h4: import("../typography").TypographyToken;
|
|
27
|
+
readonly h5: import("../typography").TypographyToken;
|
|
28
|
+
readonly h6: import("../typography").TypographyToken;
|
|
29
|
+
readonly body: import("../typography").TypographyToken;
|
|
30
|
+
readonly bodySmall: import("../typography").TypographyToken;
|
|
31
|
+
readonly bodyLarge: import("../typography").TypographyToken;
|
|
32
|
+
readonly label: import("../typography").TypographyToken;
|
|
33
|
+
readonly caption: import("../typography").TypographyToken;
|
|
34
|
+
readonly button: import("../typography").TypographyToken;
|
|
35
|
+
};
|
|
36
|
+
readonly breakpoints: Record<import("../breakpoints").BreakpointName, import("../breakpoints").BreakpointToken>;
|
|
37
|
+
readonly mode: "dark";
|
|
38
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light Theme Tokens
|
|
3
|
+
*
|
|
4
|
+
* Light theme color palette and tokens.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LIGHT_THEME: {
|
|
7
|
+
readonly colors: Record<import("../colors").ColorRole, import("../colors").SemanticColor>;
|
|
8
|
+
readonly spacing: {
|
|
9
|
+
readonly none: import("../spacing").SpacingToken;
|
|
10
|
+
readonly xs: import("../spacing").SpacingToken;
|
|
11
|
+
readonly sm: import("../spacing").SpacingToken;
|
|
12
|
+
readonly md: import("../spacing").SpacingToken;
|
|
13
|
+
readonly base: import("../spacing").SpacingToken;
|
|
14
|
+
readonly lg: import("../spacing").SpacingToken;
|
|
15
|
+
readonly xl: import("../spacing").SpacingToken;
|
|
16
|
+
readonly '2xl': import("../spacing").SpacingToken;
|
|
17
|
+
readonly '3xl': import("../spacing").SpacingToken;
|
|
18
|
+
readonly '4xl': import("../spacing").SpacingToken;
|
|
19
|
+
readonly '5xl': import("../spacing").SpacingToken;
|
|
20
|
+
readonly '6xl': import("../spacing").SpacingToken;
|
|
21
|
+
};
|
|
22
|
+
readonly typography: {
|
|
23
|
+
readonly h1: import("../typography").TypographyToken;
|
|
24
|
+
readonly h2: import("../typography").TypographyToken;
|
|
25
|
+
readonly h3: import("../typography").TypographyToken;
|
|
26
|
+
readonly h4: import("../typography").TypographyToken;
|
|
27
|
+
readonly h5: import("../typography").TypographyToken;
|
|
28
|
+
readonly h6: import("../typography").TypographyToken;
|
|
29
|
+
readonly body: import("../typography").TypographyToken;
|
|
30
|
+
readonly bodySmall: import("../typography").TypographyToken;
|
|
31
|
+
readonly bodyLarge: import("../typography").TypographyToken;
|
|
32
|
+
readonly label: import("../typography").TypographyToken;
|
|
33
|
+
readonly caption: import("../typography").TypographyToken;
|
|
34
|
+
readonly button: import("../typography").TypographyToken;
|
|
35
|
+
};
|
|
36
|
+
readonly breakpoints: Record<import("../breakpoints").BreakpointName, import("../breakpoints").BreakpointToken>;
|
|
37
|
+
readonly mode: "light";
|
|
38
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens Factory
|
|
3
|
+
*
|
|
4
|
+
* Main factory for creating all types of tokens.
|
|
5
|
+
* Implements Factory Pattern for unified token creation.
|
|
6
|
+
*/
|
|
7
|
+
import { type SpacingScale, type SpacingToken } from './spacing';
|
|
8
|
+
import { type TypographyToken, type FontSize, type LineHeight, type FontWeight } from './typography';
|
|
9
|
+
import { type ColorRole, type SemanticColor } from './colors';
|
|
10
|
+
import { type BreakpointName, type BreakpointToken } from './breakpoints';
|
|
11
|
+
export type ThemeMode = 'light' | 'dark';
|
|
12
|
+
export interface TokenSet {
|
|
13
|
+
spacing: Record<string, SpacingToken>;
|
|
14
|
+
typography: Record<string, TypographyToken>;
|
|
15
|
+
colors: Record<ColorRole, SemanticColor>;
|
|
16
|
+
breakpoints: Record<BreakpointName, BreakpointToken>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Tokens Factory
|
|
20
|
+
* Main factory for creating complete token sets
|
|
21
|
+
*/
|
|
22
|
+
export declare class TokensFactory {
|
|
23
|
+
private colorFactory;
|
|
24
|
+
constructor(theme?: ThemeMode);
|
|
25
|
+
/**
|
|
26
|
+
* Create spacing token
|
|
27
|
+
*/
|
|
28
|
+
createSpacing(scale: SpacingScale): SpacingToken;
|
|
29
|
+
/**
|
|
30
|
+
* Create typography token
|
|
31
|
+
*/
|
|
32
|
+
createTypography(size: FontSize, lineHeight?: LineHeight, weight?: FontWeight): TypographyToken;
|
|
33
|
+
/**
|
|
34
|
+
* Create color palette
|
|
35
|
+
*/
|
|
36
|
+
createColorPalette(): Record<ColorRole, SemanticColor>;
|
|
37
|
+
/**
|
|
38
|
+
* Create breakpoint token
|
|
39
|
+
*/
|
|
40
|
+
createBreakpoint(name: BreakpointName): BreakpointToken;
|
|
41
|
+
/**
|
|
42
|
+
* Create complete token set for a theme
|
|
43
|
+
*/
|
|
44
|
+
createTokenSet(): TokenSet;
|
|
45
|
+
/**
|
|
46
|
+
* Switch theme
|
|
47
|
+
*/
|
|
48
|
+
setTheme(theme: ThemeMode): void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Default factory instance (light theme)
|
|
52
|
+
*/
|
|
53
|
+
export declare const defaultTokensFactory: TokensFactory;
|
|
54
|
+
/**
|
|
55
|
+
* Helper function to create token set
|
|
56
|
+
*/
|
|
57
|
+
export declare function createTokenSet(theme?: ThemeMode): TokenSet;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typography Tokens
|
|
3
|
+
*
|
|
4
|
+
* Centralized typography system with font families, sizes, weights, and line heights.
|
|
5
|
+
* Uses Factory Pattern for type-safe token creation.
|
|
6
|
+
*/
|
|
7
|
+
export type FontFamily = 'sans' | 'serif' | 'mono';
|
|
8
|
+
export type FontWeight = 'light' | 'normal' | 'medium' | 'semibold' | 'bold';
|
|
9
|
+
export type FontSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl';
|
|
10
|
+
export type LineHeight = 'none' | 'tight' | 'snug' | 'normal' | 'relaxed' | 'loose';
|
|
11
|
+
export interface TypographyToken {
|
|
12
|
+
fontSize: {
|
|
13
|
+
value: number;
|
|
14
|
+
rem: string;
|
|
15
|
+
px: string;
|
|
16
|
+
tailwind: string;
|
|
17
|
+
};
|
|
18
|
+
lineHeight: {
|
|
19
|
+
value: number;
|
|
20
|
+
tailwind: string;
|
|
21
|
+
};
|
|
22
|
+
fontWeight: {
|
|
23
|
+
value: number;
|
|
24
|
+
tailwind: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface FontFamilyToken {
|
|
28
|
+
name: string;
|
|
29
|
+
stack: string;
|
|
30
|
+
tailwind: string;
|
|
31
|
+
}
|
|
32
|
+
export interface FontWeightToken {
|
|
33
|
+
value: number;
|
|
34
|
+
tailwind: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Typography Token Factory
|
|
38
|
+
* Creates typography tokens with consistent values
|
|
39
|
+
*/
|
|
40
|
+
export declare class TypographyTokenFactory {
|
|
41
|
+
/**
|
|
42
|
+
* Create font size token
|
|
43
|
+
*/
|
|
44
|
+
static createFontSize(size: FontSize): TypographyToken['fontSize'];
|
|
45
|
+
/**
|
|
46
|
+
* Create line height token
|
|
47
|
+
*/
|
|
48
|
+
static createLineHeight(height: LineHeight): TypographyToken['lineHeight'];
|
|
49
|
+
/**
|
|
50
|
+
* Create font weight token
|
|
51
|
+
*/
|
|
52
|
+
static createFontWeight(weight: FontWeight): FontWeightToken;
|
|
53
|
+
/**
|
|
54
|
+
* Create complete typography token
|
|
55
|
+
*/
|
|
56
|
+
static create(size: FontSize, lineHeight?: LineHeight, weight?: FontWeight): TypographyToken;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Font family tokens
|
|
60
|
+
*/
|
|
61
|
+
export declare const FONT_FAMILY_TOKENS: Record<FontFamily, FontFamilyToken>;
|
|
62
|
+
/**
|
|
63
|
+
* Font weight tokens
|
|
64
|
+
*/
|
|
65
|
+
export declare const FONT_WEIGHT_TOKENS: Record<FontWeight, FontWeightToken>;
|
|
66
|
+
/**
|
|
67
|
+
* Pre-defined typography tokens for common use cases
|
|
68
|
+
*/
|
|
69
|
+
export declare const TYPOGRAPHY_TOKENS: {
|
|
70
|
+
readonly h1: TypographyToken;
|
|
71
|
+
readonly h2: TypographyToken;
|
|
72
|
+
readonly h3: TypographyToken;
|
|
73
|
+
readonly h4: TypographyToken;
|
|
74
|
+
readonly h5: TypographyToken;
|
|
75
|
+
readonly h6: TypographyToken;
|
|
76
|
+
readonly body: TypographyToken;
|
|
77
|
+
readonly bodySmall: TypographyToken;
|
|
78
|
+
readonly bodyLarge: TypographyToken;
|
|
79
|
+
readonly label: TypographyToken;
|
|
80
|
+
readonly caption: TypographyToken;
|
|
81
|
+
readonly button: TypographyToken;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Helper function to get typography token
|
|
85
|
+
*/
|
|
86
|
+
export declare function getTypography(variant: keyof typeof TYPOGRAPHY_TOKENS): TypographyToken;
|
|
87
|
+
/**
|
|
88
|
+
* Helper function to get typography classes as string
|
|
89
|
+
*/
|
|
90
|
+
export declare function getTypographyClasses(variant: keyof typeof TYPOGRAPHY_TOKENS): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabio.caffarello/react-design-system",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"react": ">=19",
|
|
39
|
-
"react-dom": ">=19"
|
|
39
|
+
"react-dom": ">=19",
|
|
40
|
+
"lucide-react": "^0.552.0"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@tailwindcss/postcss": "^4.1.16",
|
|
@@ -1,16 +1,86 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from
|
|
2
|
-
import Button from
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import Button from './Button';
|
|
3
3
|
|
|
4
4
|
const meta: Meta<typeof Button> = {
|
|
5
|
-
title:
|
|
5
|
+
title: 'Atoms/Button',
|
|
6
6
|
component: Button,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
variant: {
|
|
10
|
+
control: 'select',
|
|
11
|
+
options: ['primary', 'secondary', 'error', 'outline', 'ghost'],
|
|
12
|
+
},
|
|
13
|
+
size: {
|
|
14
|
+
control: 'select',
|
|
15
|
+
options: ['sm', 'md', 'lg'],
|
|
16
|
+
},
|
|
17
|
+
isLoading: {
|
|
18
|
+
control: 'boolean',
|
|
19
|
+
},
|
|
20
|
+
disabled: {
|
|
21
|
+
control: 'boolean',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
7
24
|
};
|
|
8
25
|
|
|
9
|
-
export
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof Button>;
|
|
28
|
+
|
|
29
|
+
export const Primary: Story = {
|
|
10
30
|
args: {
|
|
11
|
-
|
|
12
|
-
|
|
31
|
+
variant: 'primary',
|
|
32
|
+
children: 'Primary Button',
|
|
13
33
|
},
|
|
14
34
|
};
|
|
15
35
|
|
|
16
|
-
export
|
|
36
|
+
export const Secondary: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
variant: 'secondary',
|
|
39
|
+
children: 'Secondary Button',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Error: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
variant: 'error',
|
|
46
|
+
children: 'Error Button',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Outline: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
variant: 'outline',
|
|
53
|
+
children: 'Outline Button',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const Ghost: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
variant: 'ghost',
|
|
60
|
+
children: 'Ghost Button',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Sizes: Story = {
|
|
65
|
+
render: () => (
|
|
66
|
+
<div className="flex items-center gap-4">
|
|
67
|
+
<Button size="sm">Small</Button>
|
|
68
|
+
<Button size="md">Medium</Button>
|
|
69
|
+
<Button size="lg">Large</Button>
|
|
70
|
+
</div>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Loading: Story = {
|
|
75
|
+
args: {
|
|
76
|
+
isLoading: true,
|
|
77
|
+
children: 'Loading Button',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const Disabled: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
disabled: true,
|
|
84
|
+
children: 'Disabled Button',
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -1,35 +1,183 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
4
|
+
import { getColorClass } from '../../tokens/colors';
|
|
5
|
+
|
|
6
|
+
export type ButtonVariant = 'primary' | 'regular' | 'secondary' | 'error' | 'outline' | 'ghost';
|
|
7
|
+
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
8
|
+
|
|
9
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
10
|
+
variant?: ButtonVariant;
|
|
11
|
+
size?: ButtonSize;
|
|
12
|
+
isLoading?: boolean;
|
|
13
|
+
leftIcon?: ReactNode;
|
|
14
|
+
rightIcon?: ReactNode;
|
|
5
15
|
}
|
|
6
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Button Component Builder
|
|
19
|
+
* Uses Builder Pattern for constructing button classes
|
|
20
|
+
*/
|
|
21
|
+
class ButtonClassBuilder {
|
|
22
|
+
private classes: string[] = [];
|
|
23
|
+
|
|
24
|
+
addBase(): this {
|
|
25
|
+
this.classes.push(
|
|
26
|
+
'inline-flex',
|
|
27
|
+
'items-center',
|
|
28
|
+
'justify-center',
|
|
29
|
+
'font-medium',
|
|
30
|
+
'rounded-md',
|
|
31
|
+
'transition-colors',
|
|
32
|
+
'focus:outline-none',
|
|
33
|
+
'focus:ring-2',
|
|
34
|
+
'focus:ring-offset-2',
|
|
35
|
+
'disabled:opacity-50',
|
|
36
|
+
'disabled:cursor-not-allowed'
|
|
37
|
+
);
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
addVariant(variant: ButtonVariant): this {
|
|
42
|
+
// Map 'regular' to 'primary' for backward compatibility
|
|
43
|
+
const normalizedVariant = variant === 'regular' ? 'primary' : variant;
|
|
44
|
+
|
|
45
|
+
const variantClasses: Record<'primary' | 'secondary' | 'error' | 'outline' | 'ghost', string[]> = {
|
|
46
|
+
primary: [
|
|
47
|
+
getColorClass('primary', 'DEFAULT', 'bg'),
|
|
48
|
+
getColorClass('primary', 'DEFAULT', 'text'),
|
|
49
|
+
'hover:opacity-90',
|
|
50
|
+
'focus:ring-indigo-500',
|
|
51
|
+
],
|
|
52
|
+
secondary: [
|
|
53
|
+
getColorClass('secondary', 'DEFAULT', 'bg'),
|
|
54
|
+
getColorClass('secondary', 'DEFAULT', 'text'),
|
|
55
|
+
'hover:opacity-90',
|
|
56
|
+
'focus:ring-violet-500',
|
|
57
|
+
],
|
|
58
|
+
error: [
|
|
59
|
+
getColorClass('error', 'DEFAULT', 'bg'),
|
|
60
|
+
getColorClass('error', 'DEFAULT', 'text'),
|
|
61
|
+
'hover:opacity-90',
|
|
62
|
+
'focus:ring-red-500',
|
|
63
|
+
],
|
|
64
|
+
outline: [
|
|
65
|
+
'border-2',
|
|
66
|
+
'border-gray-300',
|
|
67
|
+
'bg-transparent',
|
|
68
|
+
'text-gray-700',
|
|
69
|
+
'hover:bg-gray-50',
|
|
70
|
+
'focus:ring-gray-500',
|
|
71
|
+
],
|
|
72
|
+
ghost: [
|
|
73
|
+
'bg-transparent',
|
|
74
|
+
'text-gray-700',
|
|
75
|
+
'hover:bg-gray-100',
|
|
76
|
+
'focus:ring-gray-500',
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
this.classes.push(...variantClasses[normalizedVariant]);
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
addSize(size: ButtonSize): this {
|
|
85
|
+
const sizeClasses: Record<ButtonSize, string[]> = {
|
|
86
|
+
sm: ['px-3', 'py-1.5', 'text-sm'],
|
|
87
|
+
md: ['px-4', 'py-2', 'text-base'],
|
|
88
|
+
lg: ['px-6', 'py-3', 'text-lg'],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.classes.push(...sizeClasses[size]);
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
addCustom(className: string): this {
|
|
96
|
+
if (className) {
|
|
97
|
+
this.classes.push(className);
|
|
98
|
+
}
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
build(): string {
|
|
103
|
+
return this.classes.filter(Boolean).join(' ');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Button Component
|
|
109
|
+
*
|
|
110
|
+
* A styled button component with variants and sizes.
|
|
111
|
+
* Follows Atomic Design principles as an Atom component.
|
|
112
|
+
* Uses Builder Pattern for class construction.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```tsx
|
|
116
|
+
* <Button
|
|
117
|
+
* variant="primary"
|
|
118
|
+
* size="md"
|
|
119
|
+
* onClick={handleClick}
|
|
120
|
+
* >
|
|
121
|
+
* Click me
|
|
122
|
+
* </Button>
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
7
125
|
export default function Button({
|
|
8
|
-
|
|
9
|
-
|
|
126
|
+
variant = 'primary',
|
|
127
|
+
size = 'md',
|
|
128
|
+
isLoading = false,
|
|
129
|
+
leftIcon,
|
|
130
|
+
rightIcon,
|
|
131
|
+
className = '',
|
|
132
|
+
disabled = false,
|
|
133
|
+
children,
|
|
10
134
|
...props
|
|
11
|
-
}:
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
switch (variant) {
|
|
22
|
-
case "error":
|
|
23
|
-
classNames.push("bg-red-light", "text-red-dark", "font-bold");
|
|
24
|
-
break;
|
|
25
|
-
case "secondary":
|
|
26
|
-
classNames.push("bg-transparent", "text-grey-minor", "px-0");
|
|
27
|
-
break;
|
|
28
|
-
case "regular":
|
|
29
|
-
default:
|
|
30
|
-
classNames.push("bg-blue-light", "text-blue-dark", "font-bold");
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
135
|
+
}: ButtonProps) {
|
|
136
|
+
const builder = new ButtonClassBuilder();
|
|
137
|
+
const classes = builder
|
|
138
|
+
.addBase()
|
|
139
|
+
.addVariant(variant)
|
|
140
|
+
.addSize(size)
|
|
141
|
+
.addCustom(className)
|
|
142
|
+
.build();
|
|
33
143
|
|
|
34
|
-
return
|
|
144
|
+
return (
|
|
145
|
+
<button
|
|
146
|
+
className={classes}
|
|
147
|
+
disabled={disabled || isLoading}
|
|
148
|
+
{...props}
|
|
149
|
+
>
|
|
150
|
+
{isLoading ? (
|
|
151
|
+
<>
|
|
152
|
+
<svg
|
|
153
|
+
className="animate-spin -ml-1 mr-2 h-4 w-4"
|
|
154
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
155
|
+
fill="none"
|
|
156
|
+
viewBox="0 0 24 24"
|
|
157
|
+
>
|
|
158
|
+
<circle
|
|
159
|
+
className="opacity-25"
|
|
160
|
+
cx="12"
|
|
161
|
+
cy="12"
|
|
162
|
+
r="10"
|
|
163
|
+
stroke="currentColor"
|
|
164
|
+
strokeWidth="4"
|
|
165
|
+
/>
|
|
166
|
+
<path
|
|
167
|
+
className="opacity-75"
|
|
168
|
+
fill="currentColor"
|
|
169
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
170
|
+
/>
|
|
171
|
+
</svg>
|
|
172
|
+
Loading...
|
|
173
|
+
</>
|
|
174
|
+
) : (
|
|
175
|
+
<>
|
|
176
|
+
{leftIcon && <span className="mr-2">{leftIcon}</span>}
|
|
177
|
+
{children}
|
|
178
|
+
{rightIcon && <span className="ml-2">{rightIcon}</span>}
|
|
179
|
+
</>
|
|
180
|
+
)}
|
|
181
|
+
</button>
|
|
182
|
+
);
|
|
35
183
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import Checkbox from './Checkbox';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Checkbox> = {
|
|
5
|
+
title: 'Atoms/Checkbox',
|
|
6
|
+
component: Checkbox,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
label: {
|
|
10
|
+
control: 'text',
|
|
11
|
+
},
|
|
12
|
+
error: {
|
|
13
|
+
control: 'boolean',
|
|
14
|
+
},
|
|
15
|
+
disabled: {
|
|
16
|
+
control: 'boolean',
|
|
17
|
+
},
|
|
18
|
+
checked: {
|
|
19
|
+
control: 'boolean',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default meta;
|
|
25
|
+
type Story = StoryObj<typeof Checkbox>;
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
label: 'I agree to the terms and conditions',
|
|
30
|
+
checked: false,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const Checked: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
label: 'Subscribe to newsletter',
|
|
37
|
+
checked: true,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const WithError: Story = {
|
|
42
|
+
args: {
|
|
43
|
+
label: 'Accept terms',
|
|
44
|
+
error: true,
|
|
45
|
+
helperText: 'You must accept the terms to continue',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Disabled: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
label: 'This option is disabled',
|
|
52
|
+
disabled: true,
|
|
53
|
+
checked: false,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const WithoutLabel: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
checked: false,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { vi } from 'vitest';
|
|
4
|
+
import Checkbox from './Checkbox';
|
|
5
|
+
|
|
6
|
+
describe('Checkbox', () => {
|
|
7
|
+
it('renders checkbox with label', () => {
|
|
8
|
+
render(<Checkbox label="Test checkbox" />);
|
|
9
|
+
expect(screen.getByLabelText('Test checkbox')).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('handles click events', async () => {
|
|
13
|
+
const handleChange = vi.fn();
|
|
14
|
+
render(<Checkbox label="Test" onChange={handleChange} />);
|
|
15
|
+
|
|
16
|
+
const checkbox = screen.getByLabelText('Test');
|
|
17
|
+
await userEvent.click(checkbox);
|
|
18
|
+
|
|
19
|
+
expect(handleChange).toHaveBeenCalledTimes(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('shows error state', () => {
|
|
23
|
+
render(<Checkbox label="Test" error helperText="Error message" />);
|
|
24
|
+
expect(screen.getByText('Error message')).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByRole('checkbox')).toHaveAttribute('aria-invalid', 'true');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('is disabled when disabled prop is true', () => {
|
|
29
|
+
render(<Checkbox label="Test" disabled />);
|
|
30
|
+
expect(screen.getByRole('checkbox')).toBeDisabled();
|
|
31
|
+
});
|
|
32
|
+
});
|