oboda-fiesta 0.0.1
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/.claude/settings.local.json +9 -0
- package/components/_table/context.ts +20 -0
- package/components/_table/index.ts +10 -0
- package/components/_table/table-body.tsx +26 -0
- package/components/_table/table-cell.tsx +32 -0
- package/components/_table/table-head.tsx +171 -0
- package/components/_table/table-header-group.tsx +9 -0
- package/components/_table/table-header.tsx +35 -0
- package/components/_table/table-pagination.tsx +84 -0
- package/components/_table/table-provider.tsx +128 -0
- package/components/_table/table-row.tsx +12 -0
- package/components/_table/table.tsx +21 -0
- package/components/_table/types.ts +96 -0
- package/components/_table/utils.ts +1 -0
- package/components/accordion.tsx +53 -0
- package/components/audit-logs.tsx +125 -0
- package/components/autocomplete.tsx +111 -0
- package/components/avatar.tsx +53 -0
- package/components/badge.tsx +36 -0
- package/components/button.tsx +287 -0
- package/components/card.tsx +92 -0
- package/components/command.tsx +143 -0
- package/components/dialog.tsx +123 -0
- package/components/dropdown-menu.tsx +219 -0
- package/components/file-input.tsx +325 -0
- package/components/input.tsx +21 -0
- package/components/popover.tsx +48 -0
- package/components/scroll-area.tsx +58 -0
- package/components/select.tsx +160 -0
- package/components/table.tsx +1 -0
- package/components/tabs.tsx +42 -0
- package/components/typography.tsx +134 -0
- package/components.json +21 -0
- package/hooks/use-debouce.ts +17 -0
- package/hooks/use-mobile.ts +19 -0
- package/lib/utils.ts +14 -0
- package/package.json +51 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import { TableContextValue } from './types'
|
|
3
|
+
|
|
4
|
+
export const useTableContext = () => {
|
|
5
|
+
const context = useContext(TableContext)
|
|
6
|
+
if (!context) {
|
|
7
|
+
throw new Error('useTableContext must be used within a TableProvider')
|
|
8
|
+
}
|
|
9
|
+
return context
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const TableContext = createContext<TableContextValue>({
|
|
13
|
+
data: [],
|
|
14
|
+
columns: [],
|
|
15
|
+
table: null,
|
|
16
|
+
enableColumnOrdering: false,
|
|
17
|
+
enableColumnResizing: false,
|
|
18
|
+
enableSorting: false,
|
|
19
|
+
isLoading: false
|
|
20
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './table'
|
|
2
|
+
export * from './table-provider'
|
|
3
|
+
export * from './table-head'
|
|
4
|
+
export * from './table-header'
|
|
5
|
+
export * from './table-header-group'
|
|
6
|
+
export * from './table-body'
|
|
7
|
+
export * from './table-row'
|
|
8
|
+
export * from './table-cell'
|
|
9
|
+
export * from './table-pagination'
|
|
10
|
+
export * from './types'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { cn } from '@repo/oboda-fiesta/lib/utils'
|
|
2
|
+
import { useTableContext } from './context'
|
|
3
|
+
import { TableBodyProps } from './types'
|
|
4
|
+
import { TableCellRaw } from './table-cell'
|
|
5
|
+
import { TableRowRaw } from './table-row'
|
|
6
|
+
|
|
7
|
+
export const TableBody = ({ children, className, ...props }: TableBodyProps) => {
|
|
8
|
+
const { table, isLoading } = useTableContext()
|
|
9
|
+
const rows = table?.getRowModel().rows
|
|
10
|
+
|
|
11
|
+
if (!rows?.length && !isLoading) {
|
|
12
|
+
return (
|
|
13
|
+
<TableRowRaw>
|
|
14
|
+
<TableCellRaw colSpan={table?.getAllColumns().length} className='text-center'>
|
|
15
|
+
No result
|
|
16
|
+
</TableCellRaw>
|
|
17
|
+
</TableRowRaw>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<tbody data-slot='table-body' className={cn('overflow-x-auto [&_tr:last-child]:border-0', className)} {...props}>
|
|
23
|
+
{rows?.map((row) => children({ row }))}
|
|
24
|
+
</tbody>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { flexRender } from '@tanstack/react-table'
|
|
2
|
+
import { useTableContext } from './context'
|
|
3
|
+
import { TableCellProps } from './types'
|
|
4
|
+
import { cn } from '@repo/oboda-fiesta/lib/utils'
|
|
5
|
+
|
|
6
|
+
export const TableCellRaw = ({ className, ...props }: React.ComponentProps<'td'>) => {
|
|
7
|
+
return (
|
|
8
|
+
<td
|
|
9
|
+
data-slot='table-cell'
|
|
10
|
+
className={cn(
|
|
11
|
+
'text-foreground border-border whitespace-nowrap border-b px-4 py-3 align-middle text-sm',
|
|
12
|
+
'font-normal',
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const TableCell = ({ cell, className, ...props }: TableCellProps) => {
|
|
21
|
+
const { enableColumnResizing } = useTableContext()
|
|
22
|
+
const meta = cell.column.columnDef.meta as { align?: 'right' | 'center' }
|
|
23
|
+
return (
|
|
24
|
+
<TableCellRaw
|
|
25
|
+
className={cn(className, meta?.align === 'right' && 'text-right', meta?.align === 'center' && 'text-center')}
|
|
26
|
+
style={enableColumnResizing ? { width: cell.column.getSize() } : undefined}
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
30
|
+
</TableCellRaw>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { useSortable } from '@dnd-kit/sortable'
|
|
2
|
+
import { CSS } from '@dnd-kit/utilities'
|
|
3
|
+
import { flexRender, type Header } from '@tanstack/react-table'
|
|
4
|
+
import { cn } from '@repo/oboda-fiesta/lib/utils'
|
|
5
|
+
import { GripVertical, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'
|
|
6
|
+
import { TableHeadProps } from './types'
|
|
7
|
+
import { useTableContext } from './context'
|
|
8
|
+
|
|
9
|
+
const SortIcon = ({ header }: { header: Header<unknown, unknown> }) => {
|
|
10
|
+
const sortDirection = header.column.getIsSorted()
|
|
11
|
+
|
|
12
|
+
if (sortDirection === 'asc') {
|
|
13
|
+
return <ArrowUp className='h-4 w-4' />
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (sortDirection === 'desc') {
|
|
17
|
+
return <ArrowDown className='h-4 w-4' />
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return <ArrowUpDown className='h-4 w-4 opacity-50' />
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const TableHeadRaw = ({ className, ...props }: React.ComponentProps<'th'>) => {
|
|
24
|
+
return (
|
|
25
|
+
<th
|
|
26
|
+
data-slot='table-head'
|
|
27
|
+
className={cn(
|
|
28
|
+
'border-border text text-secondary h-12 whitespace-nowrap border-b px-4 text-left align-middle text-xs font-bold leading-[1.4] tracking-[0.02em]',
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type DraggableTableHeadProps = {
|
|
37
|
+
header: Header<unknown, unknown>
|
|
38
|
+
className?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const DraggableTableHead = ({ header, className }: DraggableTableHeadProps) => {
|
|
42
|
+
const { enableColumnResizing, enableColumnOrdering } = useTableContext()
|
|
43
|
+
const isResizable = enableColumnResizing
|
|
44
|
+
const isDraggable = enableColumnOrdering
|
|
45
|
+
const canSort = header.column.getCanSort()
|
|
46
|
+
const meta = header.column.columnDef.meta as { align?: 'right' | 'center' }
|
|
47
|
+
|
|
48
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: header.column.id })
|
|
49
|
+
|
|
50
|
+
const style = {
|
|
51
|
+
transform: CSS.Transform.toString(transform),
|
|
52
|
+
transition,
|
|
53
|
+
opacity: isDragging ? 0.5 : 1,
|
|
54
|
+
...(isResizable ? { width: header.getSize() } : {})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleSort = () => {
|
|
58
|
+
if (canSort) {
|
|
59
|
+
header.column.toggleSorting()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<TableHeadRaw
|
|
65
|
+
ref={setNodeRef}
|
|
66
|
+
key={header.id}
|
|
67
|
+
colSpan={header.colSpan}
|
|
68
|
+
className={cn(
|
|
69
|
+
isResizable && 'relative border-r',
|
|
70
|
+
isDragging && 'cursor-grabbing',
|
|
71
|
+
canSort && 'hover:bg-muted/50 cursor-pointer',
|
|
72
|
+
className
|
|
73
|
+
)}
|
|
74
|
+
style={style}
|
|
75
|
+
onClick={handleSort}
|
|
76
|
+
>
|
|
77
|
+
<div
|
|
78
|
+
className={cn(
|
|
79
|
+
'flex items-center justify-between',
|
|
80
|
+
meta?.align === 'right' && 'justify-end',
|
|
81
|
+
meta?.align === 'center' && 'justify-center'
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<div
|
|
85
|
+
className={cn(
|
|
86
|
+
'flex flex-1 items-center gap-2',
|
|
87
|
+
meta?.align === 'right' && 'justify-end',
|
|
88
|
+
meta?.align === 'center' && 'justify-center'
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
|
92
|
+
{canSort && !header.isPlaceholder && <SortIcon header={header} />}
|
|
93
|
+
</div>
|
|
94
|
+
{isDraggable && !header.isPlaceholder && (
|
|
95
|
+
<button
|
|
96
|
+
{...attributes}
|
|
97
|
+
{...listeners}
|
|
98
|
+
className='hover:text-muted-foreground/70 ml-2 cursor-grab touch-none'
|
|
99
|
+
onClick={(e) => e.stopPropagation()}
|
|
100
|
+
>
|
|
101
|
+
<GripVertical className='h-4 w-4' />
|
|
102
|
+
</button>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
{isResizable && (
|
|
106
|
+
<div
|
|
107
|
+
onDoubleClick={() => header.column.resetSize()}
|
|
108
|
+
onMouseDown={header.getResizeHandler()}
|
|
109
|
+
onTouchStart={header.getResizeHandler()}
|
|
110
|
+
className={cn(
|
|
111
|
+
'resizer bg-border absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none opacity-0 hover:opacity-100',
|
|
112
|
+
header.column.getIsResizing() && 'bg-secondary z-50 h-screen w-[2px] opacity-100',
|
|
113
|
+
'transition-opacity duration-150'
|
|
114
|
+
)}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
</TableHeadRaw>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const TableHead = ({ header, className }: TableHeadProps) => {
|
|
122
|
+
const { table } = useTableContext()
|
|
123
|
+
const isDraggable = table?.options.onColumnOrderChange !== undefined
|
|
124
|
+
const meta = header.column.columnDef.meta as { align?: 'right' | 'center' }
|
|
125
|
+
|
|
126
|
+
if (isDraggable) {
|
|
127
|
+
return <DraggableTableHead header={header} className={className} />
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const isResizable = table?.options.columnResizeMode !== undefined
|
|
131
|
+
const canSort = header.column.getCanSort()
|
|
132
|
+
|
|
133
|
+
const handleSort = () => {
|
|
134
|
+
if (canSort) {
|
|
135
|
+
header.column.toggleSorting()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<TableHeadRaw
|
|
141
|
+
key={header.id}
|
|
142
|
+
colSpan={header.colSpan}
|
|
143
|
+
className={cn(isResizable && 'relative border-r', canSort && 'hover:bg-muted/50 cursor-pointer', className)}
|
|
144
|
+
style={isResizable ? { width: header.getSize() } : undefined}
|
|
145
|
+
onClick={handleSort}
|
|
146
|
+
>
|
|
147
|
+
<div
|
|
148
|
+
className={cn(
|
|
149
|
+
'flex items-center gap-2',
|
|
150
|
+
meta?.align === 'right' && 'text-right',
|
|
151
|
+
meta?.align === 'center' && 'text-center'
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
|
155
|
+
{canSort && !header.isPlaceholder && <SortIcon header={header} />}
|
|
156
|
+
</div>
|
|
157
|
+
{isResizable && (
|
|
158
|
+
<div
|
|
159
|
+
onDoubleClick={() => header.column.resetSize()}
|
|
160
|
+
onMouseDown={header.getResizeHandler()}
|
|
161
|
+
onTouchStart={header.getResizeHandler()}
|
|
162
|
+
className={cn(
|
|
163
|
+
'resizer bg-border absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none opacity-0 hover:opacity-100',
|
|
164
|
+
header.column.getIsResizing() && 'bg-secondary z-50 h-screen w-[2px] opacity-100',
|
|
165
|
+
'transition-opacity duration-150'
|
|
166
|
+
)}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
</TableHeadRaw>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { cn } from '@repo/oboda-fiesta/lib/utils'
|
|
2
|
+
import { TableRowRaw } from './table-row'
|
|
3
|
+
import { TableHeaderGroupProps } from './types'
|
|
4
|
+
|
|
5
|
+
export const TableHeaderGroup = ({ headerGroup, children, className }: TableHeaderGroupProps) => (
|
|
6
|
+
<TableRowRaw key={headerGroup.id} className={cn(className, 'first:*:first:rounded-tl-lg first:*:last:rounded-tr-lg')}>
|
|
7
|
+
{headerGroup.headers.map((header) => children({ header }))}
|
|
8
|
+
</TableRowRaw>
|
|
9
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cn } from '@repo/oboda-fiesta/lib/utils'
|
|
2
|
+
import { TableHeaderProps } from './types'
|
|
3
|
+
import { useContext } from 'react'
|
|
4
|
+
import { TableContext, useTableContext } from './context'
|
|
5
|
+
import { SortableContext } from '@dnd-kit/sortable'
|
|
6
|
+
import { horizontalListSortingStrategy } from '@dnd-kit/sortable'
|
|
7
|
+
|
|
8
|
+
const TableHeaderRaw = ({ className, ...props }: React.ComponentProps<'thead'>) => {
|
|
9
|
+
return <thead data-slot='table-header' className={cn('bg-background z-1 sticky top-0', className)} {...props} />
|
|
10
|
+
}
|
|
11
|
+
export const TableHeader = ({ className, children }: TableHeaderProps) => {
|
|
12
|
+
const { table, enableColumnOrdering } = useTableContext()
|
|
13
|
+
const isDraggable = enableColumnOrdering
|
|
14
|
+
|
|
15
|
+
const content = (
|
|
16
|
+
<TableHeaderRaw className={className}>
|
|
17
|
+
{table?.getHeaderGroups().map((headerGroup) => children({ headerGroup }))}
|
|
18
|
+
</TableHeaderRaw>
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if (isDraggable && table) {
|
|
22
|
+
const currentColumns = table.getAllLeafColumns().map((col) => col.id)
|
|
23
|
+
const stateColumnOrder = table.getState().columnOrder
|
|
24
|
+
|
|
25
|
+
const columnOrder = stateColumnOrder?.length > 0 ? stateColumnOrder : currentColumns
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<SortableContext items={columnOrder} strategy={horizontalListSortingStrategy}>
|
|
29
|
+
{content}
|
|
30
|
+
</SortableContext>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return content
|
|
35
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { cn } from '@repo/oboda-fiesta/lib/utils'
|
|
2
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
3
|
+
import { Button } from '../button'
|
|
4
|
+
import { TablePaginationProps } from './types'
|
|
5
|
+
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../select'
|
|
6
|
+
|
|
7
|
+
export function TablePagination({
|
|
8
|
+
pageIndex,
|
|
9
|
+
pageSize,
|
|
10
|
+
pageCount,
|
|
11
|
+
totalRows,
|
|
12
|
+
onPageSizeChange,
|
|
13
|
+
onPreviousPage,
|
|
14
|
+
onNextPage,
|
|
15
|
+
pageSizeOptions = [10, 20, 30, 50, 100],
|
|
16
|
+
showPageSizeSelector = true,
|
|
17
|
+
isLoading = false,
|
|
18
|
+
className
|
|
19
|
+
}: TablePaginationProps) {
|
|
20
|
+
const startItem = pageIndex * pageSize + 1
|
|
21
|
+
const endItem = Math.min((pageIndex + 1) * pageSize, totalRows)
|
|
22
|
+
|
|
23
|
+
// Infer pagination state
|
|
24
|
+
const canPreviousPage = !isLoading && pageIndex > 0
|
|
25
|
+
const canNextPage = !isLoading && pageIndex < pageCount - 1
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={cn(
|
|
30
|
+
'flex w-full flex-col items-center justify-between gap-2 py-2 sm:flex-row sm:justify-end sm:gap-4',
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
{/* Mobile: Stack vertically, Desktop: Horizontal layout */}
|
|
35
|
+
<div className='flex w-full flex-col items-center gap-2 sm:w-auto sm:flex-row sm:gap-4'>
|
|
36
|
+
{showPageSizeSelector && (
|
|
37
|
+
<div className='flex w-full items-center justify-center gap-2 sm:w-auto sm:justify-start'>
|
|
38
|
+
<span className='text-muted-foreground whitespace-nowrap text-xs sm:text-sm'>Rows per page:</span>
|
|
39
|
+
<Select value={pageSize.toString()} onValueChange={(value) => onPageSizeChange(Number(value))}>
|
|
40
|
+
<SelectTrigger className='h-8 w-fit text-xs sm:text-sm'>
|
|
41
|
+
<SelectValue placeholder={pageSize.toString()} />
|
|
42
|
+
</SelectTrigger>
|
|
43
|
+
<SelectContent side='top'>
|
|
44
|
+
{pageSizeOptions.map((size) => (
|
|
45
|
+
<SelectItem key={size} value={size.toString()}>
|
|
46
|
+
{size}
|
|
47
|
+
</SelectItem>
|
|
48
|
+
))}
|
|
49
|
+
</SelectContent>
|
|
50
|
+
</Select>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
<div className='text-muted-foreground order-first text-xs sm:order-none sm:text-sm'>
|
|
55
|
+
{startItem}-{endItem} of {totalRows}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className='flex items-center gap-1'>
|
|
60
|
+
<Button
|
|
61
|
+
variant='tertiary'
|
|
62
|
+
size='sm'
|
|
63
|
+
onClick={onPreviousPage}
|
|
64
|
+
disabled={!canPreviousPage}
|
|
65
|
+
className='h-8 w-8 p-0 sm:h-9 sm:w-9'
|
|
66
|
+
>
|
|
67
|
+
<ChevronLeft className='h-4 w-4' />
|
|
68
|
+
<span className='sr-only'>Previous page</span>
|
|
69
|
+
</Button>
|
|
70
|
+
|
|
71
|
+
<Button
|
|
72
|
+
variant='tertiary'
|
|
73
|
+
size='sm'
|
|
74
|
+
onClick={onNextPage}
|
|
75
|
+
disabled={!canNextPage}
|
|
76
|
+
className='h-8 w-8 p-0 sm:h-9 sm:w-9'
|
|
77
|
+
>
|
|
78
|
+
<ChevronRight className='h-4 w-4' />
|
|
79
|
+
<span className='sr-only'>Next page</span>
|
|
80
|
+
</Button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
ColumnOrderState,
|
|
4
|
+
ColumnSizingState,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
SortingState,
|
|
7
|
+
Updater,
|
|
8
|
+
useReactTable
|
|
9
|
+
} from '@tanstack/react-table'
|
|
10
|
+
import { TableProviderProps } from './types'
|
|
11
|
+
import {
|
|
12
|
+
closestCenter,
|
|
13
|
+
DndContext,
|
|
14
|
+
DragEndEvent,
|
|
15
|
+
KeyboardSensor,
|
|
16
|
+
PointerSensor,
|
|
17
|
+
useSensor,
|
|
18
|
+
useSensors
|
|
19
|
+
} from '@dnd-kit/core'
|
|
20
|
+
import { arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
|
21
|
+
import { TableContext } from './context'
|
|
22
|
+
import { Loader2 } from 'lucide-react'
|
|
23
|
+
import { Table } from './table'
|
|
24
|
+
|
|
25
|
+
export const TableProvider = <TData, TValue>({
|
|
26
|
+
columns,
|
|
27
|
+
data,
|
|
28
|
+
children,
|
|
29
|
+
className,
|
|
30
|
+
enableColumnResizing = false,
|
|
31
|
+
enableColumnOrdering = false,
|
|
32
|
+
enableSorting = false,
|
|
33
|
+
sorting,
|
|
34
|
+
onSortingChange,
|
|
35
|
+
isLoading = false
|
|
36
|
+
}: TableProviderProps<TData, TValue>) => {
|
|
37
|
+
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({})
|
|
38
|
+
const [columnOrder, setColumnOrder] = useState<ColumnOrderState>(() => {
|
|
39
|
+
return []
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const table = useReactTable({
|
|
43
|
+
data,
|
|
44
|
+
columns,
|
|
45
|
+
getCoreRowModel: getCoreRowModel(),
|
|
46
|
+
enableSorting: enableSorting,
|
|
47
|
+
onSortingChange: (updaterOrValue) => {
|
|
48
|
+
const newValue = updaterOrValue instanceof Function ? updaterOrValue(sorting ?? []) : updaterOrValue
|
|
49
|
+
onSortingChange?.(newValue)
|
|
50
|
+
},
|
|
51
|
+
manualSorting: true,
|
|
52
|
+
manualFiltering: true,
|
|
53
|
+
manualPagination: true,
|
|
54
|
+
...(enableColumnResizing && {
|
|
55
|
+
columnResizeMode: 'onChange',
|
|
56
|
+
columnResizeDirection: 'ltr',
|
|
57
|
+
onColumnSizingChange: setColumnSizing
|
|
58
|
+
}),
|
|
59
|
+
...(enableColumnOrdering && {
|
|
60
|
+
onColumnOrderChange: setColumnOrder
|
|
61
|
+
}),
|
|
62
|
+
state: {
|
|
63
|
+
...(enableSorting && sorting?.length && { sorting }),
|
|
64
|
+
...(enableColumnResizing && { columnSizing }),
|
|
65
|
+
...(enableColumnOrdering && { columnOrder })
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const sensors = useSensors(
|
|
70
|
+
useSensor(PointerSensor, {
|
|
71
|
+
activationConstraint: {
|
|
72
|
+
distance: 8
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
useSensor(KeyboardSensor, {
|
|
76
|
+
coordinateGetter: sortableKeyboardCoordinates
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
81
|
+
const { active, over } = event
|
|
82
|
+
|
|
83
|
+
if (active.id !== over?.id && over?.id) {
|
|
84
|
+
setColumnOrder((currentOrder) => {
|
|
85
|
+
const actualOrder = currentOrder.length > 0 ? currentOrder : table.getAllLeafColumns().map((col) => col.id)
|
|
86
|
+
|
|
87
|
+
const oldIndex = actualOrder.indexOf(active.id as string)
|
|
88
|
+
const newIndex = actualOrder.indexOf(over.id as string)
|
|
89
|
+
|
|
90
|
+
if (oldIndex !== -1 && newIndex !== -1) {
|
|
91
|
+
return arrayMove(actualOrder, oldIndex, newIndex)
|
|
92
|
+
}
|
|
93
|
+
return actualOrder
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const content = (
|
|
99
|
+
<TableContext.Provider
|
|
100
|
+
value={{
|
|
101
|
+
data,
|
|
102
|
+
columns: columns as never,
|
|
103
|
+
table: table as never,
|
|
104
|
+
enableColumnOrdering,
|
|
105
|
+
enableColumnResizing,
|
|
106
|
+
enableSorting,
|
|
107
|
+
isLoading
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{isLoading && (
|
|
111
|
+
<div className='bg-background/50 absolute inset-0 z-10 flex items-center justify-center'>
|
|
112
|
+
<Loader2 className='h-4 w-4 animate-spin' />
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
<Table className={className}>{children}</Table>
|
|
116
|
+
</TableContext.Provider>
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if (enableColumnOrdering) {
|
|
120
|
+
return (
|
|
121
|
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
122
|
+
{content}
|
|
123
|
+
</DndContext>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return content
|
|
128
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { cn } from '@repo/oboda-fiesta/lib/utils'
|
|
2
|
+
import { TableRowProps } from './types'
|
|
3
|
+
|
|
4
|
+
export const TableRowRaw = ({ className, ...props }: React.ComponentProps<'tr'>) => {
|
|
5
|
+
return <tr data-slot='table-row' className={cn('transition-colors', 'hover:bg-muted', className)} {...props} />
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const TableRow = ({ row, children, className }: TableRowProps) => (
|
|
9
|
+
<TableRowRaw key={row.id} data-state={row.getIsSelected() && 'selected'} className={className}>
|
|
10
|
+
{row.getVisibleCells().map((cell) => children({ cell }))}
|
|
11
|
+
</TableRowRaw>
|
|
12
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cn } from '@repo/oboda-fiesta/lib/utils'
|
|
2
|
+
import { useTableContext } from './context'
|
|
3
|
+
|
|
4
|
+
export const Table = ({ className, ...props }: React.ComponentProps<'table'>) => {
|
|
5
|
+
const { table, enableColumnResizing } = useTableContext()
|
|
6
|
+
const isResizable = enableColumnResizing
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
data-slot='table-container'
|
|
11
|
+
className='border-border bg-background h-full w-full overflow-x-auto rounded-lg border shadow-sm'
|
|
12
|
+
>
|
|
13
|
+
<table
|
|
14
|
+
data-slot='table'
|
|
15
|
+
className={cn('w-full caption-bottom border-separate border-spacing-0 text-sm', className)}
|
|
16
|
+
style={isResizable && table ? { width: table.getCenterTotalSize() } : undefined}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Cell,
|
|
3
|
+
Column,
|
|
4
|
+
ColumnDef,
|
|
5
|
+
Header,
|
|
6
|
+
HeaderGroup,
|
|
7
|
+
Row,
|
|
8
|
+
Table,
|
|
9
|
+
SortingState,
|
|
10
|
+
OnChangeFn
|
|
11
|
+
} from '@tanstack/react-table'
|
|
12
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
ColumnDef,
|
|
16
|
+
ColumnResizeMode,
|
|
17
|
+
ColumnResizeDirection,
|
|
18
|
+
Cell,
|
|
19
|
+
Column,
|
|
20
|
+
Header,
|
|
21
|
+
HeaderGroup,
|
|
22
|
+
Row,
|
|
23
|
+
SortingState,
|
|
24
|
+
ColumnSizingState,
|
|
25
|
+
ColumnOrderState,
|
|
26
|
+
ColumnFiltersState,
|
|
27
|
+
PaginationState
|
|
28
|
+
} from '@tanstack/react-table'
|
|
29
|
+
|
|
30
|
+
export type TableContextValue = {
|
|
31
|
+
data: unknown[]
|
|
32
|
+
columns: ColumnDef<unknown, unknown>[]
|
|
33
|
+
table: Table<unknown> | null
|
|
34
|
+
enableColumnOrdering: boolean
|
|
35
|
+
enableColumnResizing: boolean
|
|
36
|
+
enableSorting: boolean
|
|
37
|
+
isLoading: boolean
|
|
38
|
+
}
|
|
39
|
+
export type TableProviderProps<TData, TValue> = {
|
|
40
|
+
columns: ColumnDef<TData, TValue>[]
|
|
41
|
+
data: TData[]
|
|
42
|
+
children: ReactNode
|
|
43
|
+
className?: string
|
|
44
|
+
enableColumnResizing?: boolean
|
|
45
|
+
enableColumnOrdering?: boolean
|
|
46
|
+
isLoading?: boolean
|
|
47
|
+
enableSorting?: boolean
|
|
48
|
+
sorting?: SortingState
|
|
49
|
+
onSortingChange?: (newSorting: SortingState) => void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type TableHeadProps = {
|
|
53
|
+
header: Header<unknown, unknown>
|
|
54
|
+
className?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type TableHeaderGroupProps = {
|
|
58
|
+
headerGroup: HeaderGroup<unknown>
|
|
59
|
+
children: (props: { header: Header<unknown, unknown> }) => ReactNode
|
|
60
|
+
className?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type TableHeaderProps = {
|
|
64
|
+
className?: string
|
|
65
|
+
children: (props: { headerGroup: HeaderGroup<unknown> }) => ReactNode
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type TableCellProps = {
|
|
69
|
+
cell: Cell<unknown, unknown>
|
|
70
|
+
className?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type TableRowProps = {
|
|
74
|
+
row: Row<unknown>
|
|
75
|
+
children: (props: { cell: Cell<unknown, unknown> }) => ReactNode
|
|
76
|
+
className?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type TableBodyProps = {
|
|
80
|
+
children: (props: { row: Row<unknown> }) => ReactNode
|
|
81
|
+
className?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type TablePaginationProps = {
|
|
85
|
+
pageIndex: number
|
|
86
|
+
pageSize: number
|
|
87
|
+
pageCount: number
|
|
88
|
+
totalRows: number
|
|
89
|
+
onPageSizeChange: (pageSize: number) => void
|
|
90
|
+
onPreviousPage: () => void
|
|
91
|
+
onNextPage: () => void
|
|
92
|
+
pageSizeOptions?: number[]
|
|
93
|
+
showPageSizeSelector?: boolean
|
|
94
|
+
className?: string
|
|
95
|
+
isLoading?: boolean
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|