@fabio.caffarello/react-design-system 1.0.0 → 1.2.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/README.md +1 -0
- package/dist/index.cjs +4 -4
- package/dist/index.js +330 -176
- package/dist/ui/atoms/Badge/Badge.d.ts +18 -0
- package/dist/ui/atoms/Badge/Badge.stories.d.ts +11 -0
- package/dist/ui/atoms/Badge/Badge.test.d.ts +1 -0
- package/dist/ui/atoms/Select/Select.d.ts +30 -0
- package/dist/ui/atoms/Select/Select.stories.d.ts +10 -0
- package/dist/ui/atoms/Select/Select.test.d.ts +1 -0
- package/dist/ui/atoms/Textarea/Textarea.d.ts +21 -0
- package/dist/ui/atoms/Textarea/Textarea.stories.d.ts +9 -0
- package/dist/ui/atoms/Textarea/Textarea.test.d.ts +1 -0
- package/dist/ui/atoms/index.d.ts +3 -0
- package/dist/ui/molecules/Card/Card.d.ts +22 -0
- package/dist/ui/molecules/Card/Card.stories.d.ts +11 -0
- package/dist/ui/molecules/Card/Card.test.d.ts +1 -0
- package/dist/ui/molecules/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/ui/atoms/Badge/Badge.stories.tsx +86 -0
- package/src/ui/atoms/Badge/Badge.test.tsx +63 -0
- package/src/ui/atoms/Badge/Badge.tsx +60 -0
- package/src/ui/atoms/Select/Select.stories.tsx +93 -0
- package/src/ui/atoms/Select/Select.test.tsx +63 -0
- package/src/ui/atoms/Select/Select.tsx +85 -0
- package/src/ui/atoms/Textarea/Textarea.stories.tsx +72 -0
- package/src/ui/atoms/Textarea/Textarea.test.tsx +55 -0
- package/src/ui/atoms/Textarea/Textarea.tsx +67 -0
- package/src/ui/atoms/index.ts +6 -0
- package/src/ui/molecules/Card/Card.stories.tsx +117 -0
- package/src/ui/molecules/Card/Card.test.tsx +58 -0
- package/src/ui/molecules/Card/Card.tsx +63 -0
- package/src/ui/molecules/index.ts +2 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
interface Props extends HTMLAttributes<HTMLSpanElement> {
|
|
3
|
+
variant?: "success" | "warning" | "error" | "info" | "neutral";
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Badge Component
|
|
7
|
+
*
|
|
8
|
+
* A versatile badge component for displaying status, priority, and other labels.
|
|
9
|
+
* Follows Atomic Design principles as an Atom component.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <Badge variant="success">Active</Badge>
|
|
14
|
+
* <Badge variant="error">Critical</Badge>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export default function Badge({ variant, className, children, ...props }: Props): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Badge from "./Badge";
|
|
3
|
+
declare const meta: Meta<typeof Badge>;
|
|
4
|
+
export declare const Success: StoryObj<typeof Badge>;
|
|
5
|
+
export declare const Warning: StoryObj<typeof Badge>;
|
|
6
|
+
export declare const Error: StoryObj<typeof Badge>;
|
|
7
|
+
export declare const Info: StoryObj<typeof Badge>;
|
|
8
|
+
export declare const Neutral: StoryObj<typeof Badge>;
|
|
9
|
+
export declare const AllVariants: StoryObj<typeof Badge>;
|
|
10
|
+
export declare const WithCustomContent: StoryObj<typeof Badge>;
|
|
11
|
+
export default meta;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SelectHTMLAttributes } from "react";
|
|
2
|
+
interface Option {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface Props extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "children"> {
|
|
8
|
+
options: Option[];
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
error?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Select Component
|
|
14
|
+
*
|
|
15
|
+
* A styled select dropdown component for forms.
|
|
16
|
+
* Follows Atomic Design principles as an Atom component.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <Select
|
|
21
|
+
* options={[
|
|
22
|
+
* { value: "1", label: "Option 1" },
|
|
23
|
+
* { value: "2", label: "Option 2" }
|
|
24
|
+
* ]}
|
|
25
|
+
* placeholder="Select an option"
|
|
26
|
+
* />
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export default function Select({ options, placeholder, error, className, ...props }: Props): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Select from "./Select";
|
|
3
|
+
declare const meta: Meta<typeof Select>;
|
|
4
|
+
export declare const Primary: StoryObj<typeof Select>;
|
|
5
|
+
export declare const WithSelectedValue: StoryObj<typeof Select>;
|
|
6
|
+
export declare const WithError: StoryObj<typeof Select>;
|
|
7
|
+
export declare const WithDisabledOption: StoryObj<typeof Select>;
|
|
8
|
+
export declare const StatusOptions: StoryObj<typeof Select>;
|
|
9
|
+
export declare const PriorityOptions: StoryObj<typeof Select>;
|
|
10
|
+
export default meta;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TextareaHTMLAttributes } from "react";
|
|
2
|
+
interface Props extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
3
|
+
error?: boolean;
|
|
4
|
+
resize?: "none" | "both" | "horizontal" | "vertical";
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Textarea Component
|
|
8
|
+
*
|
|
9
|
+
* A styled textarea component for longer text input.
|
|
10
|
+
* Follows Atomic Design principles as an Atom component.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <Textarea
|
|
15
|
+
* placeholder="Enter description..."
|
|
16
|
+
* rows={4}
|
|
17
|
+
* />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export default function Textarea({ error, resize, className, ...props }: Props): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Textarea from "./Textarea";
|
|
3
|
+
declare const meta: Meta<typeof Textarea>;
|
|
4
|
+
export declare const Primary: StoryObj<typeof Textarea>;
|
|
5
|
+
export declare const WithDefaultValue: StoryObj<typeof Textarea>;
|
|
6
|
+
export declare const WithError: StoryObj<typeof Textarea>;
|
|
7
|
+
export declare const NoResize: StoryObj<typeof Textarea>;
|
|
8
|
+
export declare const LargeTextarea: StoryObj<typeof Textarea>;
|
|
9
|
+
export default meta;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ui/atoms/index.d.ts
CHANGED
|
@@ -3,3 +3,6 @@ export { default as Text } from "./Text/Text";
|
|
|
3
3
|
export { default as Input } from "./Input/Input";
|
|
4
4
|
export { default as Button } from "./Button/Button";
|
|
5
5
|
export { default as BoxWrapper } from "./BoxWrapper/BoxWrapper";
|
|
6
|
+
export { default as Badge } from "./Badge/Badge";
|
|
7
|
+
export { default as Select } from "./Select/Select";
|
|
8
|
+
export { default as Textarea } from "./Textarea/Textarea";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
variant?: "default" | "hover" | "selected";
|
|
4
|
+
padding?: "none" | "small" | "medium" | "large";
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Card Component
|
|
8
|
+
*
|
|
9
|
+
* A versatile card component for displaying content in containers.
|
|
10
|
+
* Follows Atomic Design principles as a Molecule component.
|
|
11
|
+
* Can be used to replace BoxWrapper in many cases with more flexibility.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <Card variant="hover" padding="large">
|
|
16
|
+
* <h3>Card Title</h3>
|
|
17
|
+
* <p>Card content</p>
|
|
18
|
+
* </Card>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export default function Card({ variant, padding, className, children, ...props }: Props): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Card from "./Card";
|
|
3
|
+
declare const meta: Meta<typeof Card>;
|
|
4
|
+
export declare const Default: StoryObj<typeof Card>;
|
|
5
|
+
export declare const Hover: StoryObj<typeof Card>;
|
|
6
|
+
export declare const Selected: StoryObj<typeof Card>;
|
|
7
|
+
export declare const WithPaddingSmall: StoryObj<typeof Card>;
|
|
8
|
+
export declare const WithPaddingLarge: StoryObj<typeof Card>;
|
|
9
|
+
export declare const WithActions: StoryObj<typeof Card>;
|
|
10
|
+
export declare const NoPadding: StoryObj<typeof Card>;
|
|
11
|
+
export default meta;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Badge from "./Badge";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Badge> = {
|
|
5
|
+
title: "UI/Atoms/Badge",
|
|
6
|
+
component: Badge,
|
|
7
|
+
parameters: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component: "A versatile badge component for displaying status, priority, and other labels. Supports multiple variants: success, warning, error, info, and neutral.",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
variant: {
|
|
16
|
+
control: "select",
|
|
17
|
+
options: ["success", "warning", "error", "info", "neutral"],
|
|
18
|
+
description: "Visual variant of the badge",
|
|
19
|
+
},
|
|
20
|
+
children: {
|
|
21
|
+
control: "text",
|
|
22
|
+
description: "Content to display inside the badge",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const Success: StoryObj<typeof Badge> = {
|
|
28
|
+
args: {
|
|
29
|
+
children: "Success",
|
|
30
|
+
variant: "success",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const Warning: StoryObj<typeof Badge> = {
|
|
35
|
+
args: {
|
|
36
|
+
children: "Warning",
|
|
37
|
+
variant: "warning",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const Error: StoryObj<typeof Badge> = {
|
|
42
|
+
args: {
|
|
43
|
+
children: "Error",
|
|
44
|
+
variant: "error",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const Info: StoryObj<typeof Badge> = {
|
|
49
|
+
args: {
|
|
50
|
+
children: "Info",
|
|
51
|
+
variant: "info",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Neutral: StoryObj<typeof Badge> = {
|
|
56
|
+
args: {
|
|
57
|
+
children: "Neutral",
|
|
58
|
+
variant: "neutral",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const AllVariants: StoryObj<typeof Badge> = {
|
|
63
|
+
render: () => (
|
|
64
|
+
<div className="flex gap-2 flex-wrap">
|
|
65
|
+
<Badge variant="success">Success</Badge>
|
|
66
|
+
<Badge variant="warning">Warning</Badge>
|
|
67
|
+
<Badge variant="error">Error</Badge>
|
|
68
|
+
<Badge variant="info">Info</Badge>
|
|
69
|
+
<Badge variant="neutral">Neutral</Badge>
|
|
70
|
+
</div>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const WithCustomContent: StoryObj<typeof Badge> = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<div className="flex gap-2 flex-wrap">
|
|
77
|
+
<Badge variant="success">Active</Badge>
|
|
78
|
+
<Badge variant="error">Critical</Badge>
|
|
79
|
+
<Badge variant="warning">Pending</Badge>
|
|
80
|
+
<Badge variant="info">New</Badge>
|
|
81
|
+
<Badge variant="neutral">Draft</Badge>
|
|
82
|
+
</div>
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default meta;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import Badge from "./Badge";
|
|
4
|
+
|
|
5
|
+
describe("Badge", () => {
|
|
6
|
+
it("renders with default variant", () => {
|
|
7
|
+
render(<Badge>Test Badge</Badge>);
|
|
8
|
+
const badge = screen.getByText("Test Badge");
|
|
9
|
+
expect(badge).toBeInTheDocument();
|
|
10
|
+
expect(badge).toHaveClass("bg-gray-100");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("renders with success variant", () => {
|
|
14
|
+
render(<Badge variant="success">Success</Badge>);
|
|
15
|
+
const badge = screen.getByText("Success");
|
|
16
|
+
expect(badge).toHaveClass("bg-green-100", "text-green-800", "border-green-500");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("renders with warning variant", () => {
|
|
20
|
+
render(<Badge variant="warning">Warning</Badge>);
|
|
21
|
+
const badge = screen.getByText("Warning");
|
|
22
|
+
expect(badge).toHaveClass("bg-yellow-100", "text-yellow-800", "border-yellow-500");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("renders with error variant", () => {
|
|
26
|
+
render(<Badge variant="error">Error</Badge>);
|
|
27
|
+
const badge = screen.getByText("Error");
|
|
28
|
+
expect(badge).toHaveClass("bg-red-100", "text-red-800", "border-red-500");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders with info variant", () => {
|
|
32
|
+
render(<Badge variant="info">Info</Badge>);
|
|
33
|
+
const badge = screen.getByText("Info");
|
|
34
|
+
expect(badge).toHaveClass("bg-blue-100", "text-blue-800", "border-blue-500");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders with neutral variant", () => {
|
|
38
|
+
render(<Badge variant="neutral">Neutral</Badge>);
|
|
39
|
+
const badge = screen.getByText("Neutral");
|
|
40
|
+
expect(badge).toHaveClass("bg-gray-100", "text-gray-800", "border-gray-500");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("applies custom className", () => {
|
|
44
|
+
render(<Badge className="custom-class">Test</Badge>);
|
|
45
|
+
const badge = screen.getByText("Test");
|
|
46
|
+
expect(badge).toHaveClass("custom-class");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("has accessible role and aria-label for string children", () => {
|
|
50
|
+
render(<Badge>Accessible Badge</Badge>);
|
|
51
|
+
const badge = screen.getByRole("status");
|
|
52
|
+
expect(badge).toHaveAttribute("aria-label", "Accessible Badge");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("renders with custom children", () => {
|
|
56
|
+
render(
|
|
57
|
+
<Badge>
|
|
58
|
+
<span>Custom Content</span>
|
|
59
|
+
</Badge>
|
|
60
|
+
);
|
|
61
|
+
expect(screen.getByText("Custom Content")).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
interface Props extends HTMLAttributes<HTMLSpanElement> {
|
|
4
|
+
variant?: "success" | "warning" | "error" | "info" | "neutral";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Badge Component
|
|
9
|
+
*
|
|
10
|
+
* A versatile badge component for displaying status, priority, and other labels.
|
|
11
|
+
* Follows Atomic Design principles as an Atom component.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <Badge variant="success">Active</Badge>
|
|
16
|
+
* <Badge variant="error">Critical</Badge>
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export default function Badge({
|
|
20
|
+
variant = "neutral",
|
|
21
|
+
className = "",
|
|
22
|
+
children,
|
|
23
|
+
...props
|
|
24
|
+
}: Props) {
|
|
25
|
+
const baseClasses = [
|
|
26
|
+
"inline-flex",
|
|
27
|
+
"items-center",
|
|
28
|
+
"px-2",
|
|
29
|
+
"py-1",
|
|
30
|
+
"rounded",
|
|
31
|
+
"text-xs",
|
|
32
|
+
"font-medium",
|
|
33
|
+
"border",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const variantClasses: Record<NonNullable<Props["variant"]>, string> = {
|
|
37
|
+
success: "bg-green-100 text-green-800 border-green-500",
|
|
38
|
+
warning: "bg-yellow-100 text-yellow-800 border-yellow-500",
|
|
39
|
+
error: "bg-red-100 text-red-800 border-red-500",
|
|
40
|
+
info: "bg-blue-100 text-blue-800 border-blue-500",
|
|
41
|
+
neutral: "bg-gray-100 text-gray-800 border-gray-500",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const classes = [
|
|
45
|
+
...baseClasses,
|
|
46
|
+
variantClasses[variant as NonNullable<Props["variant"]>],
|
|
47
|
+
className,
|
|
48
|
+
].filter(Boolean).join(" ");
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<span
|
|
52
|
+
role="status"
|
|
53
|
+
aria-label={typeof children === "string" ? children : undefined}
|
|
54
|
+
className={classes}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</span>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Select from "./Select";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Select> = {
|
|
5
|
+
title: "UI/Atoms/Select",
|
|
6
|
+
component: Select,
|
|
7
|
+
parameters: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component: "A styled select dropdown component for forms. Supports options, placeholder, and error states.",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
options: {
|
|
16
|
+
control: "object",
|
|
17
|
+
description: "Array of options to display",
|
|
18
|
+
},
|
|
19
|
+
placeholder: {
|
|
20
|
+
control: "text",
|
|
21
|
+
description: "Placeholder text for the select",
|
|
22
|
+
},
|
|
23
|
+
error: {
|
|
24
|
+
control: "boolean",
|
|
25
|
+
description: "Whether the select is in an error state",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const defaultOptions = [
|
|
31
|
+
{ value: "1", label: "Option 1" },
|
|
32
|
+
{ value: "2", label: "Option 2" },
|
|
33
|
+
{ value: "3", label: "Option 3" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const Primary: StoryObj<typeof Select> = {
|
|
37
|
+
args: {
|
|
38
|
+
options: defaultOptions,
|
|
39
|
+
placeholder: "Select an option",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const WithSelectedValue: StoryObj<typeof Select> = {
|
|
44
|
+
args: {
|
|
45
|
+
options: defaultOptions,
|
|
46
|
+
defaultValue: "2",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const WithError: StoryObj<typeof Select> = {
|
|
51
|
+
args: {
|
|
52
|
+
options: defaultOptions,
|
|
53
|
+
placeholder: "Select an option",
|
|
54
|
+
error: true,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const WithDisabledOption: StoryObj<typeof Select> = {
|
|
59
|
+
args: {
|
|
60
|
+
options: [
|
|
61
|
+
{ value: "1", label: "Option 1" },
|
|
62
|
+
{ value: "2", label: "Option 2 (Disabled)", disabled: true },
|
|
63
|
+
{ value: "3", label: "Option 3" },
|
|
64
|
+
],
|
|
65
|
+
placeholder: "Select an option",
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const StatusOptions: StoryObj<typeof Select> = {
|
|
70
|
+
args: {
|
|
71
|
+
options: [
|
|
72
|
+
{ value: "DRAFT", label: "Draft" },
|
|
73
|
+
{ value: "ACTIVE", label: "Active" },
|
|
74
|
+
{ value: "COMPLETED", label: "Completed" },
|
|
75
|
+
{ value: "ARCHIVED", label: "Archived" },
|
|
76
|
+
],
|
|
77
|
+
placeholder: "Select status",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const PriorityOptions: StoryObj<typeof Select> = {
|
|
82
|
+
args: {
|
|
83
|
+
options: [
|
|
84
|
+
{ value: "LOW", label: "Low" },
|
|
85
|
+
{ value: "MEDIUM", label: "Medium" },
|
|
86
|
+
{ value: "HIGH", label: "High" },
|
|
87
|
+
{ value: "CRITICAL", label: "Critical" },
|
|
88
|
+
],
|
|
89
|
+
placeholder: "Select priority",
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default meta;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import Select from "./Select";
|
|
4
|
+
|
|
5
|
+
describe("Select", () => {
|
|
6
|
+
const options = [
|
|
7
|
+
{ value: "1", label: "Option 1" },
|
|
8
|
+
{ value: "2", label: "Option 2" },
|
|
9
|
+
{ value: "3", label: "Option 3" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
it("renders with options", () => {
|
|
13
|
+
render(<Select options={options} />);
|
|
14
|
+
const select = screen.getByRole("combobox");
|
|
15
|
+
expect(select).toBeInTheDocument();
|
|
16
|
+
expect(screen.getByText("Option 1")).toBeInTheDocument();
|
|
17
|
+
expect(screen.getByText("Option 2")).toBeInTheDocument();
|
|
18
|
+
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("renders with placeholder", () => {
|
|
22
|
+
render(<Select options={options} placeholder="Select an option" />);
|
|
23
|
+
const placeholder = screen.getByText("Select an option");
|
|
24
|
+
expect(placeholder).toBeInTheDocument();
|
|
25
|
+
expect(placeholder).toBeDisabled();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("applies error styling when error is true", () => {
|
|
29
|
+
render(<Select options={options} error />);
|
|
30
|
+
const select = screen.getByRole("combobox");
|
|
31
|
+
expect(select).toHaveClass("border-red-500");
|
|
32
|
+
expect(select).toHaveAttribute("aria-invalid", "true");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("applies custom className", () => {
|
|
36
|
+
render(<Select options={options} className="custom-class" />);
|
|
37
|
+
const select = screen.getByRole("combobox");
|
|
38
|
+
expect(select).toHaveClass("custom-class");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("renders disabled options", () => {
|
|
42
|
+
const optionsWithDisabled = [
|
|
43
|
+
{ value: "1", label: "Option 1" },
|
|
44
|
+
{ value: "2", label: "Option 2", disabled: true },
|
|
45
|
+
];
|
|
46
|
+
render(<Select options={optionsWithDisabled} />);
|
|
47
|
+
const option2 = screen.getByText("Option 2");
|
|
48
|
+
expect(option2).toBeDisabled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("supports defaultValue", () => {
|
|
52
|
+
render(<Select options={options} defaultValue="2" />);
|
|
53
|
+
const select = screen.getByRole("combobox") as HTMLSelectElement;
|
|
54
|
+
expect(select.value).toBe("2");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("has accessible attributes when error", () => {
|
|
58
|
+
render(<Select options={options} error id="test-select" />);
|
|
59
|
+
const select = screen.getByRole("combobox");
|
|
60
|
+
expect(select).toHaveAttribute("aria-invalid", "true");
|
|
61
|
+
expect(select).toHaveAttribute("aria-describedby", "test-select-error");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { SelectHTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
interface Option {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "children"> {
|
|
10
|
+
options: Option[];
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
error?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Select Component
|
|
17
|
+
*
|
|
18
|
+
* A styled select dropdown component for forms.
|
|
19
|
+
* Follows Atomic Design principles as an Atom component.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <Select
|
|
24
|
+
* options={[
|
|
25
|
+
* { value: "1", label: "Option 1" },
|
|
26
|
+
* { value: "2", label: "Option 2" }
|
|
27
|
+
* ]}
|
|
28
|
+
* placeholder="Select an option"
|
|
29
|
+
* />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export default function Select({
|
|
33
|
+
options,
|
|
34
|
+
placeholder,
|
|
35
|
+
error = false,
|
|
36
|
+
className = "",
|
|
37
|
+
...props
|
|
38
|
+
}: Props) {
|
|
39
|
+
const baseClasses = [
|
|
40
|
+
"block",
|
|
41
|
+
"w-full",
|
|
42
|
+
"rounded",
|
|
43
|
+
"h-form-element",
|
|
44
|
+
"px-large",
|
|
45
|
+
"border",
|
|
46
|
+
"text-base",
|
|
47
|
+
"focus:outline-none",
|
|
48
|
+
"focus:ring-2",
|
|
49
|
+
"focus:ring-offset-2",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const errorClasses = error
|
|
53
|
+
? "border-red-500 focus:ring-red-500"
|
|
54
|
+
: "border-gray-300 focus:ring-indigo-500";
|
|
55
|
+
|
|
56
|
+
const classes = [
|
|
57
|
+
...baseClasses,
|
|
58
|
+
errorClasses,
|
|
59
|
+
className,
|
|
60
|
+
].filter(Boolean).join(" ");
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<select
|
|
64
|
+
className={classes}
|
|
65
|
+
aria-invalid={error}
|
|
66
|
+
aria-describedby={error ? `${props.id}-error` : undefined}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
{placeholder && (
|
|
70
|
+
<option value="" disabled>
|
|
71
|
+
{placeholder}
|
|
72
|
+
</option>
|
|
73
|
+
)}
|
|
74
|
+
{options.map((option) => (
|
|
75
|
+
<option
|
|
76
|
+
key={option.value}
|
|
77
|
+
value={option.value}
|
|
78
|
+
disabled={option.disabled}
|
|
79
|
+
>
|
|
80
|
+
{option.label}
|
|
81
|
+
</option>
|
|
82
|
+
))}
|
|
83
|
+
</select>
|
|
84
|
+
);
|
|
85
|
+
}
|