@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
@@ -0,0 +1,120 @@
1
+ import styled from 'styled-components'
2
+ import { spacing, fontSize, fontWeight, radius } from '@liguelead/foundation'
3
+ import { parseColor } from "../../../../utils";
4
+ import { FunnelSimpleIcon, FunnelSimpleXIcon } from '@phosphor-icons/react'
5
+ import IconButton from '../../../IconButton'
6
+
7
+ export const FunnelInactiveIcon = styled(FunnelSimpleIcon)`
8
+ fill: ${({ theme }) => parseColor(theme.colors.neutral700)};
9
+ `
10
+
11
+ export const FunnelActiveIcon = styled(FunnelSimpleXIcon)`
12
+ fill: ${({ theme }) => parseColor(theme.colors.primary)};
13
+ `
14
+
15
+ export const FilterTriggerArea = styled.button`
16
+ display: inline-flex;
17
+ align-items: center;
18
+ gap: ${spacing.spacing8}px;
19
+ background: none;
20
+ border: none;
21
+ padding: 0;
22
+ margin: 0;
23
+ cursor: pointer;
24
+ color: inherit;
25
+ font: inherit;
26
+ flex: 1;
27
+ text-align: left;
28
+
29
+ &:focus-visible {
30
+ outline: 2px solid ${({ theme }) => parseColor(theme.colors.primary)};
31
+ outline-offset: 1px;
32
+ }
33
+ `
34
+
35
+ export const FilterIconWrapper = styled.span<{ $active?: boolean }>`
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ line-height: 0;
40
+ `
41
+
42
+ export const FilterButton = styled(IconButton)<{ $active?: boolean }>`
43
+ display: inline-flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ padding: 0;
47
+ transition: background 0.15s ease;
48
+
49
+ & svg {
50
+ width: 14px !important;
51
+ heigth: 14px !important;
52
+ }
53
+
54
+ &:focus-visible {
55
+ outline: 2px solid ${({ theme }) => parseColor(theme.colors.primary)};
56
+ outline-offset: 1px;
57
+ }
58
+ `
59
+
60
+ export const ClearFilterButton = styled(IconButton)`
61
+ display: inline-flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ padding: 0;
65
+ width: 12px !important;
66
+ heigth: 12px !important;
67
+ transition: background 0.15s ease, color 0.15s ease;
68
+
69
+ & svg {
70
+ width: 12px !important;
71
+ heigth: 12px !important;
72
+ }
73
+ `
74
+
75
+ export const FilterPopoverContent = styled.div`
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: ${spacing.spacing8}px;
79
+ min-width: 240px;
80
+ max-width: 320px;
81
+ background: ${({ theme }) => parseColor(theme.colors.white)};
82
+ border: 1px solid ${({ theme }) => parseColor(theme.colors.neutral200)};
83
+ border-radius: ${radius.radius4}px;
84
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
85
+ padding: ${spacing.spacing12}px;
86
+ `
87
+
88
+ export const FilterPopoverLabel = styled.span`
89
+ font-size: ${fontSize.fontSize12}px;
90
+ font-weight: ${fontWeight.fontWeight600};
91
+ color: ${({ theme }) => parseColor(theme.colors.textMedium)};
92
+ `
93
+
94
+ export const StyledTextInput = styled.input`
95
+ width: 100%;
96
+ height: 36px;
97
+ padding: 0 ${spacing.spacing12}px;
98
+ font-size: ${fontSize.fontSize14}px;
99
+ color: ${({ theme }) => parseColor(theme.colors.textDark)};
100
+ background: ${({ theme }) => parseColor(theme.colors.white)};
101
+ border: 1px solid ${({ theme }) => parseColor(theme.colors.neutral300)};
102
+ border-radius: ${radius.radius4}px;
103
+ outline: none;
104
+ box-sizing: border-box;
105
+ transition: border-color 0.2s ease;
106
+
107
+ &::placeholder {
108
+ color: ${({ theme }) => parseColor(theme.colors.textLight)};
109
+ }
110
+
111
+ &:focus {
112
+ border-color: ${({ theme }) => parseColor(theme.colors.primary)};
113
+ }
114
+ `
115
+
116
+ export const ClearFilterButtonWrapper = styled.span`
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: end;
120
+ `
@@ -0,0 +1,228 @@
1
+ import * as PopoverPrimitive from '@radix-ui/react-popover'
2
+ import { XIcon } from '@phosphor-icons/react'
3
+ import { DateRange } from 'react-day-picker'
4
+ import DatePicker from '../../../DatePicker'
5
+ import Select from '../../../Select'
6
+ import {
7
+ ColumnFilterValue,
8
+ DatatableColumnMeta,
9
+ DatatableFilterDropdownLabels,
10
+ } from '../../Table.types'
11
+
12
+ import { SortIconWrapper, ThContent } from '../../Table.styles'
13
+ import {
14
+ ClearFilterButton,
15
+ ClearFilterButtonWrapper,
16
+ FilterIconWrapper,
17
+ FilterPopoverContent,
18
+ FilterPopoverLabel,
19
+ FilterTriggerArea,
20
+ FunnelActiveIcon,
21
+ FunnelInactiveIcon,
22
+ StyledTextInput,
23
+ } from './DatatableColumnFilterMenu.styles'
24
+
25
+ interface DatatableColumnFilterMenuProps {
26
+ columnId: string
27
+ title: React.ReactNode
28
+ meta?: DatatableColumnMeta
29
+ open: boolean
30
+ onOpenChange: (isOpen: boolean) => void
31
+ filterValue: ColumnFilterValue | undefined
32
+ onFilterValueChange: (columnId: string, value: ColumnFilterValue | undefined) => void
33
+ onClearFilter: (columnId: string) => void
34
+ labels?: DatatableFilterDropdownLabels
35
+ }
36
+
37
+ const SELECT_PLACEHOLDER_VALUE = '__placeholder__'
38
+
39
+ function DatatableColumnFilterMenu({
40
+ columnId,
41
+ title,
42
+ meta,
43
+ open,
44
+ onOpenChange,
45
+ filterValue,
46
+ onFilterValueChange,
47
+ onClearFilter,
48
+ labels = {},
49
+ }: DatatableColumnFilterMenuProps) {
50
+ const filterType = meta?.filterType
51
+ const filterLabel = meta?.filterLabel ?? String(title)
52
+ const filterPlaceholder = meta?.filterPlaceholder ?? 'Pesquisar...'
53
+ const selectOptions = [
54
+ { label: filterPlaceholder, value: SELECT_PLACEHOLDER_VALUE },
55
+ ...(meta?.selectOptions ?? []).map((o) => ({
56
+ label: o.label,
57
+ value: String(o.value),
58
+ })),
59
+ ]
60
+
61
+ const hasActiveFilter =
62
+ filterValue !== undefined &&
63
+ ((filterValue.type === 'text' && filterValue.value !== '') ||
64
+ (filterValue.type === 'select' && filterValue.value != null) ||
65
+ (filterValue.type === 'dateRange' &&
66
+ (filterValue.value.from != null || filterValue.value.to != null)))
67
+
68
+ const handleTriggerClick = (e: React.MouseEvent) => {
69
+ e.stopPropagation()
70
+ }
71
+
72
+ const handleClearClick = (e: React.MouseEvent) => {
73
+ e.stopPropagation()
74
+ onClearFilter(columnId)
75
+ }
76
+
77
+ const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
78
+ onFilterValueChange(columnId, { type: 'text', value: e.target.value })
79
+ }
80
+
81
+ const handleTextKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
82
+ if (e.key === 'Enter') {
83
+ onOpenChange(false)
84
+ }
85
+ }
86
+
87
+ const handleRangeChange = (range: DateRange | undefined) => {
88
+ onFilterValueChange(columnId, {
89
+ type: 'dateRange',
90
+ value: range ? { from: range.from, to: range.to } : {},
91
+ })
92
+ }
93
+
94
+ const handleSelectChange = (value: string) => {
95
+ if (!value || value === SELECT_PLACEHOLDER_VALUE) {
96
+ onFilterValueChange(columnId, undefined)
97
+ } else {
98
+ onFilterValueChange(columnId, { type: 'select', value })
99
+ }
100
+ onOpenChange(false)
101
+ }
102
+
103
+ const rangeValue: DateRange | undefined =
104
+ filterValue?.type === 'dateRange'
105
+ ? { from: filterValue.value.from, to: filterValue.value.to }
106
+ : undefined
107
+
108
+ const textValue = filterValue?.type === 'text' ? filterValue.value : ''
109
+ const selectValue =
110
+ filterValue?.type === 'select' && filterValue.value != null
111
+ ? String(filterValue.value)
112
+ : SELECT_PLACEHOLDER_VALUE
113
+
114
+ if (!filterType) {
115
+ return (
116
+ <ThContent>
117
+ {title}
118
+ </ThContent>
119
+ )
120
+ }
121
+
122
+ return (
123
+ <PopoverPrimitive.Root
124
+ open={open}
125
+ onOpenChange={(isOpen) => {
126
+ onOpenChange(isOpen)
127
+ }}
128
+ >
129
+ <ThContent $filterable>
130
+ <PopoverPrimitive.Trigger asChild>
131
+ <FilterTriggerArea
132
+ aria-label={labels.filterButtonAriaLabel ?? `Abrir filtro da coluna ${filterLabel}`}
133
+ aria-pressed={hasActiveFilter}
134
+ aria-expanded={open}
135
+ onClick={handleTriggerClick}
136
+ >
137
+ {title}
138
+ <SortIconWrapper aria-hidden="true">
139
+ <FilterIconWrapper>
140
+ {hasActiveFilter
141
+ ? <FunnelActiveIcon size={14} />
142
+ : <FunnelInactiveIcon size={14} />
143
+ }
144
+ </FilterIconWrapper>
145
+ </SortIconWrapper>
146
+ </FilterTriggerArea>
147
+ </PopoverPrimitive.Trigger>
148
+
149
+ {hasActiveFilter && (
150
+ <ClearFilterButtonWrapper>
151
+ <ClearFilterButton
152
+ type="button"
153
+ variant='neutralGhost'
154
+ aria-label={`Limpar filtro da coluna ${filterLabel}`}
155
+ onClick={handleClearClick}
156
+ >
157
+ <XIcon size={12} weight="bold" />
158
+ </ClearFilterButton>
159
+ </ClearFilterButtonWrapper>
160
+ )}
161
+ </ThContent>
162
+
163
+ <PopoverPrimitive.Portal>
164
+ <PopoverPrimitive.Content
165
+ side="bottom"
166
+ align="start"
167
+ sideOffset={10}
168
+ alignOffset={-5}
169
+ style={{ zIndex: 200 }}
170
+ onOpenAutoFocus={(e) => e.preventDefault()}
171
+ onInteractOutside={(e) => {
172
+ const target = e.target as HTMLElement
173
+ if (target.closest('[data-radix-popper-content-wrapper]')) {
174
+ e.preventDefault()
175
+ }
176
+ }}
177
+ >
178
+ <FilterPopoverContent>
179
+ <FilterPopoverLabel>Filtrar por {filterLabel}</FilterPopoverLabel>
180
+
181
+ {filterType === 'text' && (
182
+ <StyledTextInput
183
+ type="text"
184
+ value={textValue}
185
+ onChange={handleTextChange}
186
+ onKeyDown={handleTextKeyDown}
187
+ placeholder={filterPlaceholder}
188
+ aria-label={`Filtrar por ${filterLabel}`}
189
+ autoFocus
190
+ />
191
+ )}
192
+
193
+ {filterType === 'dateRange' && (
194
+ <DatePicker
195
+ mode="range"
196
+ label=""
197
+ rangeValue={rangeValue}
198
+ onRangeChange={handleRangeChange}
199
+ size="sm"
200
+ icon
201
+ />
202
+ )}
203
+
204
+ {filterType === 'select' && (
205
+ <Select
206
+ key={`select-${columnId}-${selectValue}`}
207
+ label=""
208
+ options={selectOptions}
209
+ defaultValue={selectValue}
210
+ size="sm"
211
+ register={{
212
+ name: `datatable-select-${columnId}`,
213
+ ref: () => { },
214
+ onBlur: async () => { },
215
+ onChange: async (e: { target: { value: string } }) => {
216
+ handleSelectChange(e.target.value)
217
+ },
218
+ }}
219
+ />
220
+ )}
221
+ </FilterPopoverContent>
222
+ </PopoverPrimitive.Content>
223
+ </PopoverPrimitive.Portal>
224
+ </PopoverPrimitive.Root>
225
+ )
226
+ }
227
+
228
+ export default DatatableColumnFilterMenu
@@ -0,0 +1 @@
1
+ export { default } from './DatatableColumnFilterMenu'
@@ -0,0 +1,25 @@
1
+ import styled from 'styled-components'
2
+ import { spacing } from '@liguelead/foundation'
3
+
4
+ export const TopBarWrapper = styled.div`
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: space-between;
8
+ gap: ${spacing.spacing8}px;
9
+ margin-bottom: ${spacing.spacing16}px;
10
+ `
11
+
12
+ export const TopBarLeft = styled.div`
13
+ display: flex;
14
+ align-items: center;
15
+ gap: ${spacing.spacing8}px;
16
+ flex: 1;
17
+ min-width: 0;
18
+ `
19
+
20
+ export const TopBarRight = styled.div`
21
+ display: flex;
22
+ align-items: center;
23
+ gap: ${spacing.spacing8}px;
24
+ flex-shrink: 0;
25
+ `
@@ -0,0 +1,89 @@
1
+ import { Table } from '@tanstack/react-table'
2
+ import { DownloadSimpleIcon, FunnelSimpleXIcon } from '@phosphor-icons/react'
3
+ import Button from '../../../Button'
4
+ import { ExportButtonConfig } from '../../Table.types'
5
+ import { ColumnVisibility } from '../index'
6
+ import SearchInput from '../SearchInput/SearchInput'
7
+ import { TopBarLeft, TopBarRight, TopBarWrapper } from './DatatableTopBar.styles'
8
+
9
+ interface DatatableTopBarProps<TData> {
10
+ table: Table<TData>
11
+ globalFilter: string
12
+ setGlobalFilter: (v: string) => void
13
+ enableGlobalSearch: boolean
14
+ enableColumnVisibility: boolean
15
+ enableClearFilters: boolean
16
+ hasActiveFilters: boolean
17
+ onClearAll: () => void
18
+ searchPlaceholder?: string
19
+ searchWidth?: string
20
+ clearFiltersLabel?: string
21
+ showExportButton?: boolean
22
+ onExport?: () => void
23
+ exportButton?: ExportButtonConfig
24
+ }
25
+
26
+ function DatatableTopBar<TData>({
27
+ table,
28
+ globalFilter,
29
+ setGlobalFilter,
30
+ enableGlobalSearch,
31
+ enableColumnVisibility,
32
+ enableClearFilters,
33
+ hasActiveFilters,
34
+ onClearAll,
35
+ searchPlaceholder = 'Pesquisar',
36
+ searchWidth = '320px',
37
+ clearFiltersLabel = 'Limpar filtros',
38
+ showExportButton = false,
39
+ onExport,
40
+ exportButton = {},
41
+ }: DatatableTopBarProps<TData>) {
42
+ const showBar = enableGlobalSearch || enableColumnVisibility || (enableClearFilters && hasActiveFilters) || showExportButton
43
+
44
+ if (!showBar) return null
45
+
46
+ return (
47
+ <TopBarWrapper>
48
+ <TopBarLeft>
49
+ {enableGlobalSearch && (
50
+ <SearchInput
51
+ globalFilter={globalFilter}
52
+ setGlobalFilter={setGlobalFilter}
53
+ placeholder={searchPlaceholder}
54
+ width={searchWidth}
55
+ />
56
+ )}
57
+ </TopBarLeft>
58
+
59
+ <TopBarRight>
60
+ {enableClearFilters && hasActiveFilters && (
61
+ <Button
62
+ variant="solid"
63
+ color="danger200"
64
+ size="sm"
65
+ onClick={onClearAll}
66
+ aria-label={clearFiltersLabel}
67
+ >
68
+ <FunnelSimpleXIcon size={16} />
69
+ {clearFiltersLabel}
70
+ </Button>
71
+ )}
72
+ {showExportButton && (
73
+ <Button
74
+ variant={exportButton?.variant ?? 'neutralOutline'}
75
+ onClick={onExport}
76
+ disabled={exportButton?.disabled}
77
+ aria-label={exportButton?.ariaLabel ?? 'Exportar'}
78
+ >
79
+ <DownloadSimpleIcon size={16} />
80
+ {exportButton?.label ?? 'Exportar'}
81
+ </Button>
82
+ )}
83
+ {enableColumnVisibility && <ColumnVisibility table={table} />}
84
+ </TopBarRight>
85
+ </TopBarWrapper>
86
+ )
87
+ }
88
+
89
+ export default DatatableTopBar
@@ -0,0 +1 @@
1
+ export { default } from './DatatableTopBar'
@@ -0,0 +1,30 @@
1
+ import TextField from "../../../TextField";
2
+ import { MagnifyingGlassIcon } from "@phosphor-icons/react";
3
+
4
+ export type SearchInputProps = {
5
+ globalFilter: string;
6
+ setGlobalFilter: (value: string) => void;
7
+ placeholder?: string;
8
+ width?: string;
9
+ };
10
+
11
+ const SearchInput = ({
12
+ globalFilter,
13
+ setGlobalFilter,
14
+ placeholder = "Pesquisar",
15
+ width = '373px',
16
+ }: SearchInputProps) => {
17
+ return (
18
+ <div style={{ width }}>
19
+ <TextField
20
+ label=''
21
+ leftIcon={<MagnifyingGlassIcon />}
22
+ placeholder={placeholder}
23
+ value={globalFilter}
24
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setGlobalFilter(e.target.value)}
25
+ />
26
+ </div>
27
+ )
28
+ };
29
+
30
+ export default SearchInput;
@@ -0,0 +1,98 @@
1
+ import { flexRender, Header, HeaderGroup } from '@tanstack/react-table'
2
+ import { SortIconWrapper, Th, ThContent } from '../../Table.styles'
3
+ import { DatatableColumnFilters, DatatableColumnMeta, DatatableFilterDropdownLabels } from '../../Table.types'
4
+ import { ArrowDownIcon, ArrowUpIcon } from '../ColumnVisibility/ColumnVisibility.style'
5
+ import DatatableColumnFilterMenu from '../DatatableColumnFilterMenu/DatatableColumnFilterMenu'
6
+
7
+ interface TableHeaderProps<TData> {
8
+ headerGroups: HeaderGroup<TData>[]
9
+ isDatatable: boolean
10
+ datatableFilters: DatatableColumnFilters
11
+ openFilterColumnId: string | null
12
+ onFilterPopoverOpenChange: (colId: string, isOpen: boolean) => void
13
+ onFilterValueChange: (colId: string, value: any) => void
14
+ onClearFilter: (colId: string) => void
15
+ labels?: DatatableFilterDropdownLabels
16
+ }
17
+
18
+ function TableHeader<TData>({
19
+ headerGroups,
20
+ isDatatable,
21
+ datatableFilters,
22
+ openFilterColumnId,
23
+ onFilterPopoverOpenChange,
24
+ onFilterValueChange,
25
+ onClearFilter,
26
+ labels = {},
27
+ }: TableHeaderProps<TData>) {
28
+ return (
29
+ <thead>
30
+ {headerGroups.map((hg) => (
31
+ <tr key={hg.id}>
32
+ {hg.headers.map((header: Header<TData, unknown>) => {
33
+ const canSort = !isDatatable && header.column.getCanSort()
34
+ const sortDirection = header.column.getIsSorted()
35
+ const colMeta = header.column.columnDef.meta as DatatableColumnMeta | undefined
36
+ const colFilterValue = datatableFilters[header.column.id]
37
+ const colHasActiveFilter = isDatatable && colFilterValue !== undefined && (
38
+ (colFilterValue.type === 'text' && colFilterValue.value !== '') ||
39
+ (colFilterValue.type === 'select' && colFilterValue.value != null) ||
40
+ (colFilterValue.type === 'dateRange' && (
41
+ colFilterValue.value.from != null || colFilterValue.value.to != null
42
+ ))
43
+ )
44
+
45
+ return (
46
+ <Th
47
+ key={header.id}
48
+ $sortable={canSort}
49
+ $filterable={isDatatable && !!colMeta?.filterType}
50
+ $hasActiveSort={isDatatable ? colHasActiveFilter : !!sortDirection}
51
+ onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
52
+ onKeyDown={(e) => {
53
+ if (canSort && (e.key === 'Enter' || e.key === ' ')) {
54
+ e.preventDefault()
55
+ header.column.getToggleSortingHandler()?.(e)
56
+ }
57
+ }}
58
+ tabIndex={canSort ? 0 : undefined}
59
+ role={canSort ? 'button' : undefined}
60
+ aria-sort={
61
+ !isDatatable && sortDirection === 'asc' ? 'ascending'
62
+ : !isDatatable && sortDirection === 'desc' ? 'descending'
63
+ : undefined
64
+ }
65
+ >
66
+ {isDatatable ? (
67
+ <DatatableColumnFilterMenu
68
+ columnId={header.column.id}
69
+ title={flexRender(header.column.columnDef.header, header.getContext())}
70
+ meta={colMeta}
71
+ open={openFilterColumnId === header.column.id}
72
+ onOpenChange={(isOpen) => onFilterPopoverOpenChange(header.column.id, isOpen)}
73
+ filterValue={datatableFilters[header.column.id]}
74
+ onFilterValueChange={onFilterValueChange}
75
+ onClearFilter={onClearFilter}
76
+ labels={labels}
77
+ />
78
+ ) : (
79
+ <ThContent>
80
+ {flexRender(header.column.columnDef.header, header.getContext())}
81
+ {canSort && (
82
+ <SortIconWrapper aria-hidden="true">
83
+ <ArrowUpIcon size={14} $selected={sortDirection === 'asc' ? 'asc' : false} />
84
+ <ArrowDownIcon size={14} $selected={sortDirection === 'desc' ? 'desc' : false} />
85
+ </SortIconWrapper>
86
+ )}
87
+ </ThContent>
88
+ )}
89
+ </Th>
90
+ )
91
+ })}
92
+ </tr>
93
+ ))}
94
+ </thead>
95
+ )
96
+ }
97
+
98
+ export default TableHeader
@@ -0,0 +1,78 @@
1
+ import { Table } from '@tanstack/react-table'
2
+ import { CaretLeftIcon, CaretRightIcon, DotsThreeIcon } from '@phosphor-icons/react'
3
+ import {
4
+ PaginationButton,
5
+ PaginationContainer,
6
+ PaginationControls,
7
+ PaginationElipsis,
8
+ } from '../../Table.styles'
9
+ import IconButton from '../../../IconButton'
10
+ import { getPageNumbers } from '../../utils'
11
+
12
+ interface TablePaginationProps<TData> {
13
+ table: Table<TData>
14
+ currentPage: number
15
+ pageCount: number
16
+ ariaLabels?: { previous?: string; next?: string; page?: string }
17
+ }
18
+
19
+ function TablePagination<TData>({
20
+ table,
21
+ currentPage,
22
+ pageCount,
23
+ ariaLabels = {},
24
+ }: TablePaginationProps<TData>) {
25
+ const pageNumbersArray = getPageNumbers({ currentPage, pageCount })
26
+
27
+ return (
28
+ <PaginationContainer>
29
+ <PaginationControls>
30
+ <IconButton
31
+ variant="ghost"
32
+ onClick={() => table.previousPage()}
33
+ disabled={!table.getCanPreviousPage()}
34
+ aria-label={ariaLabels.previous ?? 'Página anterior'}
35
+ >
36
+ <CaretLeftIcon />
37
+ </IconButton>
38
+
39
+ {pageNumbersArray.map((page, index) =>
40
+ page === 'ellipsis' ? (
41
+ <PaginationElipsis key={`ellipsis-${index}`} aria-hidden="true">
42
+ <DotsThreeIcon size={16} />
43
+ </PaginationElipsis>
44
+ ) : (
45
+ <PaginationButton
46
+ key={page}
47
+ size="sm"
48
+ $active={page === currentPage}
49
+ variant={page === currentPage ? 'solid' : 'ghost'}
50
+ onClick={() => {
51
+ if (typeof page === 'number') table.setPageIndex(page - 1)
52
+ }}
53
+ aria-label={
54
+ ariaLabels.page
55
+ ? `${ariaLabels.page} ${page}`
56
+ : `Ir para página ${page}`
57
+ }
58
+ aria-current={page === currentPage ? 'page' : undefined}
59
+ >
60
+ {page}
61
+ </PaginationButton>
62
+ )
63
+ )}
64
+
65
+ <IconButton
66
+ variant="ghost"
67
+ onClick={() => table.nextPage()}
68
+ disabled={!table.getCanNextPage()}
69
+ aria-label={ariaLabels.next ?? 'Próxima página'}
70
+ >
71
+ <CaretRightIcon />
72
+ </IconButton>
73
+ </PaginationControls>
74
+ </PaginationContainer>
75
+ )
76
+ }
77
+
78
+ export default TablePagination
@@ -0,0 +1,6 @@
1
+ export { default as ColumnVisibility } from './ColumnVisibility/ColumnVisibility'
2
+ export { default as DatatableColumnFilterMenu } from './DatatableColumnFilterMenu'
3
+ export { default as DatatableTopBar } from './DatatableTopBar'
4
+ export { default as SearchInput } from './SearchInput/SearchInput'
5
+ export { default as TableHeader } from './TableHeader/TableHeader'
6
+ export { default as TablePagination } from './TablePagination/TablePagination'