Initial commit
This commit is contained in:
38
packages/core-ui/src/atoms/button/button.stories.tsx
Normal file
38
packages/core-ui/src/atoms/button/button.stories.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Button } from "./button";
|
||||
|
||||
const meta = {
|
||||
title: "Atoms/Button",
|
||||
component: Button,
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["default", "secondary", "destructive", "outline", "ghost"],
|
||||
},
|
||||
size: { control: "select", options: ["sm", "default", "lg"] },
|
||||
},
|
||||
} satisfies Meta<typeof Button>;
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: { children: "Button", variant: "default" },
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: { children: "Secondary", variant: "secondary" },
|
||||
};
|
||||
|
||||
export const Destructive: Story = {
|
||||
args: { children: "Destructive", variant: "destructive" },
|
||||
};
|
||||
|
||||
export const Outline: Story = {
|
||||
args: { children: "Outline", variant: "outline" },
|
||||
};
|
||||
|
||||
export const Ghost: Story = {
|
||||
args: { children: "Ghost", variant: "ghost" },
|
||||
};
|
||||
44
packages/core-ui/src/atoms/button/button.test.tsx
Normal file
44
packages/core-ui/src/atoms/button/button.test.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderWithProviders } from "@repo/core-testing/react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Button } from "./button";
|
||||
|
||||
describe("Button", () => {
|
||||
it("renders children inside a button", () => {
|
||||
renderWithProviders(<Button>Click me</Button>);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Click me" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onClick when activated", async () => {
|
||||
const handleClick = vi.fn();
|
||||
renderWithProviders(<Button onClick={handleClick}>Go</Button>);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Go" }));
|
||||
expect(handleClick).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("applies the destructive variant class", () => {
|
||||
renderWithProviders(<Button variant="destructive">X</Button>);
|
||||
expect(screen.getByRole("button")).toHaveClass(/destructive/);
|
||||
});
|
||||
|
||||
it("applies the lg size class", () => {
|
||||
renderWithProviders(<Button size="lg">X</Button>);
|
||||
expect(screen.getByRole("button")).toHaveClass(/h-11/);
|
||||
});
|
||||
|
||||
it("disabled prop sets the attribute and prevents click handler", async () => {
|
||||
const handleClick = vi.fn();
|
||||
renderWithProviders(
|
||||
<Button disabled onClick={handleClick}>
|
||||
X
|
||||
</Button>,
|
||||
);
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toBeDisabled();
|
||||
await userEvent.click(button);
|
||||
expect(handleClick).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
41
packages/core-ui/src/atoms/button/button.tsx
Normal file
41
packages/core-ui/src/atoms/button/button.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { forwardRef, type ButtonHTMLAttributes } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "default" | "secondary" | "destructive" | "outline" | "ghost";
|
||||
size?: "sm" | "default" | "lg";
|
||||
}
|
||||
|
||||
const variantStyles: Record<NonNullable<ButtonProps["variant"]>, string> = {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
};
|
||||
|
||||
const sizeStyles: Record<NonNullable<ButtonProps["size"]>, string> = {
|
||||
sm: "h-9 px-3 text-sm",
|
||||
default: "h-10 px-4 py-2",
|
||||
lg: "h-11 px-8 text-lg",
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "default", size = "default", ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
variantStyles[variant],
|
||||
sizeStyles[size],
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
1
packages/core-ui/src/atoms/button/index.ts
Normal file
1
packages/core-ui/src/atoms/button/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Button, type ButtonProps } from "./button";
|
||||
4
packages/core-ui/src/atoms/index.ts
Normal file
4
packages/core-ui/src/atoms/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// <gen:atoms>
|
||||
export { Button, type ButtonProps } from "./button/index";
|
||||
export { Input, type InputProps } from "./input/index";
|
||||
export { Label, type LabelProps } from "./label/index";
|
||||
1
packages/core-ui/src/atoms/input/index.ts
Normal file
1
packages/core-ui/src/atoms/input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Input, type InputProps } from "./input";
|
||||
19
packages/core-ui/src/atoms/input/input.stories.tsx
Normal file
19
packages/core-ui/src/atoms/input/input.stories.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Input } from "./input";
|
||||
|
||||
const meta = {
|
||||
title: "Atoms/Input",
|
||||
component: Input,
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof Input>;
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: { placeholder: "Enter text..." },
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: { placeholder: "Disabled", disabled: true },
|
||||
};
|
||||
34
packages/core-ui/src/atoms/input/input.test.tsx
Normal file
34
packages/core-ui/src/atoms/input/input.test.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithProviders } from "@repo/core-testing/react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Input } from "./input";
|
||||
|
||||
describe("Input", () => {
|
||||
it("renders an input with the given placeholder", () => {
|
||||
renderWithProviders(<Input placeholder="email" />);
|
||||
expect(screen.getByPlaceholderText("email")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("forwards the type attribute", () => {
|
||||
renderWithProviders(<Input type="email" placeholder="email" />);
|
||||
expect(screen.getByPlaceholderText("email")).toHaveAttribute(
|
||||
"type",
|
||||
"email",
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts user input", async () => {
|
||||
renderWithProviders(<Input placeholder="email" />);
|
||||
const input = screen.getByPlaceholderText("email");
|
||||
await userEvent.type(input, "hi@example.com");
|
||||
expect(input).toHaveValue("hi@example.com");
|
||||
});
|
||||
|
||||
it("disabled prop blocks user input", async () => {
|
||||
renderWithProviders(<Input placeholder="email" disabled />);
|
||||
const input = screen.getByPlaceholderText("email");
|
||||
await userEvent.type(input, "hi");
|
||||
expect(input).toHaveValue("");
|
||||
});
|
||||
});
|
||||
21
packages/core-ui/src/atoms/input/input.tsx
Normal file
21
packages/core-ui/src/atoms/input/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { forwardRef, type InputHTMLAttributes } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export type InputProps = InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
1
packages/core-ui/src/atoms/label/index.ts
Normal file
1
packages/core-ui/src/atoms/label/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Label, type LabelProps } from "./label";
|
||||
19
packages/core-ui/src/atoms/label/label.stories.tsx
Normal file
19
packages/core-ui/src/atoms/label/label.stories.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Label } from "./label";
|
||||
|
||||
const meta = {
|
||||
title: "Atoms/Label",
|
||||
component: Label,
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof Label>;
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: { children: "Form label" },
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: { children: "Disabled label", className: "peer-disabled:opacity-70" },
|
||||
};
|
||||
16
packages/core-ui/src/atoms/label/label.test.tsx
Normal file
16
packages/core-ui/src/atoms/label/label.test.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithProviders } from "@repo/core-testing/react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { Label } from "./label";
|
||||
|
||||
describe("Label", () => {
|
||||
it("renders the label text", () => {
|
||||
renderWithProviders(<Label htmlFor="email">Email</Label>);
|
||||
expect(screen.getByText("Email")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("forwards htmlFor to associate with an input", () => {
|
||||
renderWithProviders(<Label htmlFor="email">Email</Label>);
|
||||
expect(screen.getByText("Email")).toHaveAttribute("for", "email");
|
||||
});
|
||||
});
|
||||
20
packages/core-ui/src/atoms/label/label.tsx
Normal file
20
packages/core-ui/src/atoms/label/label.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { forwardRef, type LabelHTMLAttributes } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export type LabelProps = LabelHTMLAttributes<HTMLLabelElement>;
|
||||
|
||||
export const Label = forwardRef<HTMLLabelElement, LabelProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<label
|
||||
className={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Label.displayName = "Label";
|
||||
Reference in New Issue
Block a user