@liguelead/design-system 0.0.28 → 0.0.30
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/components/Alert/Alert.stories.tsx +60 -0
- package/components/Button/Button.appearance.ts +2 -2
- package/components/Button/Button.stories.tsx +75 -0
- package/components/Checkbox/Checkbox.stories.tsx +42 -0
- package/components/Combobox/Combobox.stories.tsx +112 -0
- package/components/Combobox/Combobox.styles.ts +7 -3
- package/components/Combobox/Combobox.tsx +3 -10
- package/components/DatePicker/DatePicker.stories.tsx +108 -0
- package/components/DatePicker/DatePicker.styles.ts +394 -50
- package/components/DatePicker/DatePicker.tsx +353 -118
- package/components/DatePicker/DatePicker.types.ts +40 -24
- package/components/Dialog/Dialog.stories.tsx +63 -0
- package/components/IconButton/IconButton.stories.tsx +49 -0
- package/components/InputOpt/InputOpt.stories.tsx +61 -0
- package/components/LinkButton/LinkButton.stories.tsx +88 -0
- package/components/PageWrapper/PageWrapper.stories.tsx +153 -0
- package/components/RadioButton/RadioButton.stories.tsx +70 -0
- package/components/RequiredAsterisk/RequiredAsterisk.stories.tsx +44 -0
- package/components/SegmentedButton/SegmentedButton.stories.tsx +182 -0
- package/components/Select/Select.stories.tsx +63 -0
- package/components/Table/Table.stories.tsx +53 -0
- package/components/Table/Table.styles.ts +76 -0
- package/components/Table/Table.tsx +157 -0
- package/components/Table/Table.types.ts +16 -0
- package/components/Table/index.ts +1 -0
- package/components/Table/utils/getPageNumbers.ts +35 -0
- package/components/Table/utils/index.ts +1 -0
- package/components/Table/utils/types.ts +4 -0
- package/components/Text/Text.stories.tsx +61 -0
- package/components/Text/Text.types.ts +1 -0
- package/components/TextField/TextField.stories.tsx +84 -0
- package/components/Toaster/Toaster.stories.tsx +129 -0
- package/components/Toaster/Toaster.ts +19 -0
- package/components/Toaster/ToasterProvider.tsx +9 -1
- package/components/Wizard/Wizard.stories.tsx +186 -0
- package/components/index.ts +1 -0
- package/package.json +16 -3
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {Meta, StoryObj} from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import Table from './Table';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Table> = {
|
|
6
|
+
title: 'Data Display/Table',
|
|
7
|
+
component: Table,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'fullscreen',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
argTypes: {
|
|
13
|
+
data: {
|
|
14
|
+
control: 'object',
|
|
15
|
+
description: 'Array of data objects to be displayed in the table',
|
|
16
|
+
},
|
|
17
|
+
columns: {
|
|
18
|
+
control: 'object',
|
|
19
|
+
description: 'Column definitions for the table',
|
|
20
|
+
},
|
|
21
|
+
footerText: {
|
|
22
|
+
control: {type: undefined},
|
|
23
|
+
description:
|
|
24
|
+
'Function to render custom footer text, receives total and filtered counts',
|
|
25
|
+
},
|
|
26
|
+
showPagination: {
|
|
27
|
+
control: 'boolean',
|
|
28
|
+
description: 'Whether to enable pagination for the table',
|
|
29
|
+
},
|
|
30
|
+
pageSize: {
|
|
31
|
+
control: 'number',
|
|
32
|
+
description: 'Initial number of rows per page when pagination is enabled',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
type Story = StoryObj<typeof meta>;
|
|
39
|
+
|
|
40
|
+
export const Default: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
data: [
|
|
43
|
+
{name: 'John Doe', email: 'john.doe@example.com'},
|
|
44
|
+
{name: 'Jane Smith', email: 'jane.smith@example.com'},
|
|
45
|
+
],
|
|
46
|
+
columns: [
|
|
47
|
+
{header: 'Name', accessorKey: 'name'},
|
|
48
|
+
{header: 'Email', accessorKey: 'email'},
|
|
49
|
+
],
|
|
50
|
+
showPagination: true,
|
|
51
|
+
pageSize: 10,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import styled from 'styled-components'
|
|
2
|
+
import { spacing, fontSize, fontWeight } from '@liguelead/foundation'
|
|
3
|
+
import { parseColor } from '../../utils'
|
|
4
|
+
|
|
5
|
+
export const TableWrapper = styled.div`
|
|
6
|
+
background: ${({ theme }) => parseColor(theme.colors.white)};
|
|
7
|
+
`
|
|
8
|
+
|
|
9
|
+
export const StyledTable = styled.table`
|
|
10
|
+
width: 100%;
|
|
11
|
+
`
|
|
12
|
+
|
|
13
|
+
export const TableFooter = styled.div<{ $hasPagination: boolean }>`
|
|
14
|
+
display: flex;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
flex-direction: ${({ $hasPagination }) => ($hasPagination ? 'column-reverse' : 'row')};
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: ${spacing.spacing8}px;
|
|
19
|
+
padding: ${spacing.spacing16}px;
|
|
20
|
+
font-size: ${fontSize.fontSize14}px;
|
|
21
|
+
border-top: 1px solid #e5e7eb;
|
|
22
|
+
`
|
|
23
|
+
|
|
24
|
+
export const FooterText = styled.div`
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
`
|
|
28
|
+
|
|
29
|
+
export const PaginationContainer = styled.div`
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
export const PaginationControls = styled.div`
|
|
35
|
+
display: flex;
|
|
36
|
+
gap: ${spacing.spacing4}px;
|
|
37
|
+
`
|
|
38
|
+
|
|
39
|
+
export const PaginationElipsis = styled.span`
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
padding: ${spacing.spacing8}px;
|
|
44
|
+
|
|
45
|
+
& svg {
|
|
46
|
+
fill: ${({ theme }) => parseColor(theme.colors.primary)};
|
|
47
|
+
font-size: ${fontSize.fontSize14}px;
|
|
48
|
+
color: ${({ theme }) => parseColor(theme.colors.primary)};
|
|
49
|
+
}
|
|
50
|
+
`
|
|
51
|
+
|
|
52
|
+
export const Td = styled.td`
|
|
53
|
+
padding: ${spacing.spacing16}px ${spacing.spacing8}px;
|
|
54
|
+
font-size: ${fontSize.fontSize14}px;
|
|
55
|
+
color: ${({ theme }) => parseColor(theme.colors.textDark)};
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
export const Tr = styled.tr`
|
|
59
|
+
&:not(:last-child) td {
|
|
60
|
+
border-bottom: 1px solid #e5e7eb;
|
|
61
|
+
}
|
|
62
|
+
`
|
|
63
|
+
export const Th = styled.th`
|
|
64
|
+
padding: 12px 16px;
|
|
65
|
+
color: ${({ theme }) => parseColor(theme.colors.textMedium)};
|
|
66
|
+
font-size: ${fontSize.fontSize14}px;
|
|
67
|
+
font-weight: ${fontWeight.fontWeight600};
|
|
68
|
+
border-bottom: 1px solid ${({ theme }) => parseColor(theme.colors.neutral300)};
|
|
69
|
+
`
|
|
70
|
+
|
|
71
|
+
export const EmptyMessage = styled.td`
|
|
72
|
+
padding: ${spacing.spacing16}px;
|
|
73
|
+
color: ${({ theme }) => parseColor(theme.colors.textMedium)};
|
|
74
|
+
font-size: ${fontSize.fontSize14}px;
|
|
75
|
+
text-align: center;
|
|
76
|
+
`
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
flexRender,
|
|
4
|
+
getCoreRowModel,
|
|
5
|
+
getPaginationRowModel,
|
|
6
|
+
useReactTable,
|
|
7
|
+
} from '@tanstack/react-table'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
CaretLeftIcon,
|
|
11
|
+
CaretRightIcon,
|
|
12
|
+
DotsThreeIcon,
|
|
13
|
+
} from '@phosphor-icons/react'
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
EmptyMessage,
|
|
17
|
+
FooterText,
|
|
18
|
+
PaginationContainer,
|
|
19
|
+
PaginationControls,
|
|
20
|
+
PaginationElipsis,
|
|
21
|
+
StyledTable,
|
|
22
|
+
TableFooter,
|
|
23
|
+
TableWrapper,
|
|
24
|
+
Td,
|
|
25
|
+
Th,
|
|
26
|
+
Tr,
|
|
27
|
+
} from './Table.styles'
|
|
28
|
+
import { TTableProps } from './Table.types'
|
|
29
|
+
import { getPageNumbers } from './utils'
|
|
30
|
+
import IconButton from '../IconButton'
|
|
31
|
+
import Button from '../Button'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
function Table<TData>({
|
|
35
|
+
data,
|
|
36
|
+
columns,
|
|
37
|
+
emptyTitle = 'Nenhum registro encontrado.',
|
|
38
|
+
footerText,
|
|
39
|
+
pageSize = 10,
|
|
40
|
+
showPagination = true,
|
|
41
|
+
}: TTableProps<TData>) {
|
|
42
|
+
const [pagination, setPagination] = useState({
|
|
43
|
+
pageIndex: 0,
|
|
44
|
+
pageSize,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const table = useReactTable({
|
|
48
|
+
data,
|
|
49
|
+
columns,
|
|
50
|
+
state: {
|
|
51
|
+
pagination,
|
|
52
|
+
},
|
|
53
|
+
onPaginationChange: setPagination,
|
|
54
|
+
getCoreRowModel: getCoreRowModel(),
|
|
55
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const total = table.getFilteredRowModel().rows.length;
|
|
59
|
+
const pageCount = table.getPageCount();
|
|
60
|
+
const hasPagination = showPagination && pageCount > 1;
|
|
61
|
+
const currentPage = pagination.pageIndex + 1;
|
|
62
|
+
const pageNumbersArray = getPageNumbers({ currentPage, pageCount });
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<TableWrapper>
|
|
66
|
+
<StyledTable>
|
|
67
|
+
<thead>
|
|
68
|
+
{table.getHeaderGroups().map((hg) => (
|
|
69
|
+
<tr key={hg.id}>
|
|
70
|
+
{hg.headers.map((header) => (
|
|
71
|
+
<Th key={header.id}>
|
|
72
|
+
{flexRender(
|
|
73
|
+
header.column.columnDef.header,
|
|
74
|
+
header.getContext()
|
|
75
|
+
)}
|
|
76
|
+
</Th>
|
|
77
|
+
))}
|
|
78
|
+
</tr>
|
|
79
|
+
))}
|
|
80
|
+
</thead>
|
|
81
|
+
|
|
82
|
+
<tbody>
|
|
83
|
+
{table.getRowModel().rows.length === 0 ? (
|
|
84
|
+
<tr>
|
|
85
|
+
<EmptyMessage colSpan={columns.length}>{emptyTitle}</EmptyMessage>
|
|
86
|
+
</tr>
|
|
87
|
+
) : (
|
|
88
|
+
table.getRowModel().rows.map((row) => (
|
|
89
|
+
<Tr key={row.id}>
|
|
90
|
+
{row.getVisibleCells().map((cell) => (
|
|
91
|
+
<Td key={cell.id}>
|
|
92
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
93
|
+
</Td>
|
|
94
|
+
))}
|
|
95
|
+
</Tr>
|
|
96
|
+
))
|
|
97
|
+
)}
|
|
98
|
+
</tbody>
|
|
99
|
+
</StyledTable>
|
|
100
|
+
|
|
101
|
+
{(footerText || hasPagination) && total > 0 && (
|
|
102
|
+
<TableFooter $hasPagination={hasPagination}>
|
|
103
|
+
{footerText && (
|
|
104
|
+
<FooterText>
|
|
105
|
+
{footerText({
|
|
106
|
+
total: data.length,
|
|
107
|
+
filtered: total,
|
|
108
|
+
})}
|
|
109
|
+
</FooterText>
|
|
110
|
+
)}
|
|
111
|
+
{hasPagination && (
|
|
112
|
+
<PaginationContainer>
|
|
113
|
+
<PaginationControls>
|
|
114
|
+
<IconButton
|
|
115
|
+
variant="outline"
|
|
116
|
+
onClick={() => table.previousPage()}
|
|
117
|
+
disabled={!table.getCanPreviousPage()}
|
|
118
|
+
>
|
|
119
|
+
<CaretLeftIcon />
|
|
120
|
+
</IconButton>
|
|
121
|
+
{pageNumbersArray.map((page, index) =>
|
|
122
|
+
page === "ellipsis" ? (
|
|
123
|
+
<PaginationElipsis key={`ellipsis-${index}`}>
|
|
124
|
+
<DotsThreeIcon size={16} />
|
|
125
|
+
</PaginationElipsis>
|
|
126
|
+
) : (
|
|
127
|
+
<Button
|
|
128
|
+
key={page}
|
|
129
|
+
size="sm"
|
|
130
|
+
variant={page === currentPage ? "solid" : "ghost"}
|
|
131
|
+
onClick={() => {
|
|
132
|
+
if (typeof page === "number") {
|
|
133
|
+
table.setPageIndex(page - 1);
|
|
134
|
+
}
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
{page}
|
|
138
|
+
</Button>
|
|
139
|
+
)
|
|
140
|
+
)}
|
|
141
|
+
<IconButton
|
|
142
|
+
variant="outline"
|
|
143
|
+
onClick={() => table.nextPage()}
|
|
144
|
+
disabled={!table.getCanNextPage()}
|
|
145
|
+
>
|
|
146
|
+
<CaretRightIcon />
|
|
147
|
+
</IconButton>
|
|
148
|
+
</PaginationControls>
|
|
149
|
+
</PaginationContainer>
|
|
150
|
+
)}
|
|
151
|
+
</TableFooter>
|
|
152
|
+
)}
|
|
153
|
+
</TableWrapper>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default Table
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ColumnDef } from '@tanstack/react-table';
|
|
2
|
+
|
|
3
|
+
export type TTableProps<TData> = {
|
|
4
|
+
data: TData[]
|
|
5
|
+
columns: ColumnDef<TData, unknown>[]
|
|
6
|
+
|
|
7
|
+
emptyTitle?: string
|
|
8
|
+
pageSize?: number
|
|
9
|
+
showPagination?: boolean
|
|
10
|
+
|
|
11
|
+
footerText?: (info: {
|
|
12
|
+
total: number
|
|
13
|
+
filtered: number
|
|
14
|
+
}) => string
|
|
15
|
+
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Table'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TPageNumbersProps } from './types'
|
|
2
|
+
|
|
3
|
+
const getPageNumbers = ({ currentPage, pageCount }: TPageNumbersProps) => {
|
|
4
|
+
if (pageCount <= 7) {
|
|
5
|
+
return Array.from({ length: pageCount }, (_, idx) => idx + 1)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (currentPage <= 3) {
|
|
9
|
+
return [1, 2, 3, 'ellipsis', pageCount]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (currentPage >= pageCount - 2) {
|
|
13
|
+
return [
|
|
14
|
+
1,
|
|
15
|
+
'ellipsis',
|
|
16
|
+
pageCount - 4,
|
|
17
|
+
pageCount - 3,
|
|
18
|
+
pageCount - 2,
|
|
19
|
+
pageCount - 1,
|
|
20
|
+
pageCount,
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
1,
|
|
26
|
+
'ellipsis',
|
|
27
|
+
currentPage - 1,
|
|
28
|
+
currentPage,
|
|
29
|
+
currentPage + 1,
|
|
30
|
+
'ellipsis',
|
|
31
|
+
pageCount,
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default getPageNumbers
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {default as getPageNumbers} from './getPageNumbers'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import Text from './Text'
|
|
3
|
+
import { themes } from '@liguelead/foundation'
|
|
4
|
+
import { fontWeight } from '@liguelead/foundation'
|
|
5
|
+
|
|
6
|
+
const themeColors = Object.keys(themes.spa.colors)
|
|
7
|
+
const themeTypography = Object.keys(themes.spa.typography)
|
|
8
|
+
const themeFontWeights = Object.keys(fontWeight)
|
|
9
|
+
|
|
10
|
+
const meta: Meta<typeof Text> = {
|
|
11
|
+
title: 'Data Display/Text',
|
|
12
|
+
component: Text,
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: 'centered',
|
|
15
|
+
},
|
|
16
|
+
tags: ['autodocs'],
|
|
17
|
+
argTypes: {
|
|
18
|
+
size: {
|
|
19
|
+
control: 'select',
|
|
20
|
+
options: themeTypography,
|
|
21
|
+
description: 'Text size variant'
|
|
22
|
+
},
|
|
23
|
+
tag: {
|
|
24
|
+
control: 'select',
|
|
25
|
+
options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'label', 'small', 'b', 'i', 'em', 'td', 'div', 'a'],
|
|
26
|
+
description: 'HTML tag to render'
|
|
27
|
+
},
|
|
28
|
+
weight: {
|
|
29
|
+
control: 'select',
|
|
30
|
+
options: themeFontWeights,
|
|
31
|
+
description: 'Font weight'
|
|
32
|
+
},
|
|
33
|
+
align: {
|
|
34
|
+
control: 'select',
|
|
35
|
+
options: ['left', 'center', 'right', 'justify'],
|
|
36
|
+
description: 'Text alignment'
|
|
37
|
+
},
|
|
38
|
+
color: {
|
|
39
|
+
control: 'select',
|
|
40
|
+
options: themeColors,
|
|
41
|
+
description: 'Text color'
|
|
42
|
+
},
|
|
43
|
+
isTitle: {
|
|
44
|
+
control: 'boolean',
|
|
45
|
+
description: 'Whether text is a title'
|
|
46
|
+
},
|
|
47
|
+
children: {
|
|
48
|
+
control: 'text',
|
|
49
|
+
description: 'Text content'
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default meta
|
|
55
|
+
type Story = StoryObj<typeof meta>
|
|
56
|
+
|
|
57
|
+
export const Default: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
children: 'This is a text component',
|
|
60
|
+
},
|
|
61
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import TextField from './TextField'
|
|
3
|
+
import { iconMap, iconOptions } from '../../../stories/utils/icons'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof TextField> = {
|
|
6
|
+
title: 'Form/TextField',
|
|
7
|
+
component: TextField,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'padded',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
argTypes: {
|
|
13
|
+
label: {
|
|
14
|
+
control: 'text',
|
|
15
|
+
description: 'Field label'
|
|
16
|
+
},
|
|
17
|
+
placeholder: {
|
|
18
|
+
control: 'text',
|
|
19
|
+
description: 'Input placeholder'
|
|
20
|
+
},
|
|
21
|
+
helperText: {
|
|
22
|
+
control: 'text',
|
|
23
|
+
description: 'Helper text below input'
|
|
24
|
+
},
|
|
25
|
+
size: {
|
|
26
|
+
control: 'select',
|
|
27
|
+
options: ['sm', 'md', 'lg'],
|
|
28
|
+
description: 'Input size'
|
|
29
|
+
},
|
|
30
|
+
type: {
|
|
31
|
+
control: 'select',
|
|
32
|
+
options: ['text', 'password', 'email', 'number', 'file'],
|
|
33
|
+
description: 'Input type'
|
|
34
|
+
},
|
|
35
|
+
disabled: {
|
|
36
|
+
control: 'boolean',
|
|
37
|
+
description: 'Whether input is disabled'
|
|
38
|
+
},
|
|
39
|
+
requiredSymbol: {
|
|
40
|
+
control: 'boolean',
|
|
41
|
+
description: 'Show required asterisk'
|
|
42
|
+
},
|
|
43
|
+
leftIcon: {
|
|
44
|
+
control: 'select',
|
|
45
|
+
options: iconOptions,
|
|
46
|
+
description: 'Left icon',
|
|
47
|
+
},
|
|
48
|
+
rightIcon: {
|
|
49
|
+
control: 'select',
|
|
50
|
+
options: iconOptions,
|
|
51
|
+
description: 'Right icon',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default meta
|
|
57
|
+
type Story = StoryObj<typeof meta>
|
|
58
|
+
|
|
59
|
+
export const Default: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
label: 'Label',
|
|
62
|
+
placeholder: 'Enter text...',
|
|
63
|
+
leftIcon: 'none',
|
|
64
|
+
rightIcon: 'none',
|
|
65
|
+
},
|
|
66
|
+
render: (args) => {
|
|
67
|
+
const leftIcon = args.leftIcon === 'none' ? undefined : iconMap[args.leftIcon as keyof typeof iconMap]
|
|
68
|
+
const rightIcon = args.rightIcon === 'none' ? undefined : iconMap[args.rightIcon as keyof typeof iconMap]
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<TextField
|
|
72
|
+
label={args.label}
|
|
73
|
+
placeholder={args.placeholder}
|
|
74
|
+
helperText={args.helperText}
|
|
75
|
+
size={args.size}
|
|
76
|
+
type={args.type}
|
|
77
|
+
disabled={args.disabled}
|
|
78
|
+
requiredSymbol={args.requiredSymbol}
|
|
79
|
+
leftIcon={leftIcon}
|
|
80
|
+
rightIcon={rightIcon}
|
|
81
|
+
/>
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import Button from '../Button'
|
|
4
|
+
import { ToastType, triggerToast } from './Toaster'
|
|
5
|
+
import ToasterProvider from './ToasterProvider'
|
|
6
|
+
|
|
7
|
+
interface ToasterStoryArgs {
|
|
8
|
+
type: ToastType
|
|
9
|
+
message: string
|
|
10
|
+
autoClose: number
|
|
11
|
+
position: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center'
|
|
12
|
+
theme: 'light' | 'dark' | 'colored'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ToasterDemo = () => <div />
|
|
16
|
+
|
|
17
|
+
const meta = {
|
|
18
|
+
title: 'Feedback/Toaster',
|
|
19
|
+
component: ToasterDemo,
|
|
20
|
+
parameters: {
|
|
21
|
+
layout: 'centered',
|
|
22
|
+
},
|
|
23
|
+
tags: ['autodocs'],
|
|
24
|
+
argTypes: {
|
|
25
|
+
type: {
|
|
26
|
+
control: 'select',
|
|
27
|
+
options: ['success', 'error', 'warning', 'info'],
|
|
28
|
+
description: 'Type of toast notification'
|
|
29
|
+
},
|
|
30
|
+
message: {
|
|
31
|
+
control: 'text',
|
|
32
|
+
description: 'Toast message content'
|
|
33
|
+
},
|
|
34
|
+
autoClose: {
|
|
35
|
+
control: 'number',
|
|
36
|
+
description: 'Auto close time in milliseconds'
|
|
37
|
+
},
|
|
38
|
+
position: {
|
|
39
|
+
control: 'select',
|
|
40
|
+
options: ['top-right', 'top-left', 'bottom-right', 'bottom-left', 'top-center', 'bottom-center'],
|
|
41
|
+
description: 'Toast position'
|
|
42
|
+
},
|
|
43
|
+
theme: {
|
|
44
|
+
control: 'select',
|
|
45
|
+
options: ['light', 'dark', 'colored'],
|
|
46
|
+
description: 'Toast theme'
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
} satisfies Meta<typeof ToasterDemo>
|
|
50
|
+
|
|
51
|
+
export default meta
|
|
52
|
+
type Story = StoryObj<ToasterStoryArgs>
|
|
53
|
+
|
|
54
|
+
export const Default: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
type: 'success',
|
|
57
|
+
message: 'This is a toast message!',
|
|
58
|
+
autoClose: 5000,
|
|
59
|
+
position: 'top-right',
|
|
60
|
+
theme: 'light'
|
|
61
|
+
},
|
|
62
|
+
render: (args) => (
|
|
63
|
+
<ToasterProvider>
|
|
64
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
65
|
+
<Button
|
|
66
|
+
onClick={() =>
|
|
67
|
+
triggerToast(args.type, args.message, {
|
|
68
|
+
autoClose: args.autoClose,
|
|
69
|
+
position: args.position,
|
|
70
|
+
theme: args.theme
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
color="primary"
|
|
74
|
+
>
|
|
75
|
+
Show Toast
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
</ToasterProvider>
|
|
79
|
+
),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const Types: Story = {
|
|
83
|
+
render: () => (
|
|
84
|
+
<ToasterProvider>
|
|
85
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
86
|
+
<Button onClick={() => triggerToast('success', 'Success message!')}>
|
|
87
|
+
Success Toast
|
|
88
|
+
</Button>
|
|
89
|
+
<Button onClick={() => triggerToast('error', 'Error message!')}>
|
|
90
|
+
Error Toast
|
|
91
|
+
</Button>
|
|
92
|
+
<Button onClick={() => triggerToast('warning', 'Warning message!')}>
|
|
93
|
+
Warning Toast
|
|
94
|
+
</Button>
|
|
95
|
+
<Button onClick={() => triggerToast('info', 'Info message!')}>
|
|
96
|
+
Info Toast
|
|
97
|
+
</Button>
|
|
98
|
+
</div>
|
|
99
|
+
</ToasterProvider>
|
|
100
|
+
),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const Positions: Story = {
|
|
104
|
+
render: () => {
|
|
105
|
+
const [position, setPosition] = useState<'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'>('top-right')
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<ToasterProvider>
|
|
109
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
110
|
+
<select
|
|
111
|
+
value={position}
|
|
112
|
+
onChange={(e) => setPosition(e.target.value as any)}
|
|
113
|
+
style={{ padding: '8px', marginBottom: '12px' }}
|
|
114
|
+
>
|
|
115
|
+
<option value="top-right">Top Right</option>
|
|
116
|
+
<option value="top-left">Top Left</option>
|
|
117
|
+
<option value="bottom-right">Bottom Right</option>
|
|
118
|
+
<option value="bottom-left">Bottom Left</option>
|
|
119
|
+
</select>
|
|
120
|
+
<Button
|
|
121
|
+
onClick={() => triggerToast('info', `Toast at ${position}`, { position })}
|
|
122
|
+
>
|
|
123
|
+
Show Toast at {position}
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</ToasterProvider>
|
|
127
|
+
)
|
|
128
|
+
},
|
|
129
|
+
}
|
|
@@ -9,6 +9,25 @@ const defaultOptions: ToastOptions = {
|
|
|
9
9
|
theme: 'light'
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export type ToastType = 'success' | 'error' | 'warning' | 'info'
|
|
13
|
+
|
|
14
|
+
export const triggerToast = (type: ToastType, message: string, options?: ToastOptions) => {
|
|
15
|
+
const mergedOptions = { ...defaultOptions, ...options }
|
|
16
|
+
|
|
17
|
+
switch (type) {
|
|
18
|
+
case 'success':
|
|
19
|
+
return toast.success(message, mergedOptions)
|
|
20
|
+
case 'error':
|
|
21
|
+
return toast.error(message, mergedOptions)
|
|
22
|
+
case 'warning':
|
|
23
|
+
return toast.warning(message, mergedOptions)
|
|
24
|
+
case 'info':
|
|
25
|
+
return toast.info(message, mergedOptions)
|
|
26
|
+
default:
|
|
27
|
+
return toast(message, mergedOptions)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
12
31
|
const Toaster = {
|
|
13
32
|
success: (message: string, options?: ToastOptions) =>
|
|
14
33
|
toast.success(message, { ...defaultOptions, ...options }),
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import 'react-toastify/dist/ReactToastify.css'
|
|
2
2
|
import { ToasterStyledContainer } from './Toaster.style'
|
|
3
|
+
import { ReactNode } from 'react'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
interface ToasterProviderProps {
|
|
6
|
+
children?: ReactNode
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ToastProvider = ({ children }: ToasterProviderProps) => {
|
|
5
10
|
return(
|
|
11
|
+
<>
|
|
12
|
+
{children}
|
|
6
13
|
<ToasterStyledContainer newestOnTop />
|
|
14
|
+
</>
|
|
7
15
|
)
|
|
8
16
|
}
|
|
9
17
|
|