@wealthx/shadcn 0.0.1 → 1.0.0
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/.turbo/turbo-build.log +160 -0
- package/CHANGELOG.md +13 -0
- package/CHANGES.md +345 -0
- package/dist/chunk-2WZVSBAY.mjs +232 -0
- package/dist/chunk-2Y7YJKPE.mjs +47 -0
- package/dist/chunk-3U7SD3MS.mjs +55 -0
- package/dist/chunk-3VQNJ235.mjs +114 -0
- package/dist/chunk-55CEW76V.mjs +35 -0
- package/dist/chunk-6AFMNC42.mjs +146 -0
- package/dist/chunk-6OJF6XRN.mjs +117 -0
- package/dist/chunk-7LDIMXGM.mjs +181 -0
- package/dist/chunk-AMJ23O53.mjs +122 -0
- package/dist/chunk-BBJBJSXQ.mjs +44 -0
- package/dist/chunk-BGP2N52Z.mjs +126 -0
- package/dist/chunk-BMFN37JH.mjs +41 -0
- package/dist/chunk-CGOKTPXU.mjs +79 -0
- package/dist/chunk-CZ3BW5GL.mjs +81 -0
- package/dist/chunk-DBHJ5KC3.mjs +55 -0
- package/dist/chunk-DDPA2XXS.mjs +97 -0
- package/dist/chunk-DS2AMHN2.mjs +30 -0
- package/dist/chunk-E3K6O4FZ.mjs +57 -0
- package/dist/chunk-FWCSY2DS.mjs +37 -0
- package/dist/chunk-GPRJQ24C.mjs +28 -0
- package/dist/chunk-HS7TFG7V.mjs +24 -0
- package/dist/chunk-HUVTPUV2.mjs +256 -0
- package/dist/chunk-IAOOZCUY.mjs +90 -0
- package/dist/chunk-JF4PHPD5.mjs +111 -0
- package/dist/chunk-JU2RUWHF.mjs +123 -0
- package/dist/chunk-KKHTJNMM.mjs +86 -0
- package/dist/chunk-MJIEMGRD.mjs +266 -0
- package/dist/chunk-MKFL5MNH.mjs +372 -0
- package/dist/chunk-MQ72DIBH.mjs +105 -0
- package/dist/chunk-NGYG2EA6.mjs +148 -0
- package/dist/chunk-NWZ46DJL.mjs +213 -0
- package/dist/chunk-OXQQNQZI.mjs +75 -0
- package/dist/chunk-PMKODV6M.mjs +161 -0
- package/dist/chunk-QOJ2DQD6.mjs +57 -0
- package/dist/chunk-RL772EH7.mjs +126 -0
- package/dist/chunk-SLWCCURD.mjs +99 -0
- package/dist/chunk-V7CNWJT3.mjs +10 -0
- package/dist/chunk-VG6UF6UT.mjs +68 -0
- package/dist/chunk-VYMHBV6D.mjs +123 -0
- package/dist/chunk-VZ2NR7L3.mjs +195 -0
- package/dist/chunk-YN5SYTOO.mjs +117 -0
- package/dist/chunk-Z3MK2KKZ.mjs +83 -0
- package/dist/chunk-ZN2QKLF6.mjs +187 -0
- package/dist/chunk-ZZV5JVNW.mjs +34 -0
- package/dist/components/ui/accordion.js +142 -0
- package/dist/components/ui/accordion.mjs +14 -0
- package/dist/components/ui/alert-dialog.js +413 -0
- package/dist/components/ui/alert-dialog.mjs +34 -0
- package/dist/components/ui/alert.js +134 -0
- package/dist/components/ui/alert.mjs +12 -0
- package/dist/components/ui/avatar.js +173 -0
- package/dist/components/ui/avatar.mjs +18 -0
- package/dist/components/ui/badge.js +163 -0
- package/dist/components/ui/badge.mjs +11 -0
- package/dist/components/ui/button.js +198 -0
- package/dist/components/ui/button.mjs +11 -0
- package/dist/components/ui/calendar.js +408 -0
- package/dist/components/ui/calendar.mjs +12 -0
- package/dist/components/ui/card.js +156 -0
- package/dist/components/ui/card.mjs +20 -0
- package/dist/components/ui/checkbox.js +166 -0
- package/dist/components/ui/checkbox.mjs +11 -0
- package/dist/components/ui/chip.js +199 -0
- package/dist/components/ui/chip.mjs +10 -0
- package/dist/components/ui/data-table.js +925 -0
- package/dist/components/ui/data-table.mjs +29 -0
- package/dist/components/ui/date-picker.js +561 -0
- package/dist/components/ui/date-picker.mjs +15 -0
- package/dist/components/ui/dialog.js +378 -0
- package/dist/components/ui/dialog.mjs +30 -0
- package/dist/components/ui/drawer.js +213 -0
- package/dist/components/ui/drawer.mjs +28 -0
- package/dist/components/ui/dropdown-menu.js +338 -0
- package/dist/components/ui/dropdown-menu.mjs +38 -0
- package/dist/components/ui/empty.js +173 -0
- package/dist/components/ui/empty.mjs +18 -0
- package/dist/components/ui/field.js +359 -0
- package/dist/components/ui/field.mjs +28 -0
- package/dist/components/ui/input-group.js +406 -0
- package/dist/components/ui/input-group.mjs +22 -0
- package/dist/components/ui/input-otp.js +149 -0
- package/dist/components/ui/input-otp.mjs +14 -0
- package/dist/components/ui/input.js +81 -0
- package/dist/components/ui/input.mjs +8 -0
- package/dist/components/ui/label.js +85 -0
- package/dist/components/ui/label.mjs +8 -0
- package/dist/components/ui/pagination.js +333 -0
- package/dist/components/ui/pagination.mjs +22 -0
- package/dist/components/ui/popover.js +167 -0
- package/dist/components/ui/popover.mjs +22 -0
- package/dist/components/ui/progress.js +97 -0
- package/dist/components/ui/progress.mjs +8 -0
- package/dist/components/ui/radio-group.js +178 -0
- package/dist/components/ui/radio-group.mjs +12 -0
- package/dist/components/ui/select.js +262 -0
- package/dist/components/ui/select.mjs +28 -0
- package/dist/components/ui/separator.js +86 -0
- package/dist/components/ui/separator.mjs +8 -0
- package/dist/components/ui/sheet.js +227 -0
- package/dist/components/ui/sheet.mjs +26 -0
- package/dist/components/ui/skeleton.js +75 -0
- package/dist/components/ui/skeleton.mjs +8 -0
- package/dist/components/ui/sonner.js +86 -0
- package/dist/components/ui/sonner.mjs +7 -0
- package/dist/components/ui/spinner.js +93 -0
- package/dist/components/ui/spinner.mjs +10 -0
- package/dist/components/ui/switch.js +178 -0
- package/dist/components/ui/switch.mjs +11 -0
- package/dist/components/ui/table.js +184 -0
- package/dist/components/ui/table.mjs +22 -0
- package/dist/components/ui/tabs.js +181 -0
- package/dist/components/ui/tabs.mjs +16 -0
- package/dist/components/ui/textarea.js +79 -0
- package/dist/components/ui/textarea.mjs +8 -0
- package/dist/components/ui/toggle-group.js +184 -0
- package/dist/components/ui/toggle-group.mjs +12 -0
- package/dist/components/ui/toggle.js +108 -0
- package/dist/components/ui/toggle.mjs +11 -0
- package/dist/components/ui/tooltip.js +140 -0
- package/dist/components/ui/tooltip.mjs +16 -0
- package/dist/index.js +4409 -0
- package/dist/index.mjs +462 -0
- package/dist/lib/colors.js +84 -0
- package/dist/lib/colors.mjs +13 -0
- package/dist/lib/theme-provider.js +150 -0
- package/dist/lib/theme-provider.mjs +13 -0
- package/dist/lib/typography.js +157 -0
- package/dist/lib/typography.mjs +25 -0
- package/dist/lib/utils.js +34 -0
- package/dist/lib/utils.mjs +7 -0
- package/dist/styles.css +2 -0
- package/package.json +228 -11
- package/scripts/build-css.ts +15 -9
- package/src/components/index.tsx +443 -0
- package/src/components/ui/accordion.tsx +99 -0
- package/src/components/ui/alert-dialog.tsx +239 -0
- package/src/components/ui/alert.tsx +81 -0
- package/src/components/ui/avatar.tsx +130 -0
- package/src/components/ui/badge.tsx +57 -0
- package/src/components/ui/button.tsx +69 -37
- package/src/components/ui/calendar.tsx +252 -0
- package/src/components/ui/card.tsx +106 -0
- package/src/components/ui/checkbox.tsx +111 -0
- package/src/components/ui/chip.tsx +65 -0
- package/src/components/ui/data-table.tsx +490 -0
- package/src/components/ui/date-picker.tsx +133 -0
- package/src/components/ui/dialog.tsx +195 -0
- package/src/components/ui/drawer.tsx +169 -0
- package/src/components/ui/dropdown-menu.tsx +315 -0
- package/src/components/ui/empty.tsx +128 -0
- package/src/components/ui/field.tsx +273 -0
- package/src/components/ui/input-group.tsx +190 -0
- package/src/components/ui/input-otp.tsx +90 -0
- package/src/components/ui/input.tsx +28 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/pagination.tsx +148 -0
- package/src/components/ui/popover.tsx +112 -0
- package/src/components/ui/progress.tsx +40 -0
- package/src/components/ui/radio-group.tsx +129 -0
- package/src/components/ui/select.tsx +201 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +182 -0
- package/src/components/ui/skeleton.tsx +22 -0
- package/src/components/ui/sonner.tsx +48 -0
- package/src/components/ui/spinner.tsx +41 -0
- package/src/components/ui/switch.tsx +126 -0
- package/src/components/ui/table.tsx +143 -0
- package/src/components/ui/tabs.tsx +119 -0
- package/src/components/ui/textarea.tsx +28 -0
- package/src/components/ui/toggle-group.tsx +94 -0
- package/src/components/ui/toggle.tsx +59 -0
- package/src/components/ui/tooltip.tsx +80 -0
- package/src/index.ts +15 -3
- package/src/lib/colors.ts +74 -0
- package/src/lib/slot.tsx +68 -0
- package/src/lib/theme-provider.tsx +134 -0
- package/src/lib/typography.ts +153 -0
- package/src/lib/utils.ts +1 -1
- package/src/styles/globals.css +377 -107
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +48 -2
- package/src/provider/ShadcnProvider.tsx +0 -89
- package/src/provider/index.ts +0 -2
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DataTable — WealthX Design System
|
|
5
|
+
*
|
|
6
|
+
* A headless, composable data-table built on \@tanstack/react-table.
|
|
7
|
+
* Composes existing WealthX shadcn components:
|
|
8
|
+
* - Table, TableHeader, TableBody, TableRow, TableHead, TableCell
|
|
9
|
+
* - Input (column filter)
|
|
10
|
+
* - Button (pagination actions, column toggle)
|
|
11
|
+
* - Checkbox (row selection)
|
|
12
|
+
* - Select (page-size selector)
|
|
13
|
+
* - Pagination (page navigation)
|
|
14
|
+
* - Skeleton (loading state)
|
|
15
|
+
* - DropdownMenu (column visibility toggle)
|
|
16
|
+
*
|
|
17
|
+
* All colors use design tokens — no hex codes.
|
|
18
|
+
*/
|
|
19
|
+
import { type ReactElement } from "react"
|
|
20
|
+
import * as React from "react"
|
|
21
|
+
import {
|
|
22
|
+
type ColumnDef,
|
|
23
|
+
type ColumnFiltersState,
|
|
24
|
+
type SortingState,
|
|
25
|
+
type VisibilityState,
|
|
26
|
+
type RowSelectionState,
|
|
27
|
+
type Table as TanstackTable,
|
|
28
|
+
flexRender,
|
|
29
|
+
getCoreRowModel,
|
|
30
|
+
getFilteredRowModel,
|
|
31
|
+
getPaginationRowModel,
|
|
32
|
+
getSortedRowModel,
|
|
33
|
+
useReactTable,
|
|
34
|
+
} from "@tanstack/react-table"
|
|
35
|
+
import {
|
|
36
|
+
ArrowUpDown,
|
|
37
|
+
ArrowUp,
|
|
38
|
+
ArrowDown,
|
|
39
|
+
ChevronLeftIcon,
|
|
40
|
+
ChevronRightIcon,
|
|
41
|
+
ChevronsLeftIcon,
|
|
42
|
+
ChevronsRightIcon,
|
|
43
|
+
SlidersHorizontal,
|
|
44
|
+
} from "lucide-react"
|
|
45
|
+
import { cn } from "@/lib/utils"
|
|
46
|
+
import {
|
|
47
|
+
Table,
|
|
48
|
+
TableBody,
|
|
49
|
+
TableCell,
|
|
50
|
+
TableHead,
|
|
51
|
+
TableHeader,
|
|
52
|
+
TableRow,
|
|
53
|
+
} from "@/components/ui/table"
|
|
54
|
+
import { Input } from "@/components/ui/input"
|
|
55
|
+
import { Button } from "@/components/ui/button"
|
|
56
|
+
import { Checkbox } from "@/components/ui/checkbox"
|
|
57
|
+
import {
|
|
58
|
+
Select,
|
|
59
|
+
SelectContent,
|
|
60
|
+
SelectItem,
|
|
61
|
+
SelectTrigger,
|
|
62
|
+
SelectValue,
|
|
63
|
+
} from "@/components/ui/select"
|
|
64
|
+
import {
|
|
65
|
+
DropdownMenu,
|
|
66
|
+
DropdownMenuCheckboxItem,
|
|
67
|
+
DropdownMenuContent,
|
|
68
|
+
DropdownMenuGroup,
|
|
69
|
+
DropdownMenuLabel,
|
|
70
|
+
DropdownMenuSeparator,
|
|
71
|
+
DropdownMenuTrigger,
|
|
72
|
+
} from "@/components/ui/dropdown-menu"
|
|
73
|
+
import { Skeleton } from "@/components/ui/skeleton"
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Types
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
export interface DataTableProps<TData, TValue> {
|
|
80
|
+
/** Column definitions — pass ColumnDef[] from \@tanstack/react-table */
|
|
81
|
+
columns: ColumnDef<TData, TValue>[]
|
|
82
|
+
/** Row data */
|
|
83
|
+
data: TData[]
|
|
84
|
+
/** Show a global search/filter input above the table */
|
|
85
|
+
searchKey?: string
|
|
86
|
+
/** Placeholder for the search input */
|
|
87
|
+
searchPlaceholder?: string
|
|
88
|
+
/** Enable row selection with checkboxes */
|
|
89
|
+
enableRowSelection?: boolean
|
|
90
|
+
/** Enable column visibility toggle dropdown */
|
|
91
|
+
enableColumnVisibility?: boolean
|
|
92
|
+
/** Page size options for the page-size selector */
|
|
93
|
+
pageSizeOptions?: number[]
|
|
94
|
+
/** Show loading skeleton */
|
|
95
|
+
loading?: boolean
|
|
96
|
+
/** Number of skeleton rows to show while loading */
|
|
97
|
+
skeletonRows?: number
|
|
98
|
+
/** Callback when row selection changes */
|
|
99
|
+
onRowSelectionChange?: (selection: RowSelectionState) => void
|
|
100
|
+
/** Additional className for the root wrapper */
|
|
101
|
+
className?: string
|
|
102
|
+
/** Render a toolbar above the table (replaces default toolbar) */
|
|
103
|
+
toolbar?: (table: TanstackTable<TData>) => React.ReactNode
|
|
104
|
+
/** Text shown when no results found */
|
|
105
|
+
emptyText?: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Helper: sortable header
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
export function DataTableColumnHeader({
|
|
113
|
+
column,
|
|
114
|
+
title,
|
|
115
|
+
className,
|
|
116
|
+
}: {
|
|
117
|
+
column: { getIsSorted: () => false | "asc" | "desc"; toggleSorting: (desc?: boolean) => void; getCanSort: () => boolean }
|
|
118
|
+
title: string
|
|
119
|
+
className?: string
|
|
120
|
+
}): ReactElement {
|
|
121
|
+
if (!column.getCanSort()) {
|
|
122
|
+
return <span className={className}>{title}</span>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sorted = column.getIsSorted()
|
|
126
|
+
|
|
127
|
+
let sortIcon: ReactElement
|
|
128
|
+
if (sorted === "asc") {
|
|
129
|
+
sortIcon = <ArrowUp className="ml-1 size-3.5" />
|
|
130
|
+
} else if (sorted === "desc") {
|
|
131
|
+
sortIcon = <ArrowDown className="ml-1 size-3.5" />
|
|
132
|
+
} else {
|
|
133
|
+
sortIcon = <ArrowUpDown className="ml-1 size-3.5" />
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Button
|
|
138
|
+
className={cn("-ml-3 h-8 font-medium text-muted-foreground hover:text-foreground", className)}
|
|
139
|
+
onClick={() => { column.toggleSorting(sorted === "asc"); }}
|
|
140
|
+
size="sm"
|
|
141
|
+
variant="ghost"
|
|
142
|
+
>
|
|
143
|
+
{title}
|
|
144
|
+
{sortIcon}
|
|
145
|
+
</Button>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Helper: selection column
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
export function getSelectionColumn<TData>(): ColumnDef<TData> {
|
|
154
|
+
return {
|
|
155
|
+
id: "select",
|
|
156
|
+
header: ({ table }) => (
|
|
157
|
+
<Checkbox
|
|
158
|
+
aria-label="Select all"
|
|
159
|
+
checked={table.getIsAllPageRowsSelected()}
|
|
160
|
+
indeterminate={table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()}
|
|
161
|
+
onCheckedChange={(checked) => { table.toggleAllPageRowsSelected(Boolean(checked)); }}
|
|
162
|
+
/>
|
|
163
|
+
),
|
|
164
|
+
cell: ({ row }) => (
|
|
165
|
+
<Checkbox
|
|
166
|
+
aria-label="Select row"
|
|
167
|
+
checked={row.getIsSelected()}
|
|
168
|
+
onCheckedChange={(checked) => { row.toggleSelected(Boolean(checked)); }}
|
|
169
|
+
/>
|
|
170
|
+
),
|
|
171
|
+
enableSorting: false,
|
|
172
|
+
enableHiding: false,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Sub-components
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
function DataTableToolbar<TData>({
|
|
181
|
+
table,
|
|
182
|
+
searchKey,
|
|
183
|
+
searchPlaceholder,
|
|
184
|
+
enableColumnVisibility,
|
|
185
|
+
}: {
|
|
186
|
+
table: TanstackTable<TData>
|
|
187
|
+
searchKey?: string
|
|
188
|
+
searchPlaceholder?: string
|
|
189
|
+
enableColumnVisibility?: boolean
|
|
190
|
+
}): ReactElement {
|
|
191
|
+
return (
|
|
192
|
+
<div className="flex items-center justify-between gap-2 py-4" data-slot="data-table-toolbar">
|
|
193
|
+
<div className="flex flex-1 items-center gap-2">
|
|
194
|
+
{searchKey ? <Input
|
|
195
|
+
className="max-w-xs"
|
|
196
|
+
onChange={(e) => table.getColumn(searchKey)?.setFilterValue(e.target.value)}
|
|
197
|
+
placeholder={searchPlaceholder ?? `Filter ${searchKey}...`}
|
|
198
|
+
value={table.getColumn(searchKey)?.getFilterValue() as string}
|
|
199
|
+
/> : null}
|
|
200
|
+
</div>
|
|
201
|
+
{enableColumnVisibility ? <DropdownMenu>
|
|
202
|
+
<DropdownMenuTrigger
|
|
203
|
+
render={<Button className="ml-auto h-8 gap-1.5" size="sm" variant="outline" />}
|
|
204
|
+
>
|
|
205
|
+
<SlidersHorizontal className="size-3.5" />
|
|
206
|
+
<span className="hidden sm:inline">Columns</span>
|
|
207
|
+
</DropdownMenuTrigger>
|
|
208
|
+
<DropdownMenuContent>
|
|
209
|
+
<DropdownMenuGroup>
|
|
210
|
+
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
|
211
|
+
<DropdownMenuSeparator />
|
|
212
|
+
{table
|
|
213
|
+
.getAllColumns()
|
|
214
|
+
.filter((col) => col.getCanHide())
|
|
215
|
+
.map((col) => (
|
|
216
|
+
<DropdownMenuCheckboxItem
|
|
217
|
+
checked={col.getIsVisible()}
|
|
218
|
+
className="capitalize"
|
|
219
|
+
key={col.id}
|
|
220
|
+
onCheckedChange={(value) => { col.toggleVisibility(Boolean(value)); }}
|
|
221
|
+
>
|
|
222
|
+
{col.id}
|
|
223
|
+
</DropdownMenuCheckboxItem>
|
|
224
|
+
))}
|
|
225
|
+
</DropdownMenuGroup>
|
|
226
|
+
</DropdownMenuContent>
|
|
227
|
+
</DropdownMenu> : null}
|
|
228
|
+
</div>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function DataTablePagination<TData>({
|
|
233
|
+
table,
|
|
234
|
+
pageSizeOptions = [10, 20, 30, 50],
|
|
235
|
+
}: {
|
|
236
|
+
table: TanstackTable<TData>
|
|
237
|
+
pageSizeOptions?: number[]
|
|
238
|
+
}): ReactElement {
|
|
239
|
+
return (
|
|
240
|
+
<div
|
|
241
|
+
className="flex items-center justify-between gap-4 py-4"
|
|
242
|
+
data-slot="data-table-pagination"
|
|
243
|
+
>
|
|
244
|
+
{/* Selected rows info */}
|
|
245
|
+
<p className="text-sm text-muted-foreground">
|
|
246
|
+
{table.getFilteredSelectedRowModel().rows.length > 0 && (
|
|
247
|
+
<>
|
|
248
|
+
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
|
249
|
+
{table.getFilteredRowModel().rows.length} row(s) selected.
|
|
250
|
+
</>
|
|
251
|
+
)}
|
|
252
|
+
</p>
|
|
253
|
+
|
|
254
|
+
<div className="flex items-center gap-6">
|
|
255
|
+
{/* Page size selector */}
|
|
256
|
+
<div className="flex items-center gap-2">
|
|
257
|
+
<p className="text-sm text-muted-foreground whitespace-nowrap">Rows per page</p>
|
|
258
|
+
<Select
|
|
259
|
+
onValueChange={(value) => { table.setPageSize(Number(value)); }}
|
|
260
|
+
value={`${table.getState().pagination.pageSize}`}
|
|
261
|
+
>
|
|
262
|
+
<SelectTrigger className="w-[70px]" size="sm">
|
|
263
|
+
<SelectValue placeholder={`${table.getState().pagination.pageSize}`} />
|
|
264
|
+
</SelectTrigger>
|
|
265
|
+
<SelectContent>
|
|
266
|
+
{pageSizeOptions.map((pageSize) => (
|
|
267
|
+
<SelectItem key={pageSize} value={`${pageSize}`}>
|
|
268
|
+
{pageSize}
|
|
269
|
+
</SelectItem>
|
|
270
|
+
))}
|
|
271
|
+
</SelectContent>
|
|
272
|
+
</Select>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
{/* Page info */}
|
|
276
|
+
<p className="text-sm text-muted-foreground whitespace-nowrap">
|
|
277
|
+
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
|
278
|
+
</p>
|
|
279
|
+
|
|
280
|
+
{/* Page navigation */}
|
|
281
|
+
<div className="flex items-center gap-1">
|
|
282
|
+
<Button
|
|
283
|
+
aria-label="Go to first page"
|
|
284
|
+
disabled={!table.getCanPreviousPage()}
|
|
285
|
+
onClick={() => { table.setPageIndex(0); }}
|
|
286
|
+
size="icon-sm"
|
|
287
|
+
variant="outline"
|
|
288
|
+
>
|
|
289
|
+
<ChevronsLeftIcon className="size-4" />
|
|
290
|
+
</Button>
|
|
291
|
+
<Button
|
|
292
|
+
aria-label="Go to previous page"
|
|
293
|
+
disabled={!table.getCanPreviousPage()}
|
|
294
|
+
onClick={() => { table.previousPage(); }}
|
|
295
|
+
size="icon-sm"
|
|
296
|
+
variant="outline"
|
|
297
|
+
>
|
|
298
|
+
<ChevronLeftIcon className="size-4" />
|
|
299
|
+
</Button>
|
|
300
|
+
<Button
|
|
301
|
+
aria-label="Go to next page"
|
|
302
|
+
disabled={!table.getCanNextPage()}
|
|
303
|
+
onClick={() => { table.nextPage(); }}
|
|
304
|
+
size="icon-sm"
|
|
305
|
+
variant="outline"
|
|
306
|
+
>
|
|
307
|
+
<ChevronRightIcon className="size-4" />
|
|
308
|
+
</Button>
|
|
309
|
+
<Button
|
|
310
|
+
aria-label="Go to last page"
|
|
311
|
+
disabled={!table.getCanNextPage()}
|
|
312
|
+
onClick={() => { table.setPageIndex(table.getPageCount() - 1); }}
|
|
313
|
+
size="icon-sm"
|
|
314
|
+
variant="outline"
|
|
315
|
+
>
|
|
316
|
+
<ChevronsRightIcon className="size-4" />
|
|
317
|
+
</Button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// Loading skeleton
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
function DataTableSkeleton({
|
|
329
|
+
columnCount,
|
|
330
|
+
rowCount = 5,
|
|
331
|
+
}: {
|
|
332
|
+
columnCount: number
|
|
333
|
+
rowCount?: number
|
|
334
|
+
}): ReactElement {
|
|
335
|
+
return (
|
|
336
|
+
<>
|
|
337
|
+
{Array.from({ length: rowCount }).map((_, rowIdx) => (
|
|
338
|
+
<TableRow key={`skeleton-row-${String(rowIdx)}`}>
|
|
339
|
+
{Array.from({ length: columnCount }).map((_inner, colIdx) => (
|
|
340
|
+
<TableCell key={`skeleton-cell-${String(rowIdx)}-${String(colIdx)}`}>
|
|
341
|
+
<Skeleton className="h-4 w-3/4" />
|
|
342
|
+
</TableCell>
|
|
343
|
+
))}
|
|
344
|
+
</TableRow>
|
|
345
|
+
))}
|
|
346
|
+
</>
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// Main DataTable
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
function DataTable<TData, TValue>({
|
|
355
|
+
columns: userColumns,
|
|
356
|
+
data,
|
|
357
|
+
searchKey,
|
|
358
|
+
searchPlaceholder,
|
|
359
|
+
enableRowSelection = false,
|
|
360
|
+
enableColumnVisibility = false,
|
|
361
|
+
pageSizeOptions = [10, 20, 30, 50],
|
|
362
|
+
loading = false,
|
|
363
|
+
skeletonRows = 5,
|
|
364
|
+
onRowSelectionChange,
|
|
365
|
+
className,
|
|
366
|
+
toolbar,
|
|
367
|
+
emptyText = "No results.",
|
|
368
|
+
}: DataTableProps<TData, TValue>): ReactElement {
|
|
369
|
+
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
370
|
+
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
|
|
371
|
+
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
|
|
372
|
+
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({})
|
|
373
|
+
|
|
374
|
+
// Prepend selection column if enabled
|
|
375
|
+
const resolvedColumns = React.useMemo(() => {
|
|
376
|
+
if (!enableRowSelection) return userColumns
|
|
377
|
+
return [getSelectionColumn<TData>(), ...userColumns] as ColumnDef<TData, TValue>[]
|
|
378
|
+
}, [userColumns, enableRowSelection])
|
|
379
|
+
|
|
380
|
+
const table = useReactTable({
|
|
381
|
+
data,
|
|
382
|
+
columns: resolvedColumns,
|
|
383
|
+
state: {
|
|
384
|
+
sorting,
|
|
385
|
+
columnFilters,
|
|
386
|
+
columnVisibility,
|
|
387
|
+
rowSelection,
|
|
388
|
+
},
|
|
389
|
+
onSortingChange: setSorting,
|
|
390
|
+
onColumnFiltersChange: setColumnFilters,
|
|
391
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
392
|
+
onRowSelectionChange: (updater) => {
|
|
393
|
+
const next = typeof updater === "function" ? updater(rowSelection) : updater
|
|
394
|
+
setRowSelection(next)
|
|
395
|
+
onRowSelectionChange?.(next)
|
|
396
|
+
},
|
|
397
|
+
getCoreRowModel: getCoreRowModel(),
|
|
398
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
399
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
400
|
+
getSortedRowModel: getSortedRowModel(),
|
|
401
|
+
enableRowSelection,
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
function renderTableBody(): ReactElement {
|
|
405
|
+
if (loading) {
|
|
406
|
+
return <DataTableSkeleton columnCount={resolvedColumns.length} rowCount={skeletonRows} />
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (table.getRowModel().rows.length) {
|
|
410
|
+
return (
|
|
411
|
+
<>
|
|
412
|
+
{table.getRowModel().rows.map((row) => (
|
|
413
|
+
<TableRow
|
|
414
|
+
data-state={row.getIsSelected() ? "selected" : undefined}
|
|
415
|
+
key={row.id}
|
|
416
|
+
>
|
|
417
|
+
{row.getVisibleCells().map((cell) => (
|
|
418
|
+
<TableCell key={cell.id}>
|
|
419
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
420
|
+
</TableCell>
|
|
421
|
+
))}
|
|
422
|
+
</TableRow>
|
|
423
|
+
))}
|
|
424
|
+
</>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<TableRow>
|
|
430
|
+
<TableCell
|
|
431
|
+
className="h-24 text-center text-muted-foreground"
|
|
432
|
+
colSpan={resolvedColumns.length}
|
|
433
|
+
>
|
|
434
|
+
{emptyText}
|
|
435
|
+
</TableCell>
|
|
436
|
+
</TableRow>
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<div className={cn("w-full font-sans", className)} data-slot="data-table">
|
|
442
|
+
{/* Toolbar */}
|
|
443
|
+
{toolbar
|
|
444
|
+
? toolbar(table)
|
|
445
|
+
: (searchKey || enableColumnVisibility) && (
|
|
446
|
+
<DataTableToolbar
|
|
447
|
+
enableColumnVisibility={enableColumnVisibility}
|
|
448
|
+
searchKey={searchKey}
|
|
449
|
+
searchPlaceholder={searchPlaceholder}
|
|
450
|
+
table={table}
|
|
451
|
+
/>
|
|
452
|
+
)}
|
|
453
|
+
|
|
454
|
+
{/* Table */}
|
|
455
|
+
<div className="border border-border">
|
|
456
|
+
<Table>
|
|
457
|
+
<TableHeader>
|
|
458
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
459
|
+
<TableRow key={headerGroup.id}>
|
|
460
|
+
{headerGroup.headers.map((header) => (
|
|
461
|
+
<TableHead key={header.id}>
|
|
462
|
+
{header.isPlaceholder
|
|
463
|
+
? null
|
|
464
|
+
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
465
|
+
</TableHead>
|
|
466
|
+
))}
|
|
467
|
+
</TableRow>
|
|
468
|
+
))}
|
|
469
|
+
</TableHeader>
|
|
470
|
+
<TableBody>
|
|
471
|
+
{renderTableBody()}
|
|
472
|
+
</TableBody>
|
|
473
|
+
</Table>
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
{/* Pagination */}
|
|
477
|
+
<DataTablePagination pageSizeOptions={pageSizeOptions} table={table} />
|
|
478
|
+
</div>
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export {
|
|
483
|
+
DataTable,
|
|
484
|
+
DataTableToolbar,
|
|
485
|
+
DataTablePagination,
|
|
486
|
+
DataTableSkeleton,
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Re-export tanstack types for consumer convenience
|
|
490
|
+
export type { ColumnDef, SortingState, ColumnFiltersState, VisibilityState, RowSelectionState }
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { type ReactElement } from "react"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
import { format } from "date-fns"
|
|
6
|
+
import { CalendarIcon } from "lucide-react"
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
import { Button } from "@/components/ui/button"
|
|
9
|
+
import { Calendar } from "@/components/ui/calendar"
|
|
10
|
+
import {
|
|
11
|
+
Popover,
|
|
12
|
+
PopoverContent,
|
|
13
|
+
PopoverTrigger,
|
|
14
|
+
} from "@/components/ui/popover"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* DatePicker — WealthX Design System
|
|
18
|
+
* Figma: https://www.figma.com/design/9V9F0NGVsif8LGmEhVjOcT/Design-System---shadcn?node-id=73-226
|
|
19
|
+
*
|
|
20
|
+
* Composes Popover + Calendar + Button.
|
|
21
|
+
* Variants:
|
|
22
|
+
* - Date-only picker (default)
|
|
23
|
+
* - Date + Time picker (showTimePicker)
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export interface DatePickerProps {
|
|
27
|
+
value?: Date
|
|
28
|
+
onChange?: (date: Date | undefined) => void
|
|
29
|
+
placeholder?: string
|
|
30
|
+
/** Show a time input below the calendar */
|
|
31
|
+
showTimePicker?: boolean
|
|
32
|
+
disabled?: boolean
|
|
33
|
+
className?: string
|
|
34
|
+
/** Passed through to Calendar (e.g. fromYear, toYear, captionLayout) */
|
|
35
|
+
calendarProps?: Omit<
|
|
36
|
+
React.ComponentProps<typeof Calendar>,
|
|
37
|
+
"mode" | "selected" | "onSelect"
|
|
38
|
+
>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function DatePicker({
|
|
42
|
+
value,
|
|
43
|
+
onChange,
|
|
44
|
+
placeholder = "Pick a date",
|
|
45
|
+
showTimePicker = false,
|
|
46
|
+
disabled = false,
|
|
47
|
+
className,
|
|
48
|
+
calendarProps,
|
|
49
|
+
}: DatePickerProps): ReactElement {
|
|
50
|
+
const [open, setOpen] = React.useState(false)
|
|
51
|
+
|
|
52
|
+
function handleDaySelect(day: Date | undefined): void {
|
|
53
|
+
if (!day) {
|
|
54
|
+
onChange?.(undefined)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
// Preserve existing time when selecting a new day
|
|
58
|
+
if (showTimePicker && value) {
|
|
59
|
+
day.setHours(value.getHours(), value.getMinutes())
|
|
60
|
+
}
|
|
61
|
+
onChange?.(day)
|
|
62
|
+
if (!showTimePicker) setOpen(false)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleTimeChange(e: React.ChangeEvent<HTMLInputElement>): void {
|
|
66
|
+
const [hours, minutes] = e.target.value.split(":").map(Number)
|
|
67
|
+
const next = value ? new Date(value) : new Date()
|
|
68
|
+
next.setHours(hours, minutes, 0, 0)
|
|
69
|
+
onChange?.(next)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const timeValue = value
|
|
73
|
+
? `${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}`
|
|
74
|
+
: ""
|
|
75
|
+
|
|
76
|
+
let displayValue: string | undefined
|
|
77
|
+
if (value && showTimePicker) {
|
|
78
|
+
displayValue = format(value, "dd/MM/yyyy HH:mm")
|
|
79
|
+
} else if (value) {
|
|
80
|
+
displayValue = format(value, "dd/MM/yyyy")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Popover onOpenChange={disabled ? undefined : setOpen} open={open}>
|
|
85
|
+
<PopoverTrigger
|
|
86
|
+
render={
|
|
87
|
+
<Button
|
|
88
|
+
className={cn(
|
|
89
|
+
"w-full justify-start rounded-none font-normal data-[empty=true]:text-muted-foreground",
|
|
90
|
+
className
|
|
91
|
+
)}
|
|
92
|
+
data-empty={!value}
|
|
93
|
+
data-slot="date-picker-trigger"
|
|
94
|
+
disabled={disabled}
|
|
95
|
+
variant="outline"
|
|
96
|
+
/>
|
|
97
|
+
}
|
|
98
|
+
>
|
|
99
|
+
<CalendarIcon />
|
|
100
|
+
{value ? displayValue : placeholder}
|
|
101
|
+
</PopoverTrigger>
|
|
102
|
+
<PopoverContent
|
|
103
|
+
align="start"
|
|
104
|
+
className="w-auto rounded-none p-0 shadow-sm"
|
|
105
|
+
>
|
|
106
|
+
<Calendar
|
|
107
|
+
captionLayout="dropdown"
|
|
108
|
+
mode="single"
|
|
109
|
+
onSelect={handleDaySelect}
|
|
110
|
+
selected={value}
|
|
111
|
+
{...calendarProps}
|
|
112
|
+
className={cn(
|
|
113
|
+
"rounded-none border-0 shadow-none",
|
|
114
|
+
calendarProps?.className
|
|
115
|
+
)}
|
|
116
|
+
/>
|
|
117
|
+
{showTimePicker ? <div className="border-t border-border px-3 pb-3 pt-2">
|
|
118
|
+
<label className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
|
119
|
+
Time
|
|
120
|
+
<input
|
|
121
|
+
className="mt-1.5 h-8 w-full rounded-none border border-input bg-transparent px-2 text-sm font-sans outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
|
122
|
+
onChange={handleTimeChange}
|
|
123
|
+
type="time"
|
|
124
|
+
value={timeValue}
|
|
125
|
+
/>
|
|
126
|
+
</label>
|
|
127
|
+
</div> : null}
|
|
128
|
+
</PopoverContent>
|
|
129
|
+
</Popover>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { DatePicker }
|