@liguelead/design-system 0.0.34 → 0.0.36
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 +10 -4
- package/components/Alert/Alert.tsx +7 -1
- package/components/Alert/Alert.types.ts +1 -0
- package/components/Skeleton/Skeleton.stories.tsx +126 -0
- package/components/Skeleton/Skeleton.styles.ts +15 -0
- package/components/Skeleton/Skeleton.tsx +38 -0
- package/components/Skeleton/Skeleton.types.ts +9 -0
- package/components/Skeleton/index.ts +2 -0
- package/components/SplitButton/SplitButton.sizes.ts +12 -0
- package/components/SplitButton/SplitButton.stories.tsx +221 -0
- package/components/SplitButton/SplitButton.styles.ts +106 -0
- package/components/SplitButton/SplitButton.tsx +83 -0
- package/components/SplitButton/SplitButton.types.ts +20 -0
- package/components/SplitButton/index.ts +2 -0
- package/components/Table/Table.tsx +11 -2
- package/components/Table/Table.types.ts +2 -1
- package/components/Table/components/DatatableColumnFilterMenu/DatatableColumnFilterMenu.styles.ts +42 -0
- package/components/Table/components/DatatableColumnFilterMenu/DatatableColumnFilterMenu.tsx +41 -2
- package/components/Table/components/TableHeader/TableHeader.tsx +5 -1
- package/components/Table/hooks/useDatatableFilters.ts +8 -2
- package/components/Table/stories.fixtures.ts +3 -2
- package/components/Table/utils/currencySortingFn.ts +20 -0
- package/components/Table/utils/index.ts +1 -0
- package/components/index.ts +3 -0
- package/package.json +3 -2
|
@@ -55,6 +55,11 @@ Componente de alerta para comunicar mensagens de feedback ao usuário.
|
|
|
55
55
|
control: 'text',
|
|
56
56
|
description: 'URL do botão de ação',
|
|
57
57
|
},
|
|
58
|
+
openNewTab: {
|
|
59
|
+
control: 'boolean',
|
|
60
|
+
description: 'Abre o link do botão em uma nova aba',
|
|
61
|
+
table: { defaultValue: { summary: 'false' } },
|
|
62
|
+
},
|
|
58
63
|
},
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -87,13 +92,14 @@ export const TodasVariantes: Story = {
|
|
|
87
92
|
export const ComBotao: Story = {
|
|
88
93
|
name: 'Com botão de ação',
|
|
89
94
|
parameters: {
|
|
90
|
-
docs: { description: { story: 'Use `hasButton` quando houver uma ação clara para o usuário tomar.' } },
|
|
95
|
+
docs: { description: { story: 'Use `hasButton` quando houver uma ação clara para o usuário tomar. Use `openNewTab` para abrir o link em nova aba.' } },
|
|
91
96
|
},
|
|
92
97
|
render: () => (
|
|
93
98
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
94
|
-
<Alert variant="info" title="Nova versão disponível" description="Uma atualização está pronta para ser instalada." hasButton buttonLabel="Atualizar agora" href="#" />
|
|
95
|
-
<Alert variant="warning" title="Sessão expirando" description="Sua sessão expira em 5 minutos." hasButton buttonLabel="Renovar sessão" href="#" />
|
|
96
|
-
<Alert variant="danger" title="Falha no pagamento" description="Não foi possível processar seu pagamento." hasButton buttonLabel="Tentar novamente" href="#" />
|
|
99
|
+
<Alert variant="info" title="Nova versão disponível" description="Uma atualização está pronta para ser instalada." hasButton buttonLabel="Atualizar agora" href="#" openNewTab={false} />
|
|
100
|
+
<Alert variant="warning" title="Sessão expirando" description="Sua sessão expira em 5 minutos." hasButton buttonLabel="Renovar sessão" href="#" openNewTab={false} />
|
|
101
|
+
<Alert variant="danger" title="Falha no pagamento" description="Não foi possível processar seu pagamento." hasButton buttonLabel="Tentar novamente" href="#" openNewTab={false} />
|
|
102
|
+
<Alert variant="info" title="Documentação disponível" description="Acesse a documentação completa no portal." hasButton buttonLabel="Ver documentação" href="#" openNewTab />
|
|
97
103
|
</div>
|
|
98
104
|
),
|
|
99
105
|
}
|
|
@@ -17,6 +17,7 @@ const Alert = ({
|
|
|
17
17
|
description,
|
|
18
18
|
href,
|
|
19
19
|
children,
|
|
20
|
+
openNewTab,
|
|
20
21
|
title,
|
|
21
22
|
hasButton
|
|
22
23
|
}: TAlertProps) => {
|
|
@@ -45,7 +46,12 @@ const Alert = ({
|
|
|
45
46
|
</AlertContent>
|
|
46
47
|
{hasButton && (
|
|
47
48
|
<AlertButtonContainer>
|
|
48
|
-
<LinkButton
|
|
49
|
+
<LinkButton
|
|
50
|
+
href={href || ''}
|
|
51
|
+
target={openNewTab ? '_blank' : undefined}
|
|
52
|
+
>
|
|
53
|
+
{buttonLabel}
|
|
54
|
+
</LinkButton>
|
|
49
55
|
</AlertButtonContainer>
|
|
50
56
|
)}
|
|
51
57
|
</AlertContainer>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import Skeleton from './Skeleton'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Skeleton> = {
|
|
5
|
+
title: 'Feedback/Skeleton',
|
|
6
|
+
component: Skeleton,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered',
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: `
|
|
12
|
+
Componente de loading skeleton baseado em \`react-loading-skeleton\`.
|
|
13
|
+
|
|
14
|
+
**Quando usar:**
|
|
15
|
+
- Enquanto dados estão sendo carregados da API
|
|
16
|
+
- Para evitar layout shift e melhorar a percepção de performance
|
|
17
|
+
- Substitua o conteúdo real pelo Skeleton durante o estado de loading
|
|
18
|
+
`,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
tags: ['autodocs'],
|
|
23
|
+
argTypes: {
|
|
24
|
+
width: {
|
|
25
|
+
control: 'text',
|
|
26
|
+
description: 'Largura do skeleton (px, %, rem...)',
|
|
27
|
+
},
|
|
28
|
+
height: {
|
|
29
|
+
control: 'text',
|
|
30
|
+
description: 'Altura do skeleton',
|
|
31
|
+
},
|
|
32
|
+
count: {
|
|
33
|
+
control: { type: 'number', min: 1, max: 10 },
|
|
34
|
+
description: 'Quantidade de linhas',
|
|
35
|
+
table: { defaultValue: { summary: '1' } },
|
|
36
|
+
},
|
|
37
|
+
circle: {
|
|
38
|
+
control: 'boolean',
|
|
39
|
+
description: 'Formato circular (avatar)',
|
|
40
|
+
table: { defaultValue: { summary: 'false' } },
|
|
41
|
+
},
|
|
42
|
+
borderRadius: {
|
|
43
|
+
control: 'text',
|
|
44
|
+
description: 'Border radius customizado',
|
|
45
|
+
},
|
|
46
|
+
inline: {
|
|
47
|
+
control: 'boolean',
|
|
48
|
+
description: 'Exibe inline',
|
|
49
|
+
table: { defaultValue: { summary: 'false' } },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default meta
|
|
55
|
+
type Story = StoryObj<typeof meta>
|
|
56
|
+
|
|
57
|
+
export const Default: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
width: 200,
|
|
60
|
+
height: 20,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const MultiplaLinhas: Story = {
|
|
65
|
+
name: 'Múltiplas linhas',
|
|
66
|
+
parameters: {
|
|
67
|
+
docs: { description: { story: 'Use `count` para simular blocos de texto.' } },
|
|
68
|
+
},
|
|
69
|
+
render: () => (
|
|
70
|
+
<div style={{ width: 320 }}>
|
|
71
|
+
<Skeleton count={4} height={16} />
|
|
72
|
+
</div>
|
|
73
|
+
),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const Circular: Story = {
|
|
77
|
+
name: 'Avatar circular',
|
|
78
|
+
parameters: {
|
|
79
|
+
docs: { description: { story: 'Use `circle` para avatares.' } },
|
|
80
|
+
},
|
|
81
|
+
render: () => (
|
|
82
|
+
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
|
83
|
+
<Skeleton circle width={32} height={32} />
|
|
84
|
+
<Skeleton circle width={40} height={40} />
|
|
85
|
+
<Skeleton circle width={56} height={56} />
|
|
86
|
+
</div>
|
|
87
|
+
),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const Card: Story = {
|
|
91
|
+
name: 'Card de conteúdo',
|
|
92
|
+
parameters: {
|
|
93
|
+
docs: { description: { story: 'Exemplo de skeleton para um card com avatar + texto.' } },
|
|
94
|
+
},
|
|
95
|
+
render: () => (
|
|
96
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, width: 320 }}>
|
|
97
|
+
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
|
|
98
|
+
<Skeleton circle width={40} height={40} />
|
|
99
|
+
<div style={{ flex: 1 }}>
|
|
100
|
+
<Skeleton height={14} width="60%" />
|
|
101
|
+
<Skeleton height={12} width="40%" />
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<Skeleton height={120} borderRadius={8} />
|
|
105
|
+
<Skeleton count={3} height={14} />
|
|
106
|
+
</div>
|
|
107
|
+
),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const Tabela: Story = {
|
|
111
|
+
name: 'Linhas de tabela',
|
|
112
|
+
parameters: {
|
|
113
|
+
docs: { description: { story: 'Simula linhas de uma tabela carregando.' } },
|
|
114
|
+
},
|
|
115
|
+
render: () => (
|
|
116
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, width: 480 }}>
|
|
117
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
118
|
+
<div key={i} style={{ display: 'flex', gap: 16 }}>
|
|
119
|
+
<Skeleton width={120} height={16} />
|
|
120
|
+
<Skeleton width={160} height={16} />
|
|
121
|
+
<Skeleton width={80} height={16} />
|
|
122
|
+
</div>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
),
|
|
126
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import styled from 'styled-components'
|
|
2
|
+
import { radius } from '@liguelead/foundation'
|
|
3
|
+
import { parseColor } from '../../utils'
|
|
4
|
+
|
|
5
|
+
export const SkeletonWrapper = styled.div<{ $inline?: boolean }>`
|
|
6
|
+
display: ${({ $inline }) => ($inline ? 'inline-flex' : 'flex')};
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: 4px;
|
|
9
|
+
`
|
|
10
|
+
|
|
11
|
+
export const SkeletonThemeWrapper = styled.div`
|
|
12
|
+
--base-color: ${({ theme }) => parseColor(theme.colors.neutral200)};
|
|
13
|
+
--highlight-color: ${({ theme }) => parseColor(theme.colors.neutral100)};
|
|
14
|
+
border-radius: ${radius.radius4}px;
|
|
15
|
+
`
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import ReactSkeleton, { SkeletonTheme } from 'react-loading-skeleton'
|
|
2
|
+
import 'react-loading-skeleton/dist/skeleton.css'
|
|
3
|
+
import { useTheme } from 'styled-components'
|
|
4
|
+
import { parseColor } from '../../utils'
|
|
5
|
+
import { SkeletonWrapper } from './Skeleton.styles'
|
|
6
|
+
import { SkeletonProps } from './Skeleton.types'
|
|
7
|
+
|
|
8
|
+
const Skeleton: React.FC<SkeletonProps> = ({
|
|
9
|
+
width,
|
|
10
|
+
height,
|
|
11
|
+
borderRadius,
|
|
12
|
+
circle = false,
|
|
13
|
+
count = 1,
|
|
14
|
+
className,
|
|
15
|
+
inline = false,
|
|
16
|
+
}) => {
|
|
17
|
+
const theme = useTheme()
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<SkeletonTheme
|
|
21
|
+
baseColor={parseColor(theme.colors.neutral200)}
|
|
22
|
+
highlightColor={parseColor(theme.colors.neutral100)}
|
|
23
|
+
>
|
|
24
|
+
<SkeletonWrapper $inline={inline} className={className}>
|
|
25
|
+
<ReactSkeleton
|
|
26
|
+
width={width}
|
|
27
|
+
height={height}
|
|
28
|
+
borderRadius={borderRadius}
|
|
29
|
+
circle={circle}
|
|
30
|
+
count={count}
|
|
31
|
+
inline={inline}
|
|
32
|
+
/>
|
|
33
|
+
</SkeletonWrapper>
|
|
34
|
+
</SkeletonTheme>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default Skeleton
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { spacing } from '@liguelead/foundation'
|
|
2
|
+
import { ButtonSizeTypes } from '../Button/Button.types'
|
|
3
|
+
|
|
4
|
+
export const SplitButtonTriggerSizes = (size: ButtonSizeTypes) => {
|
|
5
|
+
const sizes = {
|
|
6
|
+
sm: `width: ${spacing.spacing32}px; height: ${spacing.spacing32}px; padding: 0;`,
|
|
7
|
+
md: `width: ${spacing.spacing36}px; height: ${spacing.spacing36}px; padding: 0;`,
|
|
8
|
+
lg: `width: ${spacing.spacing40}px; height: ${spacing.spacing40}px; padding: 0;`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return sizes[size]
|
|
12
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import {
|
|
3
|
+
ArchiveIcon,
|
|
4
|
+
ClockIcon,
|
|
5
|
+
PencilSimpleIcon,
|
|
6
|
+
TrashIcon
|
|
7
|
+
} from '@phosphor-icons/react'
|
|
8
|
+
import SplitButton from './SplitButton'
|
|
9
|
+
|
|
10
|
+
const meta: Meta<typeof SplitButton> = {
|
|
11
|
+
title: 'Form/SplitButton',
|
|
12
|
+
component: SplitButton,
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: 'centered',
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component: `
|
|
18
|
+
Botão dividido com ação principal e menu de opções secundárias.
|
|
19
|
+
|
|
20
|
+
**Quando usar:**
|
|
21
|
+
- Quando há uma ação primária clara, mas também ações alternativas relacionadas
|
|
22
|
+
- Ex: "Publicar" com opções "Salvar rascunho" e "Agendar"
|
|
23
|
+
|
|
24
|
+
**Composição:**
|
|
25
|
+
- Lado esquerdo: botão de ação principal (\`onClick\`)
|
|
26
|
+
- Lado direito: trigger que abre um \`DropdownMenu\` com as \`options\`
|
|
27
|
+
`
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
tags: ['autodocs'],
|
|
32
|
+
argTypes: {
|
|
33
|
+
label: {
|
|
34
|
+
control: 'text',
|
|
35
|
+
description: 'Texto do botão principal'
|
|
36
|
+
},
|
|
37
|
+
variant: {
|
|
38
|
+
control: 'select',
|
|
39
|
+
options: ['solid', 'outline', 'ghost', 'neutralOutline', 'neutralGhost'],
|
|
40
|
+
description: 'Variante visual',
|
|
41
|
+
table: { defaultValue: { summary: 'neutralOutline' } }
|
|
42
|
+
},
|
|
43
|
+
size: {
|
|
44
|
+
control: 'select',
|
|
45
|
+
options: ['sm', 'md', 'lg'],
|
|
46
|
+
description: 'Tamanho do botão',
|
|
47
|
+
table: { defaultValue: { summary: 'sm' } }
|
|
48
|
+
},
|
|
49
|
+
disabled: {
|
|
50
|
+
control: 'boolean',
|
|
51
|
+
description: 'Desabilita ambos os botões',
|
|
52
|
+
table: { defaultValue: { summary: 'false' } }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default meta
|
|
58
|
+
type Story = StoryObj<typeof meta>
|
|
59
|
+
|
|
60
|
+
const defaultOptions = [
|
|
61
|
+
{
|
|
62
|
+
label: 'Salvar rascunho',
|
|
63
|
+
icon: <PencilSimpleIcon size={16} />,
|
|
64
|
+
onClick: () => console.log('rascunho')
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: 'Agendar',
|
|
68
|
+
icon: <ClockIcon size={16} />,
|
|
69
|
+
onClick: () => console.log('agendar')
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: 'Arquivar',
|
|
73
|
+
icon: <ArchiveIcon size={16} />,
|
|
74
|
+
onClick: () => console.log('arquivar')
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
export const Default: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
label: 'Publicar',
|
|
81
|
+
onClick: () => console.log('publicar'),
|
|
82
|
+
options: defaultOptions
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const Tamanhos: Story = {
|
|
87
|
+
name: 'Tamanhos',
|
|
88
|
+
parameters: {
|
|
89
|
+
docs: {
|
|
90
|
+
description: { story: 'Três tamanhos disponíveis: `sm`, `md` e `lg`.' }
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
render: () => (
|
|
94
|
+
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
|
|
95
|
+
<SplitButton
|
|
96
|
+
label="Pequeno"
|
|
97
|
+
size="sm"
|
|
98
|
+
onClick={() => {}}
|
|
99
|
+
options={defaultOptions}
|
|
100
|
+
/>
|
|
101
|
+
<SplitButton
|
|
102
|
+
label="Médio"
|
|
103
|
+
size="md"
|
|
104
|
+
onClick={() => {}}
|
|
105
|
+
options={defaultOptions}
|
|
106
|
+
/>
|
|
107
|
+
<SplitButton
|
|
108
|
+
label="Grande"
|
|
109
|
+
size="lg"
|
|
110
|
+
onClick={() => {}}
|
|
111
|
+
options={defaultOptions}
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const Variantes: Story = {
|
|
118
|
+
name: 'Variantes',
|
|
119
|
+
parameters: {
|
|
120
|
+
docs: {
|
|
121
|
+
description: { story: 'Herda as mesmas variantes do Button.' }
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
render: () => (
|
|
125
|
+
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
126
|
+
<SplitButton
|
|
127
|
+
label="Neutral Outline"
|
|
128
|
+
variant="neutralOutline"
|
|
129
|
+
onClick={() => {}}
|
|
130
|
+
options={defaultOptions}
|
|
131
|
+
/>
|
|
132
|
+
<SplitButton
|
|
133
|
+
label="Solid"
|
|
134
|
+
variant="solid"
|
|
135
|
+
onClick={() => {}}
|
|
136
|
+
options={defaultOptions}
|
|
137
|
+
/>
|
|
138
|
+
<SplitButton
|
|
139
|
+
label="Outline"
|
|
140
|
+
variant="outline"
|
|
141
|
+
onClick={() => {}}
|
|
142
|
+
options={defaultOptions}
|
|
143
|
+
/>
|
|
144
|
+
<SplitButton
|
|
145
|
+
label="Ghost"
|
|
146
|
+
variant="ghost"
|
|
147
|
+
onClick={() => {}}
|
|
148
|
+
options={defaultOptions}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const Desabilitado: Story = {
|
|
155
|
+
name: 'Estado desabilitado',
|
|
156
|
+
args: {
|
|
157
|
+
label: 'Publicar',
|
|
158
|
+
disabled: true,
|
|
159
|
+
onClick: () => {},
|
|
160
|
+
options: defaultOptions
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const SemIcones: Story = {
|
|
165
|
+
name: 'Opções sem ícone',
|
|
166
|
+
args: {
|
|
167
|
+
label: 'Ação',
|
|
168
|
+
onClick: () => {},
|
|
169
|
+
options: [
|
|
170
|
+
{ label: 'Opção A', onClick: () => {} },
|
|
171
|
+
{ label: 'Opção B', onClick: () => {} },
|
|
172
|
+
{ label: 'Opção C (desabilitada)', onClick: () => {}, disabled: true }
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const ExemploReal: Story = {
|
|
178
|
+
name: 'Exemplo: Publicação de conteúdo',
|
|
179
|
+
parameters: {
|
|
180
|
+
docs: {
|
|
181
|
+
description: {
|
|
182
|
+
story: 'Padrão comum em sistemas de CMS ou gestão de tarefas.'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
render: () => (
|
|
187
|
+
<div
|
|
188
|
+
style={{
|
|
189
|
+
display: 'flex',
|
|
190
|
+
justifyContent: 'flex-end',
|
|
191
|
+
padding: 16,
|
|
192
|
+
border: '1px solid #e5e7eb',
|
|
193
|
+
borderRadius: 8,
|
|
194
|
+
width: 400,
|
|
195
|
+
gap: 8
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<SplitButton
|
|
199
|
+
label="Publicar"
|
|
200
|
+
onClick={() => console.log('publicar')}
|
|
201
|
+
options={[
|
|
202
|
+
{
|
|
203
|
+
label: 'Salvar rascunho',
|
|
204
|
+
icon: <PencilSimpleIcon size={16} />,
|
|
205
|
+
onClick: () => console.log('rascunho')
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
label: 'Agendar publicação',
|
|
209
|
+
icon: <ClockIcon size={16} />,
|
|
210
|
+
onClick: () => console.log('agendar')
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
label: 'Mover para lixeira',
|
|
214
|
+
icon: <TrashIcon size={16} />,
|
|
215
|
+
onClick: () => console.log('lixeira')
|
|
216
|
+
}
|
|
217
|
+
]}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import styled from 'styled-components'
|
|
2
|
+
import { fontSize, fontWeight, lineHeight, radius, shadow, spacing } from '@liguelead/foundation'
|
|
3
|
+
import { parseColor } from '../../utils'
|
|
4
|
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
|
5
|
+
|
|
6
|
+
export const Wrapper = styled.div`
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: stretch;
|
|
9
|
+
`
|
|
10
|
+
|
|
11
|
+
export const MainButton = styled.button<{ $variant: string; $size: string }>`
|
|
12
|
+
position: relative;
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
outline: none;
|
|
19
|
+
transition: background-color 0.3s, box-shadow 0.3s;
|
|
20
|
+
${({ $size }) => $size}
|
|
21
|
+
${({ $variant }) => $variant}
|
|
22
|
+
border-radius: ${radius.radius4}px 0 0 ${radius.radius4}px;
|
|
23
|
+
|
|
24
|
+
&:focus-visible {
|
|
25
|
+
box-shadow: ${shadow.focusShadow};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&:disabled {
|
|
29
|
+
cursor: not-allowed;
|
|
30
|
+
}
|
|
31
|
+
`
|
|
32
|
+
|
|
33
|
+
export const TriggerButton = styled(DropdownMenu.Trigger)<{
|
|
34
|
+
$variant: string
|
|
35
|
+
$size: string
|
|
36
|
+
}>`
|
|
37
|
+
position: relative;
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
outline: none;
|
|
44
|
+
transition: background-color 0.3s, box-shadow 0.3s;
|
|
45
|
+
${({ $size }) => $size}
|
|
46
|
+
${({ $variant }) => $variant}
|
|
47
|
+
border-radius: 0 ${radius.radius4}px ${radius.radius4}px 0;
|
|
48
|
+
|
|
49
|
+
&:focus-visible {
|
|
50
|
+
box-shadow: ${shadow.focusShadow};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&:disabled {
|
|
54
|
+
cursor: not-allowed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
& svg {
|
|
58
|
+
width: 10px;
|
|
59
|
+
height: 10px;
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
}
|
|
62
|
+
`
|
|
63
|
+
|
|
64
|
+
export const StyledContent = styled(DropdownMenu.Content)`
|
|
65
|
+
width: max-content;
|
|
66
|
+
background: ${({ theme }) => parseColor(theme.colors.white)};
|
|
67
|
+
border: 1px solid ${({ theme }) => parseColor(theme.colors.neutral400)};
|
|
68
|
+
border-radius: ${radius.radius4}px;
|
|
69
|
+
box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.08);
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
z-index: 9999;
|
|
72
|
+
padding: 0;
|
|
73
|
+
`
|
|
74
|
+
|
|
75
|
+
export const StyledItem = styled(DropdownMenu.Item)`
|
|
76
|
+
height: ${spacing.spacing36}px;
|
|
77
|
+
padding: 0 ${spacing.spacing12}px;
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: ${spacing.spacing8}px;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
outline: none;
|
|
83
|
+
font-size: ${fontSize.fontSize14}px;
|
|
84
|
+
font-weight: ${fontWeight.fontWeight400};
|
|
85
|
+
line-height: ${lineHeight.lineHeight20}px;
|
|
86
|
+
color: ${({ theme }) => parseColor(theme.colors.textDark)};
|
|
87
|
+
transition: background-color 0.2s ease;
|
|
88
|
+
user-select: none;
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
|
|
91
|
+
&[data-highlighted] {
|
|
92
|
+
background-color: ${({ theme }) => parseColor(theme.colors.primaryLight)};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
&[data-disabled] {
|
|
96
|
+
opacity: 0.6;
|
|
97
|
+
cursor: not-allowed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
& svg {
|
|
101
|
+
width: 16px;
|
|
102
|
+
height: 16px;
|
|
103
|
+
flex-shrink: 0;
|
|
104
|
+
color: ${({ theme }) => parseColor(theme.colors.textMedium)};
|
|
105
|
+
}
|
|
106
|
+
`
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
|
2
|
+
import { CaretDownIcon } from '@phosphor-icons/react'
|
|
3
|
+
import { useTheme } from 'styled-components'
|
|
4
|
+
import { ButtonVariant } from '../Button/Button.appearance'
|
|
5
|
+
import { ButtonSizes } from '../Button/Button.sizes'
|
|
6
|
+
import { parseColor } from '../../utils'
|
|
7
|
+
import {
|
|
8
|
+
MainButton,
|
|
9
|
+
StyledContent,
|
|
10
|
+
StyledItem,
|
|
11
|
+
TriggerButton,
|
|
12
|
+
Wrapper
|
|
13
|
+
} from './SplitButton.styles'
|
|
14
|
+
import { SplitButtonTriggerSizes } from './SplitButton.sizes'
|
|
15
|
+
import { SplitButtonProps } from './SplitButton.types'
|
|
16
|
+
|
|
17
|
+
const SplitButton: React.FC<SplitButtonProps> = ({
|
|
18
|
+
label,
|
|
19
|
+
onClick,
|
|
20
|
+
options,
|
|
21
|
+
variant = 'neutralOutline',
|
|
22
|
+
color = 'primary',
|
|
23
|
+
size = 'sm',
|
|
24
|
+
disabled = false,
|
|
25
|
+
className
|
|
26
|
+
}) => {
|
|
27
|
+
const theme = useTheme()
|
|
28
|
+
const buttonVariant = ButtonVariant(color, variant)
|
|
29
|
+
const buttonSize = ButtonSizes(size)
|
|
30
|
+
const triggerSize = SplitButtonTriggerSizes(size)
|
|
31
|
+
|
|
32
|
+
const mainVariantOverride = `
|
|
33
|
+
${buttonVariant}
|
|
34
|
+
border-right: none;
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
const triggerVariantOverride = `
|
|
38
|
+
${buttonVariant}
|
|
39
|
+
border-left: 1px solid ${parseColor(theme.colors.neutral400)};
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<DropdownMenu.Root>
|
|
44
|
+
<Wrapper className={className}>
|
|
45
|
+
<MainButton
|
|
46
|
+
type="button"
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
onClick={onClick}
|
|
49
|
+
$variant={mainVariantOverride}
|
|
50
|
+
$size={buttonSize}
|
|
51
|
+
>
|
|
52
|
+
{label}
|
|
53
|
+
</MainButton>
|
|
54
|
+
|
|
55
|
+
<TriggerButton
|
|
56
|
+
disabled={disabled}
|
|
57
|
+
$variant={triggerVariantOverride}
|
|
58
|
+
$size={triggerSize}
|
|
59
|
+
aria-label="Mais opções"
|
|
60
|
+
>
|
|
61
|
+
<CaretDownIcon weight="fill" />
|
|
62
|
+
</TriggerButton>
|
|
63
|
+
</Wrapper>
|
|
64
|
+
|
|
65
|
+
<DropdownMenu.Portal>
|
|
66
|
+
<StyledContent align="end" sideOffset={0}>
|
|
67
|
+
{options.map((option, index) => (
|
|
68
|
+
<StyledItem
|
|
69
|
+
key={index}
|
|
70
|
+
disabled={option.disabled}
|
|
71
|
+
onSelect={option.onClick}
|
|
72
|
+
>
|
|
73
|
+
{option.icon}
|
|
74
|
+
{option.label}
|
|
75
|
+
</StyledItem>
|
|
76
|
+
))}
|
|
77
|
+
</StyledContent>
|
|
78
|
+
</DropdownMenu.Portal>
|
|
79
|
+
</DropdownMenu.Root>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default SplitButton
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ButtonSizeTypes, ButtonVariantTypes } from '../Button/Button.types'
|
|
2
|
+
import { colorType } from '../../types'
|
|
3
|
+
|
|
4
|
+
export interface SplitButtonOption {
|
|
5
|
+
label: string
|
|
6
|
+
icon?: React.ReactNode
|
|
7
|
+
onClick: () => void
|
|
8
|
+
disabled?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SplitButtonProps {
|
|
12
|
+
label: string
|
|
13
|
+
onClick: () => void
|
|
14
|
+
options: SplitButtonOption[]
|
|
15
|
+
variant?: ButtonVariantTypes
|
|
16
|
+
color?: colorType
|
|
17
|
+
size?: ButtonSizeTypes
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
className?: string
|
|
20
|
+
}
|
|
@@ -98,6 +98,7 @@ function Table<TData>({
|
|
|
98
98
|
isServerMode,
|
|
99
99
|
controlled: datatableColumnFiltersValue,
|
|
100
100
|
onChange: onDatatableColumnFiltersChange,
|
|
101
|
+
onClearSorting: (colId) => setSorting((prev) => prev.filter((s) => s.id !== colId)),
|
|
101
102
|
})
|
|
102
103
|
|
|
103
104
|
const queryState: TableQueryState = useMemo(() => ({
|
|
@@ -164,7 +165,7 @@ function Table<TData>({
|
|
|
164
165
|
const currentPage = pagination.pageIndex + 1
|
|
165
166
|
|
|
166
167
|
const hasActiveFilters = globalFilter !== '' || sorting.length > 0
|
|
167
|
-
const hasDatatableActiveFilters = globalFilter !== '' || columnFilters.length > 0
|
|
168
|
+
const hasDatatableActiveFilters = globalFilter !== '' || columnFilters.length > 0 || sorting.length > 0
|
|
168
169
|
|
|
169
170
|
const handleClearFilters = () => {
|
|
170
171
|
setGlobalFilter('')
|
|
@@ -172,6 +173,10 @@ function Table<TData>({
|
|
|
172
173
|
setPagination((prev) => ({ ...prev, pageIndex: 0 }))
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
const handleNumberSort = (colId: string, direction: 'asc' | 'desc' | null) => {
|
|
177
|
+
setSorting(direction ? [{ id: colId, desc: direction === 'desc' }] : [])
|
|
178
|
+
}
|
|
179
|
+
|
|
175
180
|
const handleExport = async () => {
|
|
176
181
|
if (onExport) {
|
|
177
182
|
await onExport(queryState)
|
|
@@ -204,7 +209,10 @@ function Table<TData>({
|
|
|
204
209
|
enableColumnVisibility={datatableEnableColumnVisibility}
|
|
205
210
|
enableClearFilters={datatableEnableClearFilters}
|
|
206
211
|
hasActiveFilters={hasDatatableActiveFilters}
|
|
207
|
-
onClearAll={() =>
|
|
212
|
+
onClearAll={() => {
|
|
213
|
+
handleClearAllFilters(() => setGlobalFilter(''))
|
|
214
|
+
setSorting([])
|
|
215
|
+
}}
|
|
208
216
|
searchPlaceholder={searchPlaceholder}
|
|
209
217
|
searchWidth={searchWidth}
|
|
210
218
|
clearFiltersLabel={clearFiltersLabel}
|
|
@@ -263,6 +271,7 @@ function Table<TData>({
|
|
|
263
271
|
onFilterPopoverOpenChange={handlePopoverOpenChange}
|
|
264
272
|
onFilterValueChange={handleFilterValueChange}
|
|
265
273
|
onClearFilter={handleClearColumnFilter}
|
|
274
|
+
onNumberSort={handleNumberSort}
|
|
266
275
|
labels={datatableFilterDropdownLabels}
|
|
267
276
|
/>
|
|
268
277
|
<tbody>
|
|
@@ -8,6 +8,7 @@ export type ColumnFilterValue =
|
|
|
8
8
|
| { type: 'text'; value: string }
|
|
9
9
|
| { type: 'dateRange'; value: { from?: Date; to?: Date } }
|
|
10
10
|
| { type: 'select'; value: string | number | null }
|
|
11
|
+
| { type: 'numberSort'; value: 'asc' | 'desc' | null }
|
|
11
12
|
|
|
12
13
|
export type DatatableColumnFilters = Record<string, ColumnFilterValue | undefined>
|
|
13
14
|
|
|
@@ -33,7 +34,7 @@ export interface ExportButtonConfig {
|
|
|
33
34
|
ariaLabel?: string
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
export type DatatableFilterType = 'text' | 'dateRange' | 'select'
|
|
37
|
+
export type DatatableFilterType = 'text' | 'dateRange' | 'select' | 'numberSort'
|
|
37
38
|
|
|
38
39
|
export interface DatatableColumnMeta {
|
|
39
40
|
filterType?: DatatableFilterType
|
package/components/Table/components/DatatableColumnFilterMenu/DatatableColumnFilterMenu.styles.ts
CHANGED
|
@@ -118,3 +118,45 @@ export const ClearFilterButtonWrapper = styled.span`
|
|
|
118
118
|
align-items: center;
|
|
119
119
|
justify-content: end;
|
|
120
120
|
`
|
|
121
|
+
|
|
122
|
+
export const NumberSortOptions = styled.div`
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
gap: ${spacing.spacing4}px;
|
|
126
|
+
`
|
|
127
|
+
|
|
128
|
+
export const NumberSortButton = styled.button<{ $active?: boolean }>`
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: ${spacing.spacing8}px;
|
|
132
|
+
width: 100%;
|
|
133
|
+
padding: ${spacing.spacing8}px ${spacing.spacing12}px;
|
|
134
|
+
font-size: ${fontSize.fontSize14}px;
|
|
135
|
+
font-weight: ${fontWeight.fontWeight400};
|
|
136
|
+
border-radius: ${radius.radius4}px;
|
|
137
|
+
border: 1px solid ${({ theme, $active }) =>
|
|
138
|
+
$active ? parseColor(theme.colors.primary) : parseColor(theme.colors.neutral300)};
|
|
139
|
+
background: ${({ theme, $active }) =>
|
|
140
|
+
$active ? parseColor(theme.colors.primaryLighter) : parseColor(theme.colors.white)};
|
|
141
|
+
color: ${({ theme, $active }) =>
|
|
142
|
+
$active ? parseColor(theme.colors.primary) : parseColor(theme.colors.textDark)};
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
145
|
+
outline: none;
|
|
146
|
+
|
|
147
|
+
& svg {
|
|
148
|
+
flex-shrink: 0;
|
|
149
|
+
color: ${({ theme, $active }) =>
|
|
150
|
+
$active ? parseColor(theme.colors.primary) : parseColor(theme.colors.textMedium)};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
&:hover:not([aria-pressed='true']) {
|
|
154
|
+
background: ${({ theme }) => parseColor(theme.colors.neutral100)};
|
|
155
|
+
border-color: ${({ theme }) => parseColor(theme.colors.neutral400)};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
&:focus-visible {
|
|
159
|
+
outline: 2px solid ${({ theme }) => parseColor(theme.colors.primary)};
|
|
160
|
+
outline-offset: 1px;
|
|
161
|
+
}
|
|
162
|
+
`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
|
2
|
-
import { XIcon } from '@phosphor-icons/react'
|
|
2
|
+
import { ArrowDownIcon, ArrowUpIcon, XIcon } from '@phosphor-icons/react'
|
|
3
3
|
import { DateRange } from 'react-day-picker'
|
|
4
4
|
import DatePicker from '../../../DatePicker'
|
|
5
5
|
import Select from '../../../Select'
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
FilterTriggerArea,
|
|
20
20
|
FunnelActiveIcon,
|
|
21
21
|
FunnelInactiveIcon,
|
|
22
|
+
NumberSortButton,
|
|
23
|
+
NumberSortOptions,
|
|
22
24
|
StyledTextInput,
|
|
23
25
|
} from './DatatableColumnFilterMenu.styles'
|
|
24
26
|
|
|
@@ -31,6 +33,7 @@ interface DatatableColumnFilterMenuProps {
|
|
|
31
33
|
filterValue: ColumnFilterValue | undefined
|
|
32
34
|
onFilterValueChange: (columnId: string, value: ColumnFilterValue | undefined) => void
|
|
33
35
|
onClearFilter: (columnId: string) => void
|
|
36
|
+
onNumberSort: (colId: string, direction: 'asc' | 'desc' | null) => void
|
|
34
37
|
labels?: DatatableFilterDropdownLabels
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -44,6 +47,7 @@ function DatatableColumnFilterMenu({
|
|
|
44
47
|
filterValue,
|
|
45
48
|
onFilterValueChange,
|
|
46
49
|
onClearFilter,
|
|
50
|
+
onNumberSort,
|
|
47
51
|
labels = {},
|
|
48
52
|
}: DatatableColumnFilterMenuProps) {
|
|
49
53
|
const filterType = meta?.filterType
|
|
@@ -59,7 +63,8 @@ function DatatableColumnFilterMenu({
|
|
|
59
63
|
((filterValue.type === 'text' && filterValue.value !== '') ||
|
|
60
64
|
(filterValue.type === 'select' && filterValue.value != null) ||
|
|
61
65
|
(filterValue.type === 'dateRange' &&
|
|
62
|
-
(filterValue.value.from != null || filterValue.value.to != null))
|
|
66
|
+
(filterValue.value.from != null || filterValue.value.to != null)) ||
|
|
67
|
+
(filterValue.type === 'numberSort' && filterValue.value != null))
|
|
63
68
|
|
|
64
69
|
const handleTriggerClick = (e: React.MouseEvent) => {
|
|
65
70
|
e.stopPropagation()
|
|
@@ -92,6 +97,17 @@ function DatatableColumnFilterMenu({
|
|
|
92
97
|
onOpenChange(false)
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
const handleNumberSort = (direction: 'asc' | 'desc') => {
|
|
101
|
+
const current = filterValue?.type === 'numberSort' ? filterValue.value : null
|
|
102
|
+
const next = current === direction ? null : direction
|
|
103
|
+
onFilterValueChange(columnId, { type: 'numberSort', value: next })
|
|
104
|
+
onNumberSort(columnId, next)
|
|
105
|
+
onOpenChange(false)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const currentSortDirection =
|
|
109
|
+
filterValue?.type === 'numberSort' ? filterValue.value : null
|
|
110
|
+
|
|
95
111
|
const rangeValue: DateRange | undefined =
|
|
96
112
|
filterValue?.type === 'dateRange'
|
|
97
113
|
? { from: filterValue.value.from, to: filterValue.value.to }
|
|
@@ -204,6 +220,29 @@ function DatatableColumnFilterMenu({
|
|
|
204
220
|
onValueChange={handleSelectChange}
|
|
205
221
|
/>
|
|
206
222
|
)}
|
|
223
|
+
|
|
224
|
+
{filterType === 'numberSort' && (
|
|
225
|
+
<NumberSortOptions>
|
|
226
|
+
<NumberSortButton
|
|
227
|
+
type="button"
|
|
228
|
+
$active={currentSortDirection === 'asc'}
|
|
229
|
+
onClick={() => handleNumberSort('asc')}
|
|
230
|
+
aria-pressed={currentSortDirection === 'asc'}
|
|
231
|
+
>
|
|
232
|
+
<ArrowUpIcon size={14} />
|
|
233
|
+
Crescente
|
|
234
|
+
</NumberSortButton>
|
|
235
|
+
<NumberSortButton
|
|
236
|
+
type="button"
|
|
237
|
+
$active={currentSortDirection === 'desc'}
|
|
238
|
+
onClick={() => handleNumberSort('desc')}
|
|
239
|
+
aria-pressed={currentSortDirection === 'desc'}
|
|
240
|
+
>
|
|
241
|
+
<ArrowDownIcon size={14} />
|
|
242
|
+
Decrescente
|
|
243
|
+
</NumberSortButton>
|
|
244
|
+
</NumberSortOptions>
|
|
245
|
+
)}
|
|
207
246
|
</FilterPopoverContent>
|
|
208
247
|
</PopoverPrimitive.Content>
|
|
209
248
|
</PopoverPrimitive.Portal>
|
|
@@ -12,6 +12,7 @@ interface TableHeaderProps<TData> {
|
|
|
12
12
|
onFilterPopoverOpenChange: (colId: string, isOpen: boolean) => void
|
|
13
13
|
onFilterValueChange: (colId: string, value: any) => void
|
|
14
14
|
onClearFilter: (colId: string) => void
|
|
15
|
+
onNumberSort: (colId: string, direction: 'asc' | 'desc' | null) => void
|
|
15
16
|
labels?: DatatableFilterDropdownLabels
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -23,6 +24,7 @@ function TableHeader<TData>({
|
|
|
23
24
|
onFilterPopoverOpenChange,
|
|
24
25
|
onFilterValueChange,
|
|
25
26
|
onClearFilter,
|
|
27
|
+
onNumberSort,
|
|
26
28
|
labels = {},
|
|
27
29
|
}: TableHeaderProps<TData>) {
|
|
28
30
|
return (
|
|
@@ -39,7 +41,8 @@ function TableHeader<TData>({
|
|
|
39
41
|
(colFilterValue.type === 'select' && colFilterValue.value != null) ||
|
|
40
42
|
(colFilterValue.type === 'dateRange' && (
|
|
41
43
|
colFilterValue.value.from != null || colFilterValue.value.to != null
|
|
42
|
-
))
|
|
44
|
+
)) ||
|
|
45
|
+
(colFilterValue.type === 'numberSort' && colFilterValue.value != null)
|
|
43
46
|
)
|
|
44
47
|
|
|
45
48
|
return (
|
|
@@ -73,6 +76,7 @@ function TableHeader<TData>({
|
|
|
73
76
|
filterValue={datatableFilters[header.column.id]}
|
|
74
77
|
onFilterValueChange={onFilterValueChange}
|
|
75
78
|
onClearFilter={onClearFilter}
|
|
79
|
+
onNumberSort={onNumberSort}
|
|
76
80
|
labels={labels}
|
|
77
81
|
/>
|
|
78
82
|
) : (
|
|
@@ -9,12 +9,14 @@ interface UseDatatableFiltersProps {
|
|
|
9
9
|
isServerMode: boolean
|
|
10
10
|
controlled?: DatatableColumnFilters
|
|
11
11
|
onChange?: (next: DatatableColumnFilters) => void
|
|
12
|
+
onClearSorting?: (colId: string) => void
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function useDatatableFilters({
|
|
15
16
|
isServerMode,
|
|
16
17
|
controlled,
|
|
17
18
|
onChange,
|
|
19
|
+
onClearSorting,
|
|
18
20
|
}: UseDatatableFiltersProps) {
|
|
19
21
|
const isControlled = controlled !== undefined
|
|
20
22
|
const [internal, setInternal] = useState<DatatableColumnFilters>({})
|
|
@@ -42,18 +44,21 @@ export function useDatatableFilters({
|
|
|
42
44
|
const { from, to } = value.value
|
|
43
45
|
return from || to ? [...rest, { id: colId, value: { from, to } }] : rest
|
|
44
46
|
}
|
|
47
|
+
// numberSort does not use columnFilters — handled via sorting state
|
|
45
48
|
return rest
|
|
46
49
|
})
|
|
47
50
|
}, [filters, isServerMode, updateFilters])
|
|
48
51
|
|
|
49
52
|
const handleClearColumnFilter = useCallback((colId: string) => {
|
|
53
|
+
const prev = filters[colId]
|
|
50
54
|
const next = { ...filters }
|
|
51
55
|
delete next[colId]
|
|
52
56
|
updateFilters(next)
|
|
53
57
|
setColumnFilters((prev) => prev.filter((f) => f.id !== colId))
|
|
54
58
|
if (openFilterColumnId === colId) setOpenFilterColumnId(null)
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
// if clearing a numberSort column, also clear the table sorting
|
|
60
|
+
if (prev?.type === 'numberSort') onClearSorting?.(colId)
|
|
61
|
+
}, [filters, openFilterColumnId, updateFilters, onClearSorting])
|
|
57
62
|
const handleClearAllFilters = useCallback((onClearGlobal: () => void) => {
|
|
58
63
|
onClearGlobal()
|
|
59
64
|
setColumnFilters([])
|
|
@@ -71,6 +76,7 @@ export function useDatatableFilters({
|
|
|
71
76
|
if (v.type === 'text') return v.value !== ''
|
|
72
77
|
if (v.type === 'select') return v.value != null
|
|
73
78
|
if (v.type === 'dateRange') return v.value.from != null || v.value.to != null
|
|
79
|
+
if (v.type === 'numberSort') return v.value != null
|
|
74
80
|
return false
|
|
75
81
|
}, [filters])
|
|
76
82
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ColumnDef } from '@tanstack/react-table'
|
|
2
2
|
import { DatatableColumnMeta } from './Table.types'
|
|
3
|
+
import { currencySortingFn } from './utils'
|
|
3
4
|
|
|
4
5
|
export type OrderRow = {
|
|
5
6
|
id: string
|
|
@@ -82,10 +83,10 @@ export const DATATABLE_COLUMNS: ColumnDef<OrderRow, unknown>[] = [
|
|
|
82
83
|
{
|
|
83
84
|
header: 'Valor',
|
|
84
85
|
accessorKey: 'amount',
|
|
86
|
+
sortingFn: currencySortingFn,
|
|
85
87
|
meta: {
|
|
86
|
-
filterType: '
|
|
88
|
+
filterType: 'numberSort',
|
|
87
89
|
filterLabel: 'valor',
|
|
88
|
-
filterPlaceholder: 'Buscar pelo valor',
|
|
89
90
|
} satisfies DatatableColumnMeta,
|
|
90
91
|
},
|
|
91
92
|
{
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Row } from '@tanstack/react-table'
|
|
2
|
+
|
|
3
|
+
const parseNumericValue = (raw: unknown): number => {
|
|
4
|
+
if (typeof raw === 'number') return raw
|
|
5
|
+
if (typeof raw !== 'string') return 0
|
|
6
|
+
const cleaned = raw.replace(/[^\d,.-]/g, '').replace(/\.(?=\d{3})/g, '').replace(',', '.')
|
|
7
|
+
return parseFloat(cleaned) || 0
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const currencySortingFn = <TData>(
|
|
11
|
+
rowA: Row<TData>,
|
|
12
|
+
rowB: Row<TData>,
|
|
13
|
+
columnId: string
|
|
14
|
+
): number => {
|
|
15
|
+
const a = parseNumericValue(rowA.getValue(columnId))
|
|
16
|
+
const b = parseNumericValue(rowB.getValue(columnId))
|
|
17
|
+
return a - b
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
currencySortingFn.autoRemove = (val: unknown) => val == null
|
package/components/index.ts
CHANGED
|
@@ -18,5 +18,8 @@ export { default as RadioButton } from './RadioButton'
|
|
|
18
18
|
export { ToastProvider, Toaster } from './Toaster'
|
|
19
19
|
export { default as Dialog } from './Dialog'
|
|
20
20
|
export { Combobox } from './Combobox'
|
|
21
|
+
export { default as SplitButton } from './SplitButton'
|
|
21
22
|
export { default as Table } from './Table'
|
|
22
23
|
export { default as Tabs } from './Tabs'
|
|
24
|
+
export { Skeleton } from './Skeleton'
|
|
25
|
+
export type { SkeletonProps } from './Skeleton'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liguelead/design-system",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.36",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "components/index.ts",
|
|
6
6
|
"publishConfig": {
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"react-toastify": "^11.0.5",
|
|
26
26
|
"cmdk": "^1.1.1",
|
|
27
27
|
"date-fns": "^2.30.0",
|
|
28
|
-
"@tanstack/react-table": "^8.0.0"
|
|
28
|
+
"@tanstack/react-table": "^8.0.0",
|
|
29
|
+
"react-loading-skeleton": "^3.5.0"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
32
|
"lint": "eslint ."
|