@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.
Files changed (46) hide show
  1. package/components/Alert/Alert.stories.tsx +94 -18
  2. package/components/Badge/Badge.stories.tsx +114 -0
  3. package/components/Badge/Badge.styles.ts +36 -0
  4. package/components/Badge/Badge.tsx +23 -0
  5. package/components/Badge/Badge.types.ts +11 -0
  6. package/components/Badge/index.ts +2 -0
  7. package/components/Button/Button.appearance.ts +1 -1
  8. package/components/Button/Button.stories.tsx +99 -18
  9. package/components/Checkbox/Checkbox.stories.tsx +107 -7
  10. package/components/DatePicker/DatePicker.styles.ts +1 -0
  11. package/components/DatePicker/DatePicker.tsx +9 -10
  12. package/components/IconButton/IconButton.sizes.ts +7 -7
  13. package/components/IconButton/IconButton.tsx +0 -1
  14. package/components/InputOpt/InputOpt.stories.tsx +30 -44
  15. package/components/Select/Select.stories.tsx +80 -19
  16. package/components/Select/Select.tsx +7 -9
  17. package/components/Table/Datatable.stories.tsx +186 -0
  18. package/components/Table/Table.stories.tsx +127 -46
  19. package/components/Table/Table.styles.ts +83 -8
  20. package/components/Table/Table.tsx +292 -142
  21. package/components/Table/Table.types.ts +104 -12
  22. package/components/Table/components/ColumnVisibility/ColumnVisibility.style.ts +46 -0
  23. package/components/Table/components/ColumnVisibility/ColumnVisibility.tsx +55 -0
  24. package/components/Table/components/DatatableColumnFilterMenu/DatatableColumnFilterMenu.styles.ts +120 -0
  25. package/components/Table/components/DatatableColumnFilterMenu/DatatableColumnFilterMenu.tsx +228 -0
  26. package/components/Table/components/DatatableColumnFilterMenu/index.ts +1 -0
  27. package/components/Table/components/DatatableTopBar/DatatableTopBar.styles.ts +25 -0
  28. package/components/Table/components/DatatableTopBar/DatatableTopBar.tsx +89 -0
  29. package/components/Table/components/DatatableTopBar/index.ts +1 -0
  30. package/components/Table/components/SearchInput/SearchInput.tsx +30 -0
  31. package/components/Table/components/TableHeader/TableHeader.tsx +98 -0
  32. package/components/Table/components/TablePagination/TablePagination.tsx +78 -0
  33. package/components/Table/components/index.ts +6 -0
  34. package/components/Table/hooks/useDatatableFilters.ts +88 -0
  35. package/components/Table/stories.fixtures.ts +100 -0
  36. package/components/Table/tanstack-table.d.ts +10 -0
  37. package/components/Table/utils/dateRangeFilterFn.ts +33 -0
  38. package/components/Table/utils/index.ts +2 -1
  39. package/components/Tabs/Tabs.stories.tsx +152 -0
  40. package/components/Tabs/Tabs.styles.ts +12 -0
  41. package/components/Tabs/Tabs.tsx +34 -0
  42. package/components/Tabs/Tabs.types.ts +15 -0
  43. package/components/Tabs/index.ts +2 -0
  44. package/components/TextField/TextField.stories.tsx +135 -12
  45. package/components/index.ts +3 -0
  46. package/package.json +3 -2
@@ -84,7 +84,7 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(
84
84
  const baseId = useId()
85
85
  const errorId = `${baseId}-error`
86
86
  const inputRef = useRef<HTMLInputElement | null>(null)
87
- const [inputValue, setInputValue] = useState(() =>
87
+ const [inputValue, setInputValue] = useState(() =>
88
88
  !isRange && value ? format(value, 'dd/MM/yyyy') : ''
89
89
  )
90
90
 
@@ -128,8 +128,8 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(
128
128
  : typeof error === 'object' &&
129
129
  error !== null &&
130
130
  'message' in error
131
- ? String((error as { message?: string }).message ?? '')
132
- : undefined
131
+ ? String((error as { message?: string }).message ?? '')
132
+ : undefined
133
133
 
134
134
  const handleSingleSelect = (date?: Date) => {
135
135
  if (disabled) return
@@ -226,11 +226,11 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(
226
226
  onRangeChange?.(nextRange)
227
227
  const serialized = nextRange
228
228
  ? JSON.stringify({
229
- from: nextRange.from
230
- ? serializeDate(nextRange.from)
231
- : null,
232
- to: nextRange.to ? serializeDate(nextRange.to) : null
233
- })
229
+ from: nextRange.from
230
+ ? serializeDate(nextRange.from)
231
+ : null,
232
+ to: nextRange.to ? serializeDate(nextRange.to) : null
233
+ })
234
234
  : ''
235
235
 
236
236
  register?.onChange({
@@ -247,7 +247,6 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(
247
247
  mode="range"
248
248
  selected={rangeValue}
249
249
  onSelect={handleRangeSelect}
250
-
251
250
  captionLayout={appliedCaptionLayout}
252
251
  navLayout="around"
253
252
  fromYear={resolvedStartYear}
@@ -355,7 +354,7 @@ const DatePicker = forwardRef<HTMLInputElement, DatePickerProps>(
355
354
  </DatePickerTriggerWrapper>
356
355
 
357
356
  <Popover.Portal>
358
- <DatePickerContent sideOffset={8} align="start">
357
+ <DatePickerContent sideOffset={2} align="center">
359
358
  {picker}
360
359
  </DatePickerContent>
361
360
  </Popover.Portal>
@@ -4,11 +4,11 @@ import { ButtonSizeTypes } from '../Button/Button.types'
4
4
  export const IconButtonSizes = (size: ButtonSizeTypes) => {
5
5
  const sizes = {
6
6
  sm: `
7
- padding: ${spacing.spacing12}px ${spacing.spacing12}px;
7
+ padding: ${spacing.spacing8}px ${spacing.spacing8}px;
8
8
  border-radius: ${spacing.spacing4}px;
9
9
  & svg {
10
- width: ${spacing.spacing16}px !important;
11
- height: ${spacing.spacing16}px;
10
+ width: ${spacing.spacing20}px;
11
+ height: ${spacing.spacing20}px;
12
12
  }
13
13
  `,
14
14
  md: `
@@ -16,16 +16,16 @@ export const IconButtonSizes = (size: ButtonSizeTypes) => {
16
16
  border-radius: ${spacing.spacing4}px;
17
17
  min-width: ${spacing.spacing36}px;
18
18
  & svg {
19
- width: ${spacing.spacing16}px;
20
- height: ${spacing.spacing16}px;
19
+ width: ${spacing.spacing20}px;
20
+ height: ${spacing.spacing20}px;
21
21
  }
22
22
  `,
23
23
  lg: `
24
24
  padding: ${spacing.spacing12}px ${spacing.spacing12}px;
25
25
  border-radius: ${spacing.spacing4}px;
26
26
  & svg {
27
- width: ${spacing.spacing16}px;
28
- height: ${spacing.spacing16}px;
27
+ width: ${spacing.spacing20}px;
28
+ height: ${spacing.spacing20}px;
29
29
  }
30
30
  `
31
31
  }
@@ -32,7 +32,6 @@ const IconButton: React.FC<ButtonProps> = ({
32
32
  }
33
33
  setRipples(prev => [...prev, newRipple])
34
34
 
35
- // Chama o onClick fornecido
36
35
  if (onClick) {
37
36
  onClick(e)
38
37
  }
@@ -4,48 +4,18 @@ import InputOpt from './InputOpt'
4
4
  const meta: Meta<typeof InputOpt> = {
5
5
  title: 'Form/InputOpt',
6
6
  component: InputOpt,
7
- parameters: {
8
- layout: 'centered',
9
- },
7
+ parameters: { layout: 'centered' },
10
8
  tags: ['autodocs'],
11
9
  argTypes: {
12
- length: {
13
- control: 'number',
14
- description: 'Number of input fields'
15
- },
16
- label: {
17
- control: 'text',
18
- description: 'Input label'
19
- },
20
- helperText: {
21
- control: 'text',
22
- description: 'Helper text below input'
23
- },
24
- placeholderChar: {
25
- control: 'text',
26
- description: 'Placeholder character for empty fields'
27
- },
28
- inputMode: {
29
- control: 'select',
30
- options: ['numeric', 'text'],
31
- description: 'Input mode for mobile keyboards'
32
- },
33
- autoFocus: {
34
- control: 'boolean',
35
- description: 'Auto focus first input on mount'
36
- },
37
- disabled: {
38
- control: 'boolean',
39
- description: 'Whether inputs are disabled'
40
- },
41
- requiredSymbol: {
42
- control: 'boolean',
43
- description: 'Show required asterisk'
44
- },
45
- error: {
46
- control: 'boolean',
47
- description: 'Whether inputs have error state'
48
- }
10
+ length: { control: 'number', description: 'Número de campos' },
11
+ label: { control: 'text', description: 'Label do campo' },
12
+ helperText: { control: 'text', description: 'Texto auxiliar abaixo do campo' },
13
+ placeholderChar: { control: 'text', description: 'Caractere placeholder para campos vazios' },
14
+ inputMode: { control: 'select', options: ['numeric', 'text'], description: 'Modo do teclado mobile' },
15
+ autoFocus: { control: 'boolean', description: 'Foca o primeiro campo automaticamente' },
16
+ disabled: { control: 'boolean', description: 'Desabilita todos os campos' },
17
+ requiredSymbol: { control: 'boolean', description: 'Exibe asterisco de campo obrigatório' },
18
+ error: { control: 'boolean', description: 'Estado de erro' },
49
19
  },
50
20
  }
51
21
 
@@ -53,9 +23,25 @@ export default meta
53
23
  type Story = StoryObj<typeof meta>
54
24
 
55
25
  export const Default: Story = {
56
- args: {
57
- label: 'Verification Code',
58
- length: 6,
59
- },
26
+ args: { label: 'Código de verificação', length: 6 },
60
27
  }
61
28
 
29
+ export const Desabilitado: Story = {
30
+ name: 'Estado desabilitado',
31
+ args: { label: 'Código de verificação', length: 6, disabled: true },
32
+ }
33
+
34
+ export const ComErro: Story = {
35
+ name: 'Estado de erro',
36
+ args: { label: 'Código de verificação', length: 6, error: true, helperText: 'Código inválido. Tente novamente.' },
37
+ }
38
+
39
+ export const Alfanumerico: Story = {
40
+ name: 'Alfanumérico',
41
+ args: { label: 'Código de convite', length: 8, inputMode: 'text', placeholderChar: '_' },
42
+ }
43
+
44
+ export const PIN4Digitos: Story = {
45
+ name: 'PIN de 4 dígitos',
46
+ args: { label: 'PIN de acesso', length: 4, inputMode: 'numeric' },
47
+ }
@@ -1,45 +1,69 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import Select from './Select'
3
3
 
4
+ const defaultOptions = [
5
+ { label: 'São Paulo', value: 'sp' },
6
+ { label: 'Rio de Janeiro', value: 'rj' },
7
+ { label: 'Minas Gerais', value: 'mg' },
8
+ { label: 'Bahia', value: 'ba' },
9
+ { label: 'Paraná', value: 'pr' },
10
+ ]
11
+
4
12
  const meta: Meta<typeof Select> = {
5
13
  title: 'Form/Select',
6
14
  component: Select,
7
15
  parameters: {
8
16
  layout: 'centered',
17
+ docs: {
18
+ description: {
19
+ component: `
20
+ Campo de seleção para escolha de uma opção em uma lista.
21
+
22
+ **Quando usar:**
23
+ - Listas com mais de 5 opções (abaixo disso, prefira RadioButton)
24
+ - Quando o espaço é limitado
25
+
26
+ **Boas práticas:**
27
+ - Sempre defina um \`placeholder\` descritivo
28
+ - Use \`helperText\` para orientar a escolha
29
+ - Use \`requiredSymbol\` em campos obrigatórios
30
+ `,
31
+ },
32
+ },
9
33
  },
10
34
  tags: ['autodocs'],
11
- args: {
12
- options: [],
13
- },
14
35
  argTypes: {
15
36
  label: {
16
37
  control: 'text',
17
- description: 'Select label',
38
+ description: 'Label do select',
18
39
  },
19
40
  placeholder: {
20
41
  control: 'text',
21
- description: 'Select placeholder',
42
+ description: 'Placeholder quando nenhuma opção está selecionada',
22
43
  },
23
44
  helperText: {
24
45
  control: 'text',
25
- description: 'Helper text below select',
46
+ description: 'Texto auxiliar abaixo do select',
26
47
  },
27
48
  size: {
28
49
  control: 'select',
29
50
  options: ['sm', 'md', 'lg'],
30
- description: 'Select size',
51
+ description: 'Tamanho do select',
52
+ table: { defaultValue: { summary: 'md' } },
31
53
  },
32
54
  disabled: {
33
55
  control: 'boolean',
34
- description: 'Whether select is disabled',
56
+ description: 'Desabilita o select',
57
+ table: { defaultValue: { summary: 'false' } },
35
58
  },
36
59
  requiredSymbol: {
37
60
  control: 'boolean',
38
- description: 'Show required asterisk',
61
+ description: 'Exibe asterisco de campo obrigatório',
62
+ table: { defaultValue: { summary: 'false' } },
39
63
  },
40
64
  options: {
41
65
  control: 'object',
42
- description: 'Options for the select dropdown',
66
+ description: 'Array de opções `{ label, value }`',
43
67
  },
44
68
  },
45
69
  }
@@ -47,17 +71,54 @@ const meta: Meta<typeof Select> = {
47
71
  export default meta
48
72
  type Story = StoryObj<typeof meta>
49
73
 
50
- const defaultOptions = [
51
- { label: 'Option 1', value: 'option1' },
52
- { label: 'Option 2', value: 'option2' },
53
- { label: 'Option 3', value: 'option3' },
54
- { label: 'Option 4', value: 'option4' },
55
- ]
56
-
57
74
  export const Default: Story = {
58
75
  args: {
59
- label: 'Select an option',
60
- placeholder: 'Choose...',
76
+ label: 'Estado',
77
+ placeholder: 'Selecione um estado...',
78
+ options: defaultOptions,
79
+ },
80
+ }
81
+
82
+ export const Tamanhos: Story = {
83
+ name: 'Tamanhos',
84
+ parameters: {
85
+ docs: { description: { story: 'Três tamanhos disponíveis para diferentes densidades de layout.' } },
86
+ },
87
+ render: () => (
88
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16, width: 320 }}>
89
+ <Select label="Pequeno" size="sm" placeholder="sm" options={defaultOptions} />
90
+ <Select label="Médio" size="md" placeholder="md" options={defaultOptions} />
91
+ <Select label="Grande" size="lg" placeholder="lg" options={defaultOptions} />
92
+ </div>
93
+ ),
94
+ }
95
+
96
+ export const ComHelperText: Story = {
97
+ name: 'Com helper text',
98
+ args: {
99
+ label: 'Categoria',
100
+ placeholder: 'Selecione...',
101
+ helperText: 'Escolha a categoria que melhor descreve o item.',
102
+ options: defaultOptions,
103
+ },
104
+ }
105
+
106
+ export const Desabilitado: Story = {
107
+ name: 'Estado desabilitado',
108
+ args: {
109
+ label: 'País',
110
+ placeholder: 'Brasil',
111
+ disabled: true,
112
+ options: defaultOptions,
113
+ },
114
+ }
115
+
116
+ export const Obrigatorio: Story = {
117
+ name: 'Campo obrigatório',
118
+ args: {
119
+ label: 'Estado',
120
+ placeholder: 'Selecione um estado...',
121
+ requiredSymbol: true,
61
122
  options: defaultOptions,
62
123
  },
63
124
  }
@@ -42,15 +42,13 @@ const Select = forwardRef<HTMLInputElement, SelectProps>(
42
42
  const [open, setOpen] = useState(false)
43
43
 
44
44
  const handleOnChange = (value: string) => {
45
- if (value) {
46
- setSelectValue(value)
47
- register?.onChange({
48
- target: {
49
- name: register.name,
50
- value
51
- }
52
- })
53
- }
45
+ setSelectValue(value)
46
+ register?.onChange({
47
+ target: {
48
+ name: register.name,
49
+ value
50
+ }
51
+ })
54
52
  }
55
53
 
56
54
  const state = getState(disabled, !!error)
@@ -0,0 +1,186 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { useState } from 'react'
3
+ import Table from './Table'
4
+ import { DatatableColumnFilters, TableQueryState } from './Table.types'
5
+ import { ORDER_DATA, DATATABLE_COLUMNS } from './stories.fixtures'
6
+
7
+ const meta: Meta = {
8
+ title: 'Data Display/Datatable',
9
+ component: Table,
10
+ parameters: {
11
+ layout: 'padded',
12
+ docs: {
13
+ description: {
14
+ component: `
15
+ Variante \`datatable\` do componente Table — focada em filtragem por coluna.
16
+
17
+ **Tipos de filtro disponíveis:**
18
+ - \`text\` — campo de texto livre
19
+ - \`select\` — dropdown com opções predefinidas
20
+ - \`dateRange\` — seletor de intervalo de datas
21
+
22
+ **Modos de operação:**
23
+ - \`mode="client"\` (padrão) — filtragem e paginação feitas no browser
24
+ - \`mode="server"\` — todo o estado é emitido via \`onQueryChange\`, o servidor processa
25
+
26
+ **Controle de estado:**
27
+ - Não-controlado: estado interno gerenciado pelo componente
28
+ - Controlado: passe \`datatableColumnFiltersValue\` + \`onDatatableColumnFiltersChange\`
29
+ `,
30
+ },
31
+ },
32
+ },
33
+ tags: ['autodocs'],
34
+ }
35
+
36
+ export default meta
37
+ type Story = StoryObj
38
+
39
+ const footer = ({ total, filtered }: { total: number; filtered: number }) =>
40
+ `${filtered} de ${total}`
41
+
42
+ export const Default: Story = {
43
+ name: 'Padrão',
44
+ parameters: {
45
+ docs: { description: { story: 'Clique no cabeçalho de qualquer coluna filtrável para abrir o menu de filtro.' } },
46
+ },
47
+ render: () => (
48
+ <Table
49
+ variant="datatable"
50
+ data={ORDER_DATA}
51
+ columns={DATATABLE_COLUMNS}
52
+ pageSize={5}
53
+ footerText={footer}
54
+ />
55
+ ),
56
+ }
57
+
58
+ export const ComToolbar: Story = {
59
+ name: 'Com toolbar completa',
60
+ parameters: {
61
+ docs: { description: { story: 'Busca global + visibilidade de colunas + botão de limpar todos os filtros.' } },
62
+ },
63
+ render: () => (
64
+ <Table
65
+ variant="datatable"
66
+ data={ORDER_DATA}
67
+ columns={DATATABLE_COLUMNS}
68
+ pageSize={5}
69
+ datatableEnableGlobalSearch
70
+ datatableEnableColumnVisibility
71
+ datatableEnableClearFilters
72
+ footerText={footer}
73
+ />
74
+ ),
75
+ }
76
+
77
+ export const Controlado: Story = {
78
+ name: 'Controlado',
79
+ parameters: {
80
+ docs: {
81
+ description: {
82
+ story: 'Estado dos filtros gerenciado externamente. O JSON abaixo reflete o estado em tempo real.',
83
+ },
84
+ },
85
+ },
86
+ render: () => {
87
+ // eslint-disable-next-line react-hooks/rules-of-hooks
88
+ const [filters, setFilters] = useState<DatatableColumnFilters>({
89
+ customer: { type: 'text', value: 'João' },
90
+ })
91
+
92
+ return (
93
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
94
+ <Table
95
+ variant="datatable"
96
+ data={ORDER_DATA}
97
+ columns={DATATABLE_COLUMNS}
98
+ pageSize={5}
99
+ datatableColumnFiltersValue={filters}
100
+ onDatatableColumnFiltersChange={setFilters}
101
+ footerText={footer}
102
+ />
103
+ <pre style={{ fontSize: 12, background: '#f5f5f5', padding: 12, borderRadius: 6, margin: 0 }}>
104
+ {JSON.stringify(filters, null, 2)}
105
+ </pre>
106
+ </div>
107
+ )
108
+ },
109
+ }
110
+
111
+ export const ServerMode: Story = {
112
+ name: 'Server mode',
113
+ parameters: {
114
+ docs: {
115
+ description: {
116
+ story: 'Com `mode="server"`, qualquer mudança de filtro, paginação ou busca emite o `queryState` completo via `onQueryChange`.',
117
+ },
118
+ },
119
+ },
120
+ render: () => {
121
+ // eslint-disable-next-line react-hooks/rules-of-hooks
122
+ const [queryState, setQueryState] = useState<TableQueryState | null>(null)
123
+
124
+ return (
125
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
126
+ <Table
127
+ variant="datatable"
128
+ mode="server"
129
+ data={ORDER_DATA}
130
+ columns={DATATABLE_COLUMNS}
131
+ pageSize={5}
132
+ pageCount={2}
133
+ totalRows={10}
134
+ filteredRows={10}
135
+ onQueryChange={setQueryState}
136
+ footerText={footer}
137
+ />
138
+ {queryState && (
139
+ <pre style={{ fontSize: 12, background: '#f5f5f5', padding: 12, borderRadius: 6, margin: 0 }}>
140
+ {JSON.stringify(queryState, null, 2)}
141
+ </pre>
142
+ )}
143
+ </div>
144
+ )
145
+ },
146
+ }
147
+
148
+ export const ComExportacao: Story = {
149
+ name: 'Com exportação',
150
+ parameters: {
151
+ docs: {
152
+ description: {
153
+ story: 'O `queryState` passado para `onExport` inclui os filtros ativos no momento do clique.',
154
+ },
155
+ },
156
+ },
157
+ render: () => (
158
+ <Table
159
+ variant="datatable"
160
+ data={ORDER_DATA}
161
+ columns={DATATABLE_COLUMNS}
162
+ pageSize={5}
163
+ onExport={(_state: TableQueryState) => {
164
+ console.log('[Datatable Export]', _state)
165
+ alert('Exportação disparada — veja o console.')
166
+ }}
167
+ exportButton={{ label: 'Exportar', variant: 'neutralOutline' }}
168
+ footerText={footer}
169
+ />
170
+ ),
171
+ }
172
+
173
+ export const SemDados: Story = {
174
+ name: 'Sem dados',
175
+ parameters: {
176
+ docs: { description: { story: 'Estado vazio do datatable.' } },
177
+ },
178
+ render: () => (
179
+ <Table
180
+ variant="datatable"
181
+ data={[]}
182
+ columns={DATATABLE_COLUMNS}
183
+ emptyTitle="Nenhum registro encontrado."
184
+ />
185
+ ),
186
+ }