@yusufalperendumlu/component-library 0.0.5 → 0.0.7
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/.storybook/main.ts +1 -1
- package/.storybook/preview.ts +2 -2
- package/dist/cjs/index.css +1 -1
- package/dist/cjs/index.css.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.css +1 -1
- package/dist/esm/index.css.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +63 -7
- package/dist/tailwind.css +1 -1
- package/eslint.config.js +23 -0
- package/jest.config.ts +13 -14
- package/package.json +18 -2
- package/postcss.config.js +6 -6
- package/prettier.config.js +84 -0
- package/src/components/Autocomplete/Autocomplete.stories.tsx +65 -0
- package/src/components/Autocomplete/Autocomplete.tsx +127 -0
- package/src/components/Autocomplete/Autocomplete.types.ts +14 -0
- package/src/components/Autocomplete/index.ts +3 -0
- package/src/components/Button/Button.stories.tsx +52 -52
- package/src/components/Button/Button.tsx +55 -50
- package/src/components/Button/Button.types.ts +7 -7
- package/src/components/Button/index.ts +3 -3
- package/src/components/Dialog/Dialog.stories.tsx +102 -0
- package/src/components/Dialog/Dialog.tsx +90 -0
- package/src/components/Dialog/Dialog.types.ts +7 -0
- package/src/components/Dialog/index.ts +3 -0
- package/src/components/Input/Input.stories.tsx +34 -0
- package/src/components/Input/Input.tsx +31 -9
- package/src/components/Input/Input.types.ts +6 -0
- package/src/components/Input/index.ts +3 -0
- package/src/components/Table/Table.stories.tsx +53 -0
- package/src/components/Table/Table.tsx +104 -0
- package/src/components/Table/Table.types.ts +13 -0
- package/src/components/Table/index.ts +3 -0
- package/src/components/Toast/Toast.stories.tsx +44 -0
- package/src/components/Toast/Toast.tsx +70 -0
- package/src/components/Toast/Toast.types.ts +7 -0
- package/src/components/Toast/icons.tsx +9 -0
- package/src/components/Toast/index.ts +0 -0
- package/src/components/index.ts +5 -2
- package/src/index.ts +3 -3
- package/src/styles/tailwind.css +124 -124
- package/src/tests/Autocomplete.test.tsx +81 -0
- package/src/tests/Button.test.tsx +48 -48
- package/src/tests/Dialog.test.tsx +86 -0
- package/src/tests/Input.test.tsx +42 -0
- package/src/tests/Table.test.tsx +55 -0
- package/src/tests/styleMock.ts +1 -1
- package/tailwind.config.js +3 -2
- package/tsconfig.json +20 -30
- package/src/components/Input/index.tsx +0 -3
@@ -1,50 +1,55 @@
|
|
1
|
-
import
|
2
|
-
import
|
3
|
-
import
|
4
|
-
|
5
|
-
import { IButtonProps } from
|
6
|
-
|
7
|
-
const buttonVariants = cva(
|
8
|
-
|
9
|
-
{
|
10
|
-
variants: {
|
11
|
-
variant: {
|
12
|
-
primary:
|
13
|
-
secondary:
|
14
|
-
outline:
|
15
|
-
danger:
|
16
|
-
},
|
17
|
-
size: {
|
18
|
-
small:
|
19
|
-
medium:
|
20
|
-
large:
|
21
|
-
},
|
22
|
-
border: {
|
23
|
-
primary:
|
24
|
-
secondary:
|
25
|
-
outline:
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
}
|
49
|
-
|
50
|
-
|
1
|
+
import { cva } from 'class-variance-authority';
|
2
|
+
import clsx from 'clsx';
|
3
|
+
import React from 'react';
|
4
|
+
|
5
|
+
import { IButtonProps } from './Button.types';
|
6
|
+
|
7
|
+
const buttonVariants = cva(
|
8
|
+
'outline-none min-w-[70px] font-inter transition-all font-[12px] px-4 py-2 gap-2 rounded-md duration-300 ease-in-out disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:text-inherit',
|
9
|
+
{
|
10
|
+
variants: {
|
11
|
+
variant: {
|
12
|
+
primary: 'bg-[#5876EE] text-white shadow-xs hover:bg-[#4a66d9]',
|
13
|
+
secondary: 'bg-[#BDC311] text-[#424242] shadow-xs hover:bg-[#a8b00e]',
|
14
|
+
outline: 'bg-transparent shadow-xs hover:bg-[#f5f5f5]',
|
15
|
+
danger: 'bg-[#FF0000] text-white shadow-xs hover:bg-[#d40000]',
|
16
|
+
},
|
17
|
+
size: {
|
18
|
+
small: 'w-fit px-3 text-sm',
|
19
|
+
medium: 'w-1/2 px-4 text-base',
|
20
|
+
large: 'w-full px-5 text-lg',
|
21
|
+
},
|
22
|
+
border: {
|
23
|
+
primary: 'border border-[#5876EE] hover:bg-[#4a66d9]',
|
24
|
+
secondary: 'border border-[#BDC311] hover:bg-[#a8b00e]',
|
25
|
+
outline:
|
26
|
+
'border border-[#424242] hover:bg-[#E2E2E2] dark:border-[#E2E2E2',
|
27
|
+
danger: 'border border-[#FF0000] text-[#FF0000] hover:bg-[#FF0000]',
|
28
|
+
},
|
29
|
+
},
|
30
|
+
defaultVariants: {
|
31
|
+
variant: 'primary',
|
32
|
+
size: 'medium',
|
33
|
+
},
|
34
|
+
}
|
35
|
+
);
|
36
|
+
|
37
|
+
const Button = ({
|
38
|
+
variant,
|
39
|
+
size,
|
40
|
+
border,
|
41
|
+
title,
|
42
|
+
className,
|
43
|
+
...props
|
44
|
+
}: IButtonProps) => {
|
45
|
+
return (
|
46
|
+
<button
|
47
|
+
className={clsx(buttonVariants({ variant, size, border }), className)}
|
48
|
+
{...props}
|
49
|
+
>
|
50
|
+
{title}
|
51
|
+
</button>
|
52
|
+
);
|
53
|
+
};
|
54
|
+
|
55
|
+
export default Button;
|
@@ -1,7 +1,7 @@
|
|
1
|
-
export type IButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
2
|
-
className?: string;
|
3
|
-
variant:
|
4
|
-
size?:
|
5
|
-
border?:
|
6
|
-
title: string;
|
7
|
-
};
|
1
|
+
export type IButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
2
|
+
className?: string;
|
3
|
+
variant: 'primary' | 'secondary' | 'outline' | 'danger';
|
4
|
+
size?: 'small' | 'medium' | 'large';
|
5
|
+
border?: 'primary' | 'secondary' | 'outline' | 'danger';
|
6
|
+
title: string;
|
7
|
+
};
|
@@ -1,3 +1,3 @@
|
|
1
|
-
import Button from
|
2
|
-
|
3
|
-
export default Button;
|
1
|
+
import Button from './Button';
|
2
|
+
|
3
|
+
export default Button;
|
@@ -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,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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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,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>>> = args => (
|
45
|
+
<Table {...args} />
|
46
|
+
);
|
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,44 @@
|
|
1
|
+
// Toast.stories.tsx
|
2
|
+
import React from 'react';
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
4
|
+
import Toast from './Toast';
|
5
|
+
|
6
|
+
const meta: Meta<typeof Toast> = {
|
7
|
+
title: 'Components/Toast',
|
8
|
+
component: Toast,
|
9
|
+
tags: ['autodocs'],
|
10
|
+
argTypes: {
|
11
|
+
type: {
|
12
|
+
control: 'radio',
|
13
|
+
options: ['success', 'error'],
|
14
|
+
},
|
15
|
+
position: {
|
16
|
+
control: 'radio',
|
17
|
+
options: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
18
|
+
},
|
19
|
+
},
|
20
|
+
};
|
21
|
+
|
22
|
+
export default meta;
|
23
|
+
|
24
|
+
type Story = StoryObj<typeof Toast>;
|
25
|
+
|
26
|
+
export const SuccessTopRight: Story = {
|
27
|
+
args: {
|
28
|
+
title: 'Success',
|
29
|
+
description: 'Operation was successful!',
|
30
|
+
duration: 3000,
|
31
|
+
type: 'success',
|
32
|
+
position: 'top-right',
|
33
|
+
},
|
34
|
+
};
|
35
|
+
|
36
|
+
export const ErrorBottomLeft: Story = {
|
37
|
+
args: {
|
38
|
+
title: 'Error',
|
39
|
+
description: 'Something went wrong!',
|
40
|
+
duration: 3000,
|
41
|
+
type: 'error',
|
42
|
+
position: 'bottom-left',
|
43
|
+
},
|
44
|
+
};
|