@liguelead/design-system 0.0.30 → 0.0.32
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 +94 -18
- package/components/Badge/Badge.stories.tsx +114 -0
- package/components/Badge/Badge.styles.ts +36 -0
- package/components/Badge/Badge.tsx +23 -0
- package/components/Badge/Badge.types.ts +11 -0
- package/components/Badge/index.ts +2 -0
- package/components/Button/Button.appearance.ts +1 -1
- package/components/Button/Button.stories.tsx +99 -18
- package/components/Checkbox/Checkbox.stories.tsx +107 -7
- package/components/DatePicker/DatePicker.styles.ts +1 -0
- package/components/DatePicker/DatePicker.tsx +9 -10
- package/components/IconButton/IconButton.sizes.ts +7 -7
- package/components/IconButton/IconButton.tsx +0 -1
- package/components/InputOpt/InputOpt.stories.tsx +30 -44
- package/components/Select/Select.stories.tsx +80 -19
- package/components/Select/Select.tsx +7 -9
- package/components/Table/Datatable.stories.tsx +186 -0
- package/components/Table/Table.stories.tsx +127 -46
- package/components/Table/Table.styles.ts +83 -8
- package/components/Table/Table.tsx +292 -142
- package/components/Table/Table.types.ts +104 -12
- package/components/Table/components/ColumnVisibility/ColumnVisibility.style.ts +46 -0
- package/components/Table/components/ColumnVisibility/ColumnVisibility.tsx +55 -0
- package/components/Table/components/DatatableColumnFilterMenu/DatatableColumnFilterMenu.styles.ts +120 -0
- package/components/Table/components/DatatableColumnFilterMenu/DatatableColumnFilterMenu.tsx +228 -0
- package/components/Table/components/DatatableColumnFilterMenu/index.ts +1 -0
- package/components/Table/components/DatatableTopBar/DatatableTopBar.styles.ts +25 -0
- package/components/Table/components/DatatableTopBar/DatatableTopBar.tsx +89 -0
- package/components/Table/components/DatatableTopBar/index.ts +1 -0
- package/components/Table/components/SearchInput/SearchInput.tsx +30 -0
- package/components/Table/components/TableHeader/TableHeader.tsx +98 -0
- package/components/Table/components/TablePagination/TablePagination.tsx +78 -0
- package/components/Table/components/index.ts +6 -0
- package/components/Table/hooks/useDatatableFilters.ts +88 -0
- package/components/Table/stories.fixtures.ts +100 -0
- package/components/Table/tanstack-table.d.ts +10 -0
- package/components/Table/utils/dateRangeFilterFn.ts +33 -0
- package/components/Table/utils/index.ts +2 -1
- package/components/Tabs/Tabs.stories.tsx +152 -0
- package/components/Tabs/Tabs.styles.ts +12 -0
- package/components/Tabs/Tabs.tsx +34 -0
- package/components/Tabs/Tabs.types.ts +15 -0
- package/components/Tabs/index.ts +2 -0
- package/components/TextField/TextField.stories.tsx +135 -12
- package/components/index.ts +3 -0
- package/package.json +3 -2
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
import { ColumnFiltersState } from '@tanstack/react-table'
|
|
3
|
+
import {
|
|
4
|
+
ColumnFilterValue,
|
|
5
|
+
DatatableColumnFilters,
|
|
6
|
+
} from '../Table.types'
|
|
7
|
+
|
|
8
|
+
interface UseDatatableFiltersProps {
|
|
9
|
+
isServerMode: boolean
|
|
10
|
+
controlled?: DatatableColumnFilters
|
|
11
|
+
onChange?: (next: DatatableColumnFilters) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useDatatableFilters({
|
|
15
|
+
isServerMode,
|
|
16
|
+
controlled,
|
|
17
|
+
onChange,
|
|
18
|
+
}: UseDatatableFiltersProps) {
|
|
19
|
+
const isControlled = controlled !== undefined
|
|
20
|
+
const [internal, setInternal] = useState<DatatableColumnFilters>({})
|
|
21
|
+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
|
22
|
+
const [openFilterColumnId, setOpenFilterColumnId] = useState<string | null>(null)
|
|
23
|
+
|
|
24
|
+
const filters: DatatableColumnFilters = isControlled ? controlled! : internal
|
|
25
|
+
|
|
26
|
+
const updateFilters = useCallback((next: DatatableColumnFilters) => {
|
|
27
|
+
if (!isControlled) setInternal(next)
|
|
28
|
+
onChange?.(next)
|
|
29
|
+
}, [isControlled, onChange])
|
|
30
|
+
|
|
31
|
+
const handleFilterValueChange = useCallback((colId: string, value: ColumnFilterValue | undefined) => {
|
|
32
|
+
updateFilters({ ...filters, [colId]: value })
|
|
33
|
+
|
|
34
|
+
if (isServerMode) return
|
|
35
|
+
|
|
36
|
+
setColumnFilters((prev) => {
|
|
37
|
+
const rest = prev.filter((f) => f.id !== colId)
|
|
38
|
+
if (!value) return rest
|
|
39
|
+
if (value.type === 'text') return value.value ? [...rest, { id: colId, value: value.value }] : rest
|
|
40
|
+
if (value.type === 'select') return value.value != null ? [...rest, { id: colId, value: value.value }] : rest
|
|
41
|
+
if (value.type === 'dateRange') {
|
|
42
|
+
const { from, to } = value.value
|
|
43
|
+
return from || to ? [...rest, { id: colId, value: { from, to } }] : rest
|
|
44
|
+
}
|
|
45
|
+
return rest
|
|
46
|
+
})
|
|
47
|
+
}, [filters, isServerMode, updateFilters])
|
|
48
|
+
|
|
49
|
+
const handleClearColumnFilter = useCallback((colId: string) => {
|
|
50
|
+
const next = { ...filters }
|
|
51
|
+
delete next[colId]
|
|
52
|
+
updateFilters(next)
|
|
53
|
+
setColumnFilters((prev) => prev.filter((f) => f.id !== colId))
|
|
54
|
+
if (openFilterColumnId === colId) setOpenFilterColumnId(null)
|
|
55
|
+
}, [filters, openFilterColumnId, updateFilters])
|
|
56
|
+
|
|
57
|
+
const handleClearAllFilters = useCallback((onClearGlobal: () => void) => {
|
|
58
|
+
onClearGlobal()
|
|
59
|
+
setColumnFilters([])
|
|
60
|
+
setOpenFilterColumnId(null)
|
|
61
|
+
updateFilters({})
|
|
62
|
+
}, [updateFilters])
|
|
63
|
+
|
|
64
|
+
const handlePopoverOpenChange = useCallback((colId: string, isOpen: boolean) => {
|
|
65
|
+
setOpenFilterColumnId(isOpen ? colId : null)
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
const hasActiveFilter = useCallback((colId: string): boolean => {
|
|
69
|
+
const v = filters[colId]
|
|
70
|
+
if (!v) return false
|
|
71
|
+
if (v.type === 'text') return v.value !== ''
|
|
72
|
+
if (v.type === 'select') return v.value != null
|
|
73
|
+
if (v.type === 'dateRange') return v.value.from != null || v.value.to != null
|
|
74
|
+
return false
|
|
75
|
+
}, [filters])
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
filters,
|
|
79
|
+
columnFilters,
|
|
80
|
+
setColumnFilters,
|
|
81
|
+
openFilterColumnId,
|
|
82
|
+
handleFilterValueChange,
|
|
83
|
+
handleClearColumnFilter,
|
|
84
|
+
handleClearAllFilters,
|
|
85
|
+
handlePopoverOpenChange,
|
|
86
|
+
hasActiveFilter,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ColumnDef } from '@tanstack/react-table'
|
|
2
|
+
import { DatatableColumnMeta } from './Table.types'
|
|
3
|
+
|
|
4
|
+
export type OrderRow = {
|
|
5
|
+
id: string
|
|
6
|
+
customer: string
|
|
7
|
+
product: string
|
|
8
|
+
status: 'Aprovado' | 'Pendente' | 'Cancelado'
|
|
9
|
+
amount: string
|
|
10
|
+
createdAt: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ORDER_DATA: OrderRow[] = [
|
|
14
|
+
{ id: 'ORD-001', customer: 'João Silva', product: 'Plano Pro', status: 'Aprovado', amount: 'R$ 299,00', createdAt: '10/01/2026' },
|
|
15
|
+
{ id: 'ORD-002', customer: 'Maria Oliveira', product: 'Plano Basic', status: 'Pendente', amount: 'R$ 99,00', createdAt: '12/01/2026' },
|
|
16
|
+
{ id: 'ORD-003', customer: 'Carlos Souza', product: 'Plano Enterprise', status: 'Aprovado', amount: 'R$ 899,00', createdAt: '15/01/2026' },
|
|
17
|
+
{ id: 'ORD-004', customer: 'Fernanda Lima', product: 'Plano Pro', status: 'Cancelado', amount: 'R$ 299,00', createdAt: '18/01/2026' },
|
|
18
|
+
{ id: 'ORD-005', customer: 'Paulo Mendes', product: 'Plano Basic', status: 'Aprovado', amount: 'R$ 99,00', createdAt: '20/01/2026' },
|
|
19
|
+
{ id: 'ORD-006', customer: 'Aline Costa', product: 'Plano Pro', status: 'Pendente', amount: 'R$ 299,00', createdAt: '22/01/2026' },
|
|
20
|
+
{ id: 'ORD-007', customer: 'Rafael Santos', product: 'Plano Enterprise', status: 'Aprovado', amount: 'R$ 899,00', createdAt: '25/01/2026' },
|
|
21
|
+
{ id: 'ORD-008', customer: 'Juliana Freitas', product: 'Plano Basic', status: 'Aprovado', amount: 'R$ 99,00', createdAt: '28/01/2026' },
|
|
22
|
+
{ id: 'ORD-009', customer: 'Bruno Carvalho', product: 'Plano Pro', status: 'Cancelado', amount: 'R$ 299,00', createdAt: '01/02/2026' },
|
|
23
|
+
{ id: 'ORD-010', customer: 'Camila Rocha', product: 'Plano Enterprise', status: 'Pendente', amount: 'R$ 899,00', createdAt: '03/02/2026' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export const TABLE_COLUMNS: ColumnDef<OrderRow, unknown>[] = [
|
|
27
|
+
{ header: 'ID', accessorKey: 'id', enableSorting: true },
|
|
28
|
+
{ header: 'Cliente', accessorKey: 'customer', enableSorting: true },
|
|
29
|
+
{ header: 'Produto', accessorKey: 'product', enableSorting: true },
|
|
30
|
+
{ header: 'Status', accessorKey: 'status', enableSorting: true },
|
|
31
|
+
{ header: 'Valor', accessorKey: 'amount' },
|
|
32
|
+
{ header: 'Data', accessorKey: 'createdAt', enableSorting: true },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
export const DATATABLE_COLUMNS: ColumnDef<OrderRow, unknown>[] = [
|
|
36
|
+
{
|
|
37
|
+
header: 'ID',
|
|
38
|
+
accessorKey: 'id',
|
|
39
|
+
meta: {
|
|
40
|
+
filterType: 'text',
|
|
41
|
+
filterLabel: 'ID',
|
|
42
|
+
filterPlaceholder: 'Buscar pelo ID',
|
|
43
|
+
} satisfies DatatableColumnMeta,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
header: 'Cliente',
|
|
47
|
+
accessorKey: 'customer',
|
|
48
|
+
meta: {
|
|
49
|
+
filterType: 'text',
|
|
50
|
+
filterLabel: 'cliente',
|
|
51
|
+
filterPlaceholder: 'Buscar cliente',
|
|
52
|
+
} satisfies DatatableColumnMeta,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
header: 'Produto',
|
|
56
|
+
accessorKey: 'product',
|
|
57
|
+
meta: {
|
|
58
|
+
filterType: 'select',
|
|
59
|
+
filterLabel: 'produto',
|
|
60
|
+
filterPlaceholder: 'Selecione um produto',
|
|
61
|
+
selectOptions: [
|
|
62
|
+
{ label: 'Plano Basic', value: 'Plano Basic' },
|
|
63
|
+
{ label: 'Plano Pro', value: 'Plano Pro' },
|
|
64
|
+
{ label: 'Plano Enterprise', value: 'Plano Enterprise' },
|
|
65
|
+
],
|
|
66
|
+
} satisfies DatatableColumnMeta,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
header: 'Status',
|
|
70
|
+
accessorKey: 'status',
|
|
71
|
+
meta: {
|
|
72
|
+
filterType: 'select',
|
|
73
|
+
filterLabel: 'status',
|
|
74
|
+
filterPlaceholder: 'Selecione um status',
|
|
75
|
+
selectOptions: [
|
|
76
|
+
{ label: 'Aprovado', value: 'Aprovado' },
|
|
77
|
+
{ label: 'Pendente', value: 'Pendente' },
|
|
78
|
+
{ label: 'Cancelado', value: 'Cancelado' },
|
|
79
|
+
],
|
|
80
|
+
} satisfies DatatableColumnMeta,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
header: 'Valor',
|
|
84
|
+
accessorKey: 'amount',
|
|
85
|
+
meta: {
|
|
86
|
+
filterType: 'text',
|
|
87
|
+
filterLabel: 'valor',
|
|
88
|
+
filterPlaceholder: 'Buscar pelo valor',
|
|
89
|
+
} satisfies DatatableColumnMeta,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
header: 'Data',
|
|
93
|
+
accessorKey: 'createdAt',
|
|
94
|
+
filterFn: 'dateRange',
|
|
95
|
+
meta: {
|
|
96
|
+
filterType: 'dateRange',
|
|
97
|
+
filterLabel: 'Período',
|
|
98
|
+
} satisfies DatatableColumnMeta,
|
|
99
|
+
},
|
|
100
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import '@tanstack/react-table'
|
|
2
|
+
import { DatatableColumnMeta } from './Table.types'
|
|
3
|
+
|
|
4
|
+
declare module '@tanstack/react-table' {
|
|
5
|
+
interface FilterFns {
|
|
6
|
+
dateRange: import('@tanstack/react-table').FilterFn<unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ColumnMeta<TData extends RowData, TValue> extends DatatableColumnMeta {}
|
|
10
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { FilterFn, Row } from '@tanstack/react-table'
|
|
2
|
+
|
|
3
|
+
export const dateRangeFilterFn: FilterFn<unknown> = (
|
|
4
|
+
row: Row<unknown>,
|
|
5
|
+
columnId: string,
|
|
6
|
+
filterValue: { from?: Date; to?: Date }
|
|
7
|
+
) => {
|
|
8
|
+
const raw = row.getValue(columnId)
|
|
9
|
+
if (!raw) return true
|
|
10
|
+
|
|
11
|
+
let cellDate: Date | null = null
|
|
12
|
+
if (raw instanceof Date) {
|
|
13
|
+
cellDate = raw
|
|
14
|
+
} else if (typeof raw === 'string') {
|
|
15
|
+
const parts = raw.includes('/') ? raw.split('/').reverse() : raw.split('-')
|
|
16
|
+
const parsed = new Date(parts.join('-'))
|
|
17
|
+
if (!isNaN(parsed.getTime())) cellDate = parsed
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!cellDate) return true
|
|
21
|
+
|
|
22
|
+
const { from, to } = filterValue
|
|
23
|
+
if (from && cellDate < from) return false
|
|
24
|
+
if (to) {
|
|
25
|
+
const toEnd = new Date(to)
|
|
26
|
+
toEnd.setHours(23, 59, 59, 999)
|
|
27
|
+
if (cellDate > toEnd) return false
|
|
28
|
+
}
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
dateRangeFilterFn.autoRemove = (val: { from?: Date; to?: Date }) =>
|
|
33
|
+
!val || (!val.from && !val.to)
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {default as getPageNumbers} from './getPageNumbers'
|
|
1
|
+
export { default as getPageNumbers } from './getPageNumbers'
|
|
2
|
+
export { dateRangeFilterFn } from './dateRangeFilterFn'
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { HouseIcon, UserIcon, GearIcon, BellIcon } from '@phosphor-icons/react'
|
|
4
|
+
import Tabs from './Tabs'
|
|
5
|
+
import type { TabItem } from './Tabs.types'
|
|
6
|
+
|
|
7
|
+
const defaultItems: TabItem[] = [
|
|
8
|
+
{ key: 'tab1', label: 'Início' },
|
|
9
|
+
{ key: 'tab2', label: 'Perfil' },
|
|
10
|
+
{ key: 'tab3', label: 'Configurações' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const meta: Meta<typeof Tabs> = {
|
|
14
|
+
title: 'Navigation/Tabs',
|
|
15
|
+
component: Tabs,
|
|
16
|
+
parameters: {
|
|
17
|
+
layout: 'centered',
|
|
18
|
+
docs: {
|
|
19
|
+
description: {
|
|
20
|
+
component: `
|
|
21
|
+
Componente de navegação por abas baseado no design system LigueLead.
|
|
22
|
+
|
|
23
|
+
**Quando usar:**
|
|
24
|
+
- Alternar entre seções de conteúdo relacionadas
|
|
25
|
+
- Navegação secundária dentro de uma página
|
|
26
|
+
- Filtros de visualização (ex: Ativo / Inativo)
|
|
27
|
+
|
|
28
|
+
**Comportamento:**
|
|
29
|
+
- A tab ativa recebe estilo \`Solid/Primary\` com sombra
|
|
30
|
+
- As tabs inativas são \`Ghost/Neutral\` com fundo transparente
|
|
31
|
+
- Suporte a ícones à esquerda e direita
|
|
32
|
+
- Suporte a estado \`disabled\` por tab individual
|
|
33
|
+
- Prop \`fullWidth\` distribui as tabs igualmente no container
|
|
34
|
+
`,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
tags: ['autodocs'],
|
|
39
|
+
argTypes: {
|
|
40
|
+
activeKey: {
|
|
41
|
+
control: 'select',
|
|
42
|
+
options: ['tab1', 'tab2', 'tab3'],
|
|
43
|
+
description: 'Chave da tab ativa',
|
|
44
|
+
},
|
|
45
|
+
fullWidth: {
|
|
46
|
+
control: 'boolean',
|
|
47
|
+
description: 'Distribui as tabs igualmente na largura do container',
|
|
48
|
+
table: { defaultValue: { summary: 'false' } },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
render: (args) => {
|
|
52
|
+
const [active, setActive] = useState(args.activeKey ?? 'tab1')
|
|
53
|
+
return <Tabs {...args} activeKey={active} onChange={setActive} />
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default meta
|
|
58
|
+
type Story = StoryObj<typeof meta>
|
|
59
|
+
|
|
60
|
+
export const Default: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
items: defaultItems,
|
|
63
|
+
activeKey: 'tab1',
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const TabAtivaDireita: Story = {
|
|
68
|
+
name: 'Tab ativa à direita',
|
|
69
|
+
parameters: {
|
|
70
|
+
docs: {
|
|
71
|
+
description: { story: 'Variante com a tab ativa posicionada à direita (Variant=Right active do Figma).' },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
args: {
|
|
75
|
+
items: defaultItems,
|
|
76
|
+
activeKey: 'tab3',
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const ComIcones: Story = {
|
|
81
|
+
name: 'Com ícones',
|
|
82
|
+
parameters: {
|
|
83
|
+
docs: {
|
|
84
|
+
description: { story: 'Tabs com ícones à esquerda para reforçar o contexto visual.' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
args: {
|
|
88
|
+
items: [
|
|
89
|
+
{ key: 'home', label: 'Início', leftIcon: <HouseIcon size={16} /> },
|
|
90
|
+
{ key: 'user', label: 'Perfil', leftIcon: <UserIcon size={16} /> },
|
|
91
|
+
{ key: 'settings', label: 'Configurações', leftIcon: <GearIcon size={16} /> },
|
|
92
|
+
{ key: 'notifications', label: 'Notificações', leftIcon: <BellIcon size={16} /> },
|
|
93
|
+
],
|
|
94
|
+
activeKey: 'home',
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const ComDisabled: Story = {
|
|
99
|
+
name: 'Com tab desabilitada',
|
|
100
|
+
parameters: {
|
|
101
|
+
docs: {
|
|
102
|
+
description: { story: 'Tabs com uma ou mais opções desabilitadas.' },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
args: {
|
|
106
|
+
items: [
|
|
107
|
+
{ key: 'tab1', label: 'Ativo' },
|
|
108
|
+
{ key: 'tab2', label: 'Pendente' },
|
|
109
|
+
{ key: 'tab3', label: 'Bloqueado', disabled: true },
|
|
110
|
+
],
|
|
111
|
+
activeKey: 'tab1',
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const LarguraTotal: Story = {
|
|
116
|
+
name: 'Largura total',
|
|
117
|
+
parameters: {
|
|
118
|
+
layout: 'padded',
|
|
119
|
+
docs: {
|
|
120
|
+
description: { story: 'Use `fullWidth` para distribuir as tabs igualmente no container.' },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
render: () => {
|
|
124
|
+
const [active, setActive] = useState('tab1')
|
|
125
|
+
return (
|
|
126
|
+
<div style={{ width: 400 }}>
|
|
127
|
+
<Tabs
|
|
128
|
+
items={defaultItems}
|
|
129
|
+
activeKey={active}
|
|
130
|
+
onChange={setActive}
|
|
131
|
+
fullWidth
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const DuasAbas: Story = {
|
|
139
|
+
name: 'Duas abas',
|
|
140
|
+
parameters: {
|
|
141
|
+
docs: {
|
|
142
|
+
description: { story: 'Caso de uso comum: alternar entre dois estados (ex: Ativo / Inativo).' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
args: {
|
|
146
|
+
items: [
|
|
147
|
+
{ key: 'active', label: 'Ativo' },
|
|
148
|
+
{ key: 'inactive', label: 'Inativo' },
|
|
149
|
+
],
|
|
150
|
+
activeKey: 'active',
|
|
151
|
+
},
|
|
152
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import styled from 'styled-components'
|
|
2
|
+
import { spacing, radius } from '@liguelead/foundation'
|
|
3
|
+
import { parseColor } from '../../utils'
|
|
4
|
+
|
|
5
|
+
export const TabsContainer = styled.div<{ $fullWidth?: boolean }>`
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: stretch;
|
|
8
|
+
padding: ${spacing.spacing4}px;
|
|
9
|
+
border-radius: ${radius.radius4}px;
|
|
10
|
+
background-color: ${({ theme }) => parseColor(theme.colors.neutral200)};
|
|
11
|
+
width: ${({ $fullWidth }) => ($fullWidth ? '100%' : 'auto')};
|
|
12
|
+
`
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Button from '../Button'
|
|
3
|
+
import { TabsContainer } from './Tabs.styles'
|
|
4
|
+
import { TabsProps } from './Tabs.types'
|
|
5
|
+
|
|
6
|
+
const Tabs: React.FC<TabsProps> = ({
|
|
7
|
+
items,
|
|
8
|
+
activeKey,
|
|
9
|
+
onChange,
|
|
10
|
+
className,
|
|
11
|
+
fullWidth = false,
|
|
12
|
+
}) => {
|
|
13
|
+
return (
|
|
14
|
+
<TabsContainer $fullWidth={fullWidth} className={className} role="tablist">
|
|
15
|
+
{items.map(item => (
|
|
16
|
+
<Button
|
|
17
|
+
key={item.key}
|
|
18
|
+
variant={activeKey === item.key ? 'solid' : 'neutralGhost'}
|
|
19
|
+
color={activeKey === item.key ? 'primary' : undefined}
|
|
20
|
+
size="sm"
|
|
21
|
+
fullWidth={fullWidth}
|
|
22
|
+
disabled={item.disabled}
|
|
23
|
+
onClick={() => onChange(item.key)}
|
|
24
|
+
type="button">
|
|
25
|
+
{item.leftIcon}
|
|
26
|
+
{item.label}
|
|
27
|
+
{item.rightIcon}
|
|
28
|
+
</Button>
|
|
29
|
+
))}
|
|
30
|
+
</TabsContainer>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default Tabs
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface TabItem {
|
|
2
|
+
key: string
|
|
3
|
+
label: string
|
|
4
|
+
disabled?: boolean
|
|
5
|
+
leftIcon?: React.ReactNode
|
|
6
|
+
rightIcon?: React.ReactNode
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TabsProps {
|
|
10
|
+
items: TabItem[]
|
|
11
|
+
activeKey: string
|
|
12
|
+
onChange: (key: string) => void
|
|
13
|
+
className?: string
|
|
14
|
+
fullWidth?: boolean
|
|
15
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { EnvelopeIcon, LockIcon, EyeIcon, MagnifyingGlassIcon } from '@phosphor-icons/react'
|
|
2
3
|
import TextField from './TextField'
|
|
3
4
|
import { iconMap, iconOptions } from '../../../stories/utils/icons'
|
|
4
5
|
|
|
@@ -7,48 +8,69 @@ const meta: Meta<typeof TextField> = {
|
|
|
7
8
|
component: TextField,
|
|
8
9
|
parameters: {
|
|
9
10
|
layout: 'padded',
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component: `
|
|
14
|
+
Campo de texto para entrada de dados pelo usuário.
|
|
15
|
+
|
|
16
|
+
**Tamanhos:** \`sm\` · \`md\` · \`lg\`
|
|
17
|
+
|
|
18
|
+
**Tipos suportados:** \`text\` · \`password\` · \`email\` · \`number\` · \`file\`
|
|
19
|
+
|
|
20
|
+
**Boas práticas:**
|
|
21
|
+
- Sempre use \`label\` para identificar o campo
|
|
22
|
+
- Use \`helperText\` para instruções ou validações
|
|
23
|
+
- Use \`error\` para comunicar erros de validação
|
|
24
|
+
- Ícones devem reforçar o tipo de dado esperado
|
|
25
|
+
`,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
10
28
|
},
|
|
11
29
|
tags: ['autodocs'],
|
|
12
30
|
argTypes: {
|
|
13
31
|
label: {
|
|
14
32
|
control: 'text',
|
|
15
|
-
description: '
|
|
33
|
+
description: 'Label do campo',
|
|
16
34
|
},
|
|
17
35
|
placeholder: {
|
|
18
36
|
control: 'text',
|
|
19
|
-
description: '
|
|
37
|
+
description: 'Placeholder do input',
|
|
20
38
|
},
|
|
21
39
|
helperText: {
|
|
22
40
|
control: 'text',
|
|
23
|
-
description: '
|
|
41
|
+
description: 'Texto auxiliar abaixo do campo',
|
|
24
42
|
},
|
|
25
43
|
size: {
|
|
26
44
|
control: 'select',
|
|
27
45
|
options: ['sm', 'md', 'lg'],
|
|
28
|
-
description: '
|
|
46
|
+
description: 'Tamanho do campo',
|
|
47
|
+
table: { defaultValue: { summary: 'md' } },
|
|
29
48
|
},
|
|
30
49
|
type: {
|
|
31
50
|
control: 'select',
|
|
32
51
|
options: ['text', 'password', 'email', 'number', 'file'],
|
|
33
|
-
description: '
|
|
52
|
+
description: 'Tipo do input HTML',
|
|
53
|
+
table: { defaultValue: { summary: 'text' } },
|
|
34
54
|
},
|
|
35
55
|
disabled: {
|
|
36
56
|
control: 'boolean',
|
|
37
|
-
description: '
|
|
57
|
+
description: 'Desabilita o campo',
|
|
58
|
+
table: { defaultValue: { summary: 'false' } },
|
|
38
59
|
},
|
|
39
60
|
requiredSymbol: {
|
|
40
61
|
control: 'boolean',
|
|
41
|
-
description: '
|
|
62
|
+
description: 'Exibe asterisco de campo obrigatório',
|
|
63
|
+
table: { defaultValue: { summary: 'false' } },
|
|
42
64
|
},
|
|
43
65
|
leftIcon: {
|
|
44
66
|
control: 'select',
|
|
45
67
|
options: iconOptions,
|
|
46
|
-
description: '
|
|
68
|
+
description: 'Ícone à esquerda do campo',
|
|
47
69
|
},
|
|
48
70
|
rightIcon: {
|
|
49
71
|
control: 'select',
|
|
50
72
|
options: iconOptions,
|
|
51
|
-
description: '
|
|
73
|
+
description: 'Ícone à direita do campo',
|
|
52
74
|
},
|
|
53
75
|
},
|
|
54
76
|
}
|
|
@@ -58,15 +80,14 @@ type Story = StoryObj<typeof meta>
|
|
|
58
80
|
|
|
59
81
|
export const Default: Story = {
|
|
60
82
|
args: {
|
|
61
|
-
label: '
|
|
62
|
-
placeholder: '
|
|
83
|
+
label: 'Nome completo',
|
|
84
|
+
placeholder: 'Digite seu nome...',
|
|
63
85
|
leftIcon: 'none',
|
|
64
86
|
rightIcon: 'none',
|
|
65
87
|
},
|
|
66
88
|
render: (args) => {
|
|
67
89
|
const leftIcon = args.leftIcon === 'none' ? undefined : iconMap[args.leftIcon as keyof typeof iconMap]
|
|
68
90
|
const rightIcon = args.rightIcon === 'none' ? undefined : iconMap[args.rightIcon as keyof typeof iconMap]
|
|
69
|
-
|
|
70
91
|
return (
|
|
71
92
|
<TextField
|
|
72
93
|
label={args.label}
|
|
@@ -82,3 +103,105 @@ export const Default: Story = {
|
|
|
82
103
|
)
|
|
83
104
|
},
|
|
84
105
|
}
|
|
106
|
+
|
|
107
|
+
export const Tamanhos: Story = {
|
|
108
|
+
name: 'Tamanhos',
|
|
109
|
+
parameters: {
|
|
110
|
+
docs: { description: { story: 'Três tamanhos disponíveis para diferentes densidades de layout.' } },
|
|
111
|
+
},
|
|
112
|
+
render: () => (
|
|
113
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, width: 360 }}>
|
|
114
|
+
<TextField label="Pequeno" size="sm" placeholder="sm" />
|
|
115
|
+
<TextField label="Médio" size="md" placeholder="md" />
|
|
116
|
+
<TextField label="Grande" size="lg" placeholder="lg" />
|
|
117
|
+
</div>
|
|
118
|
+
),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const ComHelperText: Story = {
|
|
122
|
+
name: 'Com helper text',
|
|
123
|
+
parameters: {
|
|
124
|
+
docs: { description: { story: 'Use `helperText` para instruções de preenchimento.' } },
|
|
125
|
+
},
|
|
126
|
+
args: {
|
|
127
|
+
label: 'Senha',
|
|
128
|
+
placeholder: '••••••••',
|
|
129
|
+
helperText: 'Mínimo de 8 caracteres, com letras e números.',
|
|
130
|
+
type: 'password',
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const ComErro: Story = {
|
|
135
|
+
name: 'Estado de erro',
|
|
136
|
+
parameters: {
|
|
137
|
+
docs: { description: { story: 'Use `error` para comunicar falhas de validação.' } },
|
|
138
|
+
},
|
|
139
|
+
args: {
|
|
140
|
+
label: 'E-mail',
|
|
141
|
+
placeholder: 'seu@email.com',
|
|
142
|
+
error: { message: 'E-mail inválido.' },
|
|
143
|
+
value: 'email-invalido',
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const Desabilitado: Story = {
|
|
148
|
+
name: 'Estado desabilitado',
|
|
149
|
+
args: {
|
|
150
|
+
label: 'Campo desabilitado',
|
|
151
|
+
placeholder: 'Não editável',
|
|
152
|
+
disabled: true,
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const ComIcones: Story = {
|
|
157
|
+
name: 'Com ícones',
|
|
158
|
+
parameters: {
|
|
159
|
+
docs: { description: { story: 'Ícones reforçam o tipo de dado esperado.' } },
|
|
160
|
+
},
|
|
161
|
+
render: () => (
|
|
162
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, width: 360 }}>
|
|
163
|
+
<TextField label="E-mail" placeholder="seu@email.com" leftIcon={<EnvelopeIcon />} />
|
|
164
|
+
<TextField label="Senha" placeholder="••••••••" leftIcon={<LockIcon />} type="password" />
|
|
165
|
+
<TextField label="Buscar" placeholder="Pesquisar..." leftIcon={<MagnifyingGlassIcon />} />
|
|
166
|
+
<TextField label="Confirmar senha" placeholder="••••••••" rightIcon={<EyeIcon />} type="password" />
|
|
167
|
+
</div>
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const Obrigatorio: Story = {
|
|
172
|
+
name: 'Campo obrigatório',
|
|
173
|
+
parameters: {
|
|
174
|
+
docs: { description: { story: 'Use `requiredSymbol` para indicar campos obrigatórios.' } },
|
|
175
|
+
},
|
|
176
|
+
args: {
|
|
177
|
+
label: 'Nome completo',
|
|
178
|
+
placeholder: 'Digite seu nome...',
|
|
179
|
+
requiredSymbol: true,
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const ExemploLogin: Story = {
|
|
184
|
+
name: 'Exemplo: Formulário de login',
|
|
185
|
+
parameters: {
|
|
186
|
+
docs: { description: { story: 'Padrão de uso em formulários de autenticação.' } },
|
|
187
|
+
},
|
|
188
|
+
render: () => (
|
|
189
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, width: 360 }}>
|
|
190
|
+
<TextField
|
|
191
|
+
label="E-mail"
|
|
192
|
+
placeholder="seu@email.com"
|
|
193
|
+
leftIcon={<EnvelopeIcon />}
|
|
194
|
+
requiredSymbol
|
|
195
|
+
type="email"
|
|
196
|
+
/>
|
|
197
|
+
<TextField
|
|
198
|
+
label="Senha"
|
|
199
|
+
placeholder="••••••••"
|
|
200
|
+
leftIcon={<LockIcon />}
|
|
201
|
+
requiredSymbol
|
|
202
|
+
type="password"
|
|
203
|
+
helperText="Mínimo de 8 caracteres."
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
),
|
|
207
|
+
}
|
package/components/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as Alert } from './Alert'
|
|
2
|
+
export { Badge } from './Badge'
|
|
3
|
+
export type { TBadgeProps } from './Badge'
|
|
2
4
|
export { default as Button } from './Button'
|
|
3
5
|
export { default as Checkbox } from './Checkbox'
|
|
4
6
|
export { default as DatePicker } from './DatePicker'
|
|
@@ -17,3 +19,4 @@ export { ToastProvider, Toaster } from './Toaster'
|
|
|
17
19
|
export { default as Dialog } from './Dialog'
|
|
18
20
|
export { Combobox } from './Combobox'
|
|
19
21
|
export { default as Table } from './Table'
|
|
22
|
+
export { default as Tabs } from './Tabs'
|