@yusufalperendumlu/component-library 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/cjs/index.css +1 -1
  2. package/dist/cjs/index.css.map +1 -1
  3. package/dist/cjs/index.js +1 -1
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/esm/index.css +1 -1
  6. package/dist/esm/index.css.map +1 -1
  7. package/dist/esm/index.js +1 -1
  8. package/dist/esm/index.js.map +1 -1
  9. package/dist/index.d.ts +65 -7
  10. package/dist/tailwind.css +1 -1
  11. package/eslint.config.js +23 -0
  12. package/jest.config.ts +13 -0
  13. package/package.json +20 -14
  14. package/prettier.config.js +84 -0
  15. package/src/components/Autocomplete/Autocomplete.stories.tsx +65 -0
  16. package/src/components/Autocomplete/Autocomplete.tsx +127 -0
  17. package/src/components/Autocomplete/Autocomplete.types.ts +14 -0
  18. package/src/components/Autocomplete/index.ts +3 -0
  19. package/src/components/Button/Button.stories.tsx +12 -2
  20. package/src/components/Button/Button.tsx +55 -38
  21. package/src/components/Button/Button.types.ts +7 -0
  22. package/src/components/Button/index.ts +3 -3
  23. package/src/components/Dialog/Dialog.stories.tsx +102 -0
  24. package/src/components/Dialog/Dialog.tsx +90 -0
  25. package/src/components/Dialog/Dialog.types.ts +7 -0
  26. package/src/components/Dialog/index.ts +3 -0
  27. package/src/components/Input/Input.stories.tsx +34 -0
  28. package/src/components/Input/Input.tsx +31 -9
  29. package/src/components/Input/Input.types.ts +6 -0
  30. package/src/components/Input/index.ts +3 -0
  31. package/src/components/Table/Table.stories.tsx +53 -0
  32. package/src/components/Table/Table.tsx +104 -0
  33. package/src/components/Table/Table.types.ts +13 -0
  34. package/src/components/Table/index.ts +3 -0
  35. package/src/components/index.ts +5 -2
  36. package/src/index.ts +3 -3
  37. package/src/tests/Autocomplete.test.tsx +81 -0
  38. package/src/tests/Button.test.tsx +36 -25
  39. package/src/tests/Dialog.test.tsx +86 -0
  40. package/src/tests/Input.test.tsx +42 -0
  41. package/src/tests/Table.test.tsx +55 -0
  42. package/src/tests/styleMock.ts +1 -1
  43. package/tailwind.config.js +6 -1
  44. package/tsconfig.json +6 -2
  45. package/src/components/Input/index.tsx +0 -3
@@ -0,0 +1,102 @@
1
+ import React, { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import Dialog from "./Dialog";
4
+ import Button from "../Button";
5
+ const meta: Meta<typeof Dialog> = {
6
+ title: "Components/Dialog",
7
+ component: Dialog,
8
+ tags: ["autodocs"],
9
+ };
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof Dialog>;
13
+
14
+ const DialogWrapper = ({
15
+ title,
16
+ body,
17
+ variant = "default",
18
+ confirmText = "Update",
19
+ cancelText = "Cancel",
20
+ }: {
21
+ title: string;
22
+ body: React.ReactNode;
23
+ variant?: "default" | "destructive";
24
+ confirmText?: string;
25
+ cancelText?: string;
26
+ }) => {
27
+ const [open, setOpen] = useState(false);
28
+
29
+ return (
30
+ <>
31
+ <Button onClick={() => setOpen(true)} variant="outline" title={title} />
32
+ <Dialog isOpen={open} onClose={() => setOpen(false)}>
33
+ <Dialog.Header>{title}</Dialog.Header>
34
+ <Dialog.Body>{body}</Dialog.Body>
35
+ <Dialog.Footer>
36
+ <Button
37
+ variant="outline"
38
+ onClick={() => setOpen(false)}
39
+ title={cancelText}
40
+ size="small"
41
+ />
42
+
43
+ <Button
44
+ onClick={() => setOpen(false)}
45
+ title={confirmText}
46
+ variant="primary"
47
+ size="small"
48
+ />
49
+ </Dialog.Footer>
50
+ </Dialog>
51
+ </>
52
+ );
53
+ };
54
+
55
+ // 🟣 Update Dialog
56
+ export const UpdateDialog: Story = {
57
+ render: () => (
58
+ <DialogWrapper
59
+ title="Update XY1234"
60
+ body={
61
+ <>
62
+ <div className="mb-4">
63
+ <label className="block text-sm mb-1">Type</label>
64
+ <select className="w-full border rounded p-2 dark:bg-zinc-800">
65
+ <option>Passenger</option>
66
+ <option>Cargo</option>
67
+ </select>
68
+ </div>
69
+ <div>
70
+ <label className="block text-sm mb-1">Classification</label>
71
+ <select className="w-full border rounded p-2 dark:bg-zinc-800">
72
+ <option>Friendly</option>
73
+ <option>Hostile</option>
74
+ </select>
75
+ </div>
76
+ </>
77
+ }
78
+ />
79
+ ),
80
+ };
81
+
82
+ // 🔴 Delete Dialog
83
+ export const DeleteDialog: Story = {
84
+ render: () => (
85
+ <DialogWrapper
86
+ title="Delete XY1234"
87
+ variant="destructive"
88
+ confirmText="Delete"
89
+ body={
90
+ <>
91
+ <p className="text-base font-semibold text-zinc-900 dark:text-white">
92
+ Are you sure you want to delete aircraft XY1234?
93
+ </p>
94
+ <p className="text-sm text-zinc-500 dark:text-zinc-400 mt-2">
95
+ This action cannot be undone. The aircraft will be permanently
96
+ removed from the system.
97
+ </p>
98
+ </>
99
+ }
100
+ />
101
+ ),
102
+ };
@@ -0,0 +1,90 @@
1
+ import clsx from 'clsx';
2
+ import React, { useEffect, useRef } from 'react';
3
+ import { IoClose } from 'react-icons/io5';
4
+
5
+ import { DialogProps } from './Dialog.types';
6
+
7
+ const DialogComponent: React.FC<DialogProps> = ({
8
+ isOpen,
9
+ onClose,
10
+ children,
11
+ className,
12
+ showCloseIcon = true,
13
+ }) => {
14
+ const dialogRef = useRef<HTMLDivElement>(null);
15
+
16
+ useEffect(() => {
17
+ const handleOutsideClick = (e: MouseEvent) => {
18
+ if (dialogRef.current && !dialogRef.current.contains(e.target as Node)) {
19
+ onClose();
20
+ }
21
+ };
22
+
23
+ const handleEscape = (e: KeyboardEvent) => {
24
+ if (e.key === 'Escape') {
25
+ onClose();
26
+ }
27
+ };
28
+
29
+ if (isOpen) {
30
+ document.addEventListener('mousedown', handleOutsideClick);
31
+ document.addEventListener('keydown', handleEscape);
32
+ }
33
+
34
+ return () => {
35
+ document.removeEventListener('mousedown', handleOutsideClick);
36
+ document.removeEventListener('keydown', handleEscape);
37
+ };
38
+ }, [onClose, isOpen]);
39
+
40
+ if (!isOpen) return null;
41
+
42
+ return (
43
+ <div className='fixed inset-0 z-50 bg-black/50 flex items-center justify-center px-4'>
44
+ <div
45
+ ref={dialogRef}
46
+ className={clsx(
47
+ 'bg-white relative dark:bg-[#424242] rounded-lg w-full max-w-md shadow-lg',
48
+ className
49
+ )}
50
+ >
51
+ {showCloseIcon && (
52
+ <button
53
+ onClick={onClose}
54
+ className='absolute top-5 right-4 text-zinc-500 hover:text-zinc-900 dark:hover:text-white'
55
+ >
56
+ <IoClose className='w-5 h-5' />
57
+ </button>
58
+ )}
59
+ {children}
60
+ </div>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ // Slot Components
66
+ const Header: React.FC<{ children: React.ReactNode }> = ({ children }) => (
67
+ <div className='px-6 py-4 border-b border-zinc-200 dark:border-zinc-700'>
68
+ <h2 className='text-lg font-medium text-zinc-900 dark:text-white'>
69
+ {children}
70
+ </h2>
71
+ </div>
72
+ );
73
+
74
+ const Body: React.FC<{ children: React.ReactNode }> = ({ children }) => (
75
+ <div className='px-6 py-4'>{children}</div>
76
+ );
77
+
78
+ const Footer: React.FC<{ children: React.ReactNode }> = ({ children }) => (
79
+ <div className='flex justify-end gap-2 px-6 py-4 border-t border-zinc-200 dark:border-zinc-700'>
80
+ {children}
81
+ </div>
82
+ );
83
+
84
+ const Dialog = Object.assign(DialogComponent, {
85
+ Header,
86
+ Body,
87
+ Footer,
88
+ });
89
+
90
+ export default Dialog;
@@ -0,0 +1,7 @@
1
+ export type DialogProps = {
2
+ isOpen: boolean;
3
+ onClose: () => void;
4
+ children: React.ReactNode;
5
+ className?: string;
6
+ showCloseIcon?: boolean;
7
+ };
@@ -0,0 +1,3 @@
1
+ import Dialog from './Dialog';
2
+
3
+ export default Dialog;
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import Input from "./Input";
4
+
5
+ const meta: Meta<typeof Input> = {
6
+ title: "Components/Input",
7
+ component: Input,
8
+ tags: ["autodocs"],
9
+ argTypes: {
10
+ placeholder: {
11
+ control: "text",
12
+ description: "Placeholder text for the input field",
13
+ },
14
+ },
15
+ };
16
+
17
+ export default meta;
18
+
19
+ type Story = StoryObj<typeof Input>;
20
+
21
+ export const Primary: Story = {
22
+ args: {
23
+ label: "Input",
24
+ placeholder: "Enter text here",
25
+ type: "text",
26
+ },
27
+ };
28
+
29
+ export const Secondary: Story = {
30
+ args: {
31
+ placeholder: "Search...",
32
+ type: "dropdown",
33
+ },
34
+ };
@@ -1,9 +1,31 @@
1
- type InputProps = {
2
- placeholder?: string;
3
- };
4
-
5
- const Input: React.FC<InputProps> = ({ placeholder }) => {
6
- return <input placeholder={placeholder}>Input</input>;
7
- };
8
-
9
- export default Input;
1
+ import { cva } from 'class-variance-authority';
2
+ import clsx from 'clsx';
3
+ import React from 'react';
4
+
5
+ import { InputProps } from './Input.types';
6
+
7
+ const inputStyles = cva(
8
+ 'outline-none font-inter bg-transparent min-w-[160px] px-4 py-2 placeholder:text-[#3f3f3f] rounded-md border border-[#424242] focus:outline-none transition-colors duration-200 ease-in-out rounded-md disabled:cursor-not-allowed disabled:opacity-50'
9
+ );
10
+
11
+ const Input: React.FC<InputProps> = ({
12
+ placeholder,
13
+ type,
14
+ className,
15
+ label,
16
+ ...props
17
+ }) => {
18
+ return (
19
+ <div className={clsx('flex flex-col', className)}>
20
+ {label && <label className='mb-1 text-sm text-[#424242]'>{label}</label>}
21
+ <input
22
+ type={type}
23
+ className={clsx(inputStyles(), className)}
24
+ placeholder={placeholder}
25
+ {...props}
26
+ />
27
+ </div>
28
+ );
29
+ };
30
+
31
+ export default Input;
@@ -0,0 +1,6 @@
1
+ export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
2
+ className?: string;
3
+ placeholder?: string;
4
+ type: string;
5
+ label?: string;
6
+ };
@@ -0,0 +1,3 @@
1
+ import Input from './Input';
2
+
3
+ export default Input;
@@ -0,0 +1,53 @@
1
+ import React from "react";
2
+ import { Meta, StoryFn } from "@storybook/react";
3
+ import Table from "./Table";
4
+ import { TableColumn } from "./Table.types";
5
+
6
+ interface Person {
7
+ id: number;
8
+ name: string;
9
+ age: number;
10
+ email: string;
11
+ }
12
+
13
+ const columns: TableColumn<Person>[] = [
14
+ { key: "id", label: "ID", sortable: true },
15
+ { key: "name", label: "Name", sortable: true },
16
+ { key: "age", label: "Age", sortable: true },
17
+ {
18
+ key: "email",
19
+ label: "Email",
20
+ render: (value) => (
21
+ <a href={`mailto:${value}`} className="text-blue-400 underline">
22
+ {value}
23
+ </a>
24
+ ),
25
+ },
26
+ ];
27
+
28
+ const data: Person[] = [
29
+ { id: 1, name: "Alice", age: 28, email: "alice@example.com" },
30
+ { id: 2, name: "Bob", age: 34, email: "bob@example.com" },
31
+ { id: 3, name: "Charlie", age: 22, email: "charlie@example.com" },
32
+ ];
33
+
34
+ const meta: Meta<typeof Table> = {
35
+ title: "Components/Table",
36
+ component: Table,
37
+ argTypes: {
38
+ onRowClick: { action: "row clicked" },
39
+ },
40
+ };
41
+
42
+ export default meta;
43
+
44
+ const Template: StoryFn<React.ComponentProps<typeof Table<Person>>> = (
45
+ args
46
+ ) => <Table {...args} />;
47
+
48
+ export const Default = Template.bind({});
49
+ Default.args = {
50
+ columns,
51
+ data,
52
+ onRowClick: (row) => console.log("Row clicked:", row),
53
+ };
@@ -0,0 +1,104 @@
1
+ import clsx from 'clsx';
2
+ import React, { useState } from 'react';
3
+ import { FaArrowDown, FaArrowUp } from 'react-icons/fa';
4
+
5
+ import { TableProps } from './Table.types';
6
+
7
+ function Table<T extends Record<string, any>>({
8
+ columns,
9
+ data,
10
+ onRowClick,
11
+ className,
12
+ }: TableProps<T>) {
13
+ const [sortKey, setSortKey] = useState<keyof T | null>(null);
14
+ const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
15
+ const [selectedRowIndex, setSelectedRowIndex] = useState<number | null>(null);
16
+
17
+ const sortedData = React.useMemo(() => {
18
+ if (!sortKey) return data;
19
+
20
+ const sorted = [...data].sort((a, b) => {
21
+ if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1;
22
+ if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1;
23
+ return 0;
24
+ });
25
+ return sorted;
26
+ }, [data, sortKey, sortOrder]);
27
+
28
+ const handleSort = (key: keyof T, sortable?: boolean) => {
29
+ if (!sortable) return;
30
+
31
+ if (sortKey === key) {
32
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
33
+ } else {
34
+ setSortKey(key);
35
+ setSortOrder('asc');
36
+ }
37
+ };
38
+
39
+ console.log(data);
40
+
41
+ return (
42
+ <div
43
+ className={clsx(
44
+ 'overflow-auto rounded-lg border border-zinc-700',
45
+ className
46
+ )}
47
+ >
48
+ <table className='table-fixed min-w-full text-left text-sm text-zinc-100'>
49
+ <thead className='bg-zinc-900 border-b-2 border-[#5876EE] text-[#5876EE]'>
50
+ <tr>
51
+ {columns.map(({ key, label, sortable }) => (
52
+ <th
53
+ key={key as string}
54
+ scope='col'
55
+ className={clsx(
56
+ 'cursor-pointer select-none px-6 py-3 font-medium'
57
+ )}
58
+ onClick={() => handleSort(key, sortable)}
59
+ >
60
+ <div className='inline-flex items-center gap-1'>
61
+ {label}{' '}
62
+ {sortable &&
63
+ sortKey === key &&
64
+ (sortOrder === 'asc' ? (
65
+ <FaArrowUp className='h-3 w-3 font-light' />
66
+ ) : (
67
+ <FaArrowDown className='h-3 w-3 font-light' />
68
+ ))}
69
+ </div>
70
+ </th>
71
+ ))}
72
+ </tr>
73
+ </thead>
74
+ <tbody>
75
+ {sortedData.map((row, idx) => (
76
+ <tr
77
+ key={idx}
78
+ onClick={() => {
79
+ setSelectedRowIndex(idx);
80
+ onRowClick?.(row);
81
+ }}
82
+ className={clsx(
83
+ selectedRowIndex === idx
84
+ ? 'bg-[#BDC311]'
85
+ : idx % 2 === 0
86
+ ? 'bg-[#1D1B1D]'
87
+ : 'bg-[#424242]',
88
+ 'cursor-pointer'
89
+ )}
90
+ >
91
+ {columns.map(({ key, render }) => (
92
+ <td key={key as string} className='whitespace-nowrap px-6 py-4'>
93
+ {render ? render(row[key], row) : row[key]}
94
+ </td>
95
+ ))}
96
+ </tr>
97
+ ))}
98
+ </tbody>
99
+ </table>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ export default Table;
@@ -0,0 +1,13 @@
1
+ export interface TableColumn<T> {
2
+ key: keyof T;
3
+ label: string;
4
+ sortable?: boolean;
5
+ render?: (value: any, row: T) => React.ReactNode;
6
+ }
7
+
8
+ export interface TableProps<T> {
9
+ columns: TableColumn<T>[];
10
+ data: T[];
11
+ onRowClick?: (row: T) => void;
12
+ className?: string;
13
+ }
@@ -0,0 +1,3 @@
1
+ import Table from './Table';
2
+
3
+ export default Table;
@@ -1,2 +1,5 @@
1
- export { default as Button } from "./Button";
2
- export { default as Input } from "./Input";
1
+ export { default as Autocomplete } from './Autocomplete';
2
+ export { default as Button } from './Button';
3
+ export { default as Dialog } from './Dialog';
4
+ export { default as Input } from './Input';
5
+ export { default as Table } from './Table';
package/src/index.ts CHANGED
@@ -1,3 +1,3 @@
1
- import "./styles/tailwind.css"
2
-
3
- export * from "./components";
1
+ import './styles/tailwind.css';
2
+
3
+ export * from './components';
@@ -0,0 +1,81 @@
1
+ import "@testing-library/jest-dom";
2
+
3
+ import React from "react";
4
+ import { render, screen, fireEvent } from "@testing-library/react";
5
+ import Autocomplete from "../components/Autocomplete";
6
+ import {
7
+ Option,
8
+ AutocompleteProps,
9
+ } from "../components/Autocomplete/Autocomplete.types";
10
+
11
+ describe("Autocomplete component", () => {
12
+ const options: Option[] = [
13
+ { id: 1, name: "Apple" },
14
+ { id: 2, name: "Banana" },
15
+ { id: 3, name: "Orange" },
16
+ ];
17
+
18
+ const setup = (props?: Partial<AutocompleteProps>) => {
19
+ const defaultProps: AutocompleteProps = {
20
+ options,
21
+ selected: [],
22
+ setSelected: jest.fn(),
23
+ placeholder: "Search...",
24
+ showChosen: false,
25
+ ...props,
26
+ };
27
+
28
+ render(<Autocomplete {...defaultProps} />);
29
+ return defaultProps;
30
+ };
31
+
32
+ it("renders input with placeholder", () => {
33
+ setup();
34
+ expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument();
35
+ });
36
+
37
+ it("filters options based on input", () => {
38
+ setup();
39
+
40
+ const input = screen.getByPlaceholderText("Search...");
41
+ fireEvent.change(input, { target: { value: "ban" } });
42
+
43
+ expect(screen.getByText("Banana")).toBeInTheDocument();
44
+ expect(screen.queryByText("Apple")).not.toBeInTheDocument();
45
+ });
46
+
47
+ it("calls setSelected on item click (single selection)", () => {
48
+ const mockSetSelected = jest.fn();
49
+ setup({ setSelected: mockSetSelected });
50
+
51
+ const input = screen.getByPlaceholderText("Search...");
52
+ fireEvent.change(input, { target: { value: "oran" } });
53
+
54
+ const option = screen.getByText("Orange");
55
+ fireEvent.click(option);
56
+
57
+ expect(mockSetSelected).toHaveBeenCalledWith([{ id: 3, name: "Orange" }]);
58
+ });
59
+
60
+ it("supports multiple selections when showChosen is true", () => {
61
+ const mockSetSelected = jest.fn();
62
+ setup({ setSelected: mockSetSelected, showChosen: true });
63
+
64
+ const input = screen.getByPlaceholderText("Search...");
65
+ fireEvent.change(input, { target: { value: "app" } });
66
+
67
+ fireEvent.click(screen.getByText("Apple"));
68
+
69
+ expect(mockSetSelected).toHaveBeenCalledWith([{ id: 1, name: "Apple" }]);
70
+ });
71
+
72
+ it("does not show placeholder if item is selected and showChosen is false", () => {
73
+ setup({
74
+ selected: [{ id: 2, name: "Banana" }],
75
+ showChosen: false,
76
+ });
77
+
78
+ expect(screen.queryByPlaceholderText("Search...")).not.toBeInTheDocument();
79
+ expect(screen.getByText("Banana")).toBeInTheDocument();
80
+ });
81
+ });
@@ -1,37 +1,48 @@
1
- // src/tests/Button.test.tsx
2
-
3
- import React from "react";
4
1
  import "@testing-library/jest-dom";
5
- import { render, screen } from "@testing-library/react";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
6
3
  import Button from "../components/Button";
4
+ import type { IButtonProps } from "../components/Button/Button.types";
7
5
 
8
- describe("Button", () => {
9
- test("renders with default styles when no variant is provided", () => {
10
- render(<Button title="Default Button" />);
11
- const buttonElement = screen.getByRole("button", {
12
- name: /default button/i,
13
- });
6
+ describe("Button component", () => {
7
+ it("renders with given title", () => {
8
+ render(<Button variant="outline" title="Click Me" />);
9
+ expect(screen.getByText("Click Me")).toBeInTheDocument();
10
+ });
14
11
 
15
- expect(buttonElement.style.borderColor).toBe("");
16
- expect(buttonElement.style.color).toBe("");
17
- expect(buttonElement.style.backgroundColor).toBe("");
12
+ it("calls onClick when clicked", () => {
13
+ const handleClick = jest.fn();
14
+ render(<Button variant="outline" title="Click Me" onClick={handleClick} />);
15
+ fireEvent.click(screen.getByText("Click Me"));
16
+ expect(handleClick).toHaveBeenCalledTimes(1);
18
17
  });
19
18
 
20
- test("renders with custom style prop", () => {
21
- render(<Button title="Custom Button" style={{ padding: "10px" }} />);
22
- const buttonElement = screen.getByRole("button", {
23
- name: /custom button/i,
24
- });
19
+ it("is disabled when disabled prop is true", () => {
20
+ render(<Button variant="danger" title="Disabled" disabled />);
21
+ const button = screen.getByText("Disabled") as HTMLButtonElement;
22
+ expect(button).toBeDisabled();
23
+ });
25
24
 
26
- expect(buttonElement).toHaveStyle("padding: 10px");
25
+ it('applies the correct class for variant "primary"', () => {
26
+ render(<Button title="Primary" variant="primary" />);
27
+ const button = screen.getByText("Primary");
28
+ expect(button.className).toMatch(/bg-\[#5876EE\]/); // Tailwind class match
27
29
  });
28
30
 
29
- test("button click event works", () => {
30
- const handleClick = jest.fn();
31
- render(<Button title="Click Me" onClick={handleClick} />);
32
- const buttonElement = screen.getByRole("button", { name: /click me/i });
31
+ it('applies the correct class for size "large"', () => {
32
+ render(<Button variant="primary" title="Large Button" size="large" />);
33
+ const button = screen.getByText("Large Button");
34
+ expect(button.className).toMatch(/w-full/);
35
+ });
33
36
 
34
- buttonElement.click();
35
- expect(handleClick).toHaveBeenCalledTimes(1);
37
+ it("allows custom className via props", () => {
38
+ render(
39
+ <Button
40
+ variant="secondary"
41
+ title="Custom Class"
42
+ className="custom-class"
43
+ />
44
+ );
45
+ const button = screen.getByText("Custom Class");
46
+ expect(button.className).toMatch(/custom-class/);
36
47
  });
37
48
  });