@wealthx/shadcn 0.0.2 → 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.
Files changed (188) hide show
  1. package/.turbo/turbo-build.log +135 -11
  2. package/CHANGELOG.md +6 -0
  3. package/CHANGES.md +345 -0
  4. package/dist/chunk-2WZVSBAY.mjs +232 -0
  5. package/dist/chunk-2Y7YJKPE.mjs +47 -0
  6. package/dist/chunk-3U7SD3MS.mjs +55 -0
  7. package/dist/chunk-3VQNJ235.mjs +114 -0
  8. package/dist/chunk-55CEW76V.mjs +35 -0
  9. package/dist/chunk-6AFMNC42.mjs +146 -0
  10. package/dist/chunk-6OJF6XRN.mjs +117 -0
  11. package/dist/chunk-7LDIMXGM.mjs +181 -0
  12. package/dist/chunk-AMJ23O53.mjs +122 -0
  13. package/dist/chunk-BBJBJSXQ.mjs +44 -0
  14. package/dist/chunk-BGP2N52Z.mjs +126 -0
  15. package/dist/chunk-BMFN37JH.mjs +41 -0
  16. package/dist/chunk-CGOKTPXU.mjs +79 -0
  17. package/dist/chunk-CZ3BW5GL.mjs +81 -0
  18. package/dist/chunk-DBHJ5KC3.mjs +55 -0
  19. package/dist/chunk-DDPA2XXS.mjs +97 -0
  20. package/dist/chunk-DS2AMHN2.mjs +30 -0
  21. package/dist/chunk-E3K6O4FZ.mjs +57 -0
  22. package/dist/chunk-FWCSY2DS.mjs +37 -0
  23. package/dist/chunk-GPRJQ24C.mjs +28 -0
  24. package/dist/chunk-HS7TFG7V.mjs +24 -0
  25. package/dist/chunk-HUVTPUV2.mjs +256 -0
  26. package/dist/chunk-IAOOZCUY.mjs +90 -0
  27. package/dist/chunk-JF4PHPD5.mjs +111 -0
  28. package/dist/chunk-JU2RUWHF.mjs +123 -0
  29. package/dist/chunk-KKHTJNMM.mjs +86 -0
  30. package/dist/chunk-MJIEMGRD.mjs +266 -0
  31. package/dist/chunk-MKFL5MNH.mjs +372 -0
  32. package/dist/chunk-MQ72DIBH.mjs +105 -0
  33. package/dist/chunk-NGYG2EA6.mjs +148 -0
  34. package/dist/chunk-NWZ46DJL.mjs +213 -0
  35. package/dist/chunk-OXQQNQZI.mjs +75 -0
  36. package/dist/chunk-PMKODV6M.mjs +161 -0
  37. package/dist/chunk-QOJ2DQD6.mjs +57 -0
  38. package/dist/chunk-RL772EH7.mjs +126 -0
  39. package/dist/chunk-SLWCCURD.mjs +99 -0
  40. package/dist/chunk-V7CNWJT3.mjs +10 -0
  41. package/dist/chunk-VG6UF6UT.mjs +68 -0
  42. package/dist/chunk-VYMHBV6D.mjs +123 -0
  43. package/dist/chunk-VZ2NR7L3.mjs +195 -0
  44. package/dist/chunk-YN5SYTOO.mjs +117 -0
  45. package/dist/chunk-Z3MK2KKZ.mjs +83 -0
  46. package/dist/chunk-ZN2QKLF6.mjs +187 -0
  47. package/dist/chunk-ZZV5JVNW.mjs +34 -0
  48. package/dist/components/ui/accordion.js +142 -0
  49. package/dist/components/ui/accordion.mjs +14 -0
  50. package/dist/components/ui/alert-dialog.js +413 -0
  51. package/dist/components/ui/alert-dialog.mjs +34 -0
  52. package/dist/components/ui/alert.js +134 -0
  53. package/dist/components/ui/alert.mjs +12 -0
  54. package/dist/components/ui/avatar.js +173 -0
  55. package/dist/components/ui/avatar.mjs +18 -0
  56. package/dist/components/ui/badge.js +163 -0
  57. package/dist/components/ui/badge.mjs +11 -0
  58. package/dist/components/ui/button.js +198 -0
  59. package/dist/components/ui/button.mjs +11 -0
  60. package/dist/components/ui/calendar.js +408 -0
  61. package/dist/components/ui/calendar.mjs +12 -0
  62. package/dist/components/ui/card.js +156 -0
  63. package/dist/components/ui/card.mjs +20 -0
  64. package/dist/components/ui/checkbox.js +166 -0
  65. package/dist/components/ui/checkbox.mjs +11 -0
  66. package/dist/components/ui/chip.js +199 -0
  67. package/dist/components/ui/chip.mjs +10 -0
  68. package/dist/components/ui/data-table.js +925 -0
  69. package/dist/components/ui/data-table.mjs +29 -0
  70. package/dist/components/ui/date-picker.js +561 -0
  71. package/dist/components/ui/date-picker.mjs +15 -0
  72. package/dist/components/ui/dialog.js +378 -0
  73. package/dist/components/ui/dialog.mjs +30 -0
  74. package/dist/components/ui/drawer.js +213 -0
  75. package/dist/components/ui/drawer.mjs +28 -0
  76. package/dist/components/ui/dropdown-menu.js +338 -0
  77. package/dist/components/ui/dropdown-menu.mjs +38 -0
  78. package/dist/components/ui/empty.js +173 -0
  79. package/dist/components/ui/empty.mjs +18 -0
  80. package/dist/components/ui/field.js +359 -0
  81. package/dist/components/ui/field.mjs +28 -0
  82. package/dist/components/ui/input-group.js +406 -0
  83. package/dist/components/ui/input-group.mjs +22 -0
  84. package/dist/components/ui/input-otp.js +149 -0
  85. package/dist/components/ui/input-otp.mjs +14 -0
  86. package/dist/components/ui/input.js +81 -0
  87. package/dist/components/ui/input.mjs +8 -0
  88. package/dist/components/ui/label.js +85 -0
  89. package/dist/components/ui/label.mjs +8 -0
  90. package/dist/components/ui/pagination.js +333 -0
  91. package/dist/components/ui/pagination.mjs +22 -0
  92. package/dist/components/ui/popover.js +167 -0
  93. package/dist/components/ui/popover.mjs +22 -0
  94. package/dist/components/ui/progress.js +97 -0
  95. package/dist/components/ui/progress.mjs +8 -0
  96. package/dist/components/ui/radio-group.js +178 -0
  97. package/dist/components/ui/radio-group.mjs +12 -0
  98. package/dist/components/ui/select.js +262 -0
  99. package/dist/components/ui/select.mjs +28 -0
  100. package/dist/components/ui/separator.js +86 -0
  101. package/dist/components/ui/separator.mjs +8 -0
  102. package/dist/components/ui/sheet.js +227 -0
  103. package/dist/components/ui/sheet.mjs +26 -0
  104. package/dist/components/ui/skeleton.js +75 -0
  105. package/dist/components/ui/skeleton.mjs +8 -0
  106. package/dist/components/ui/sonner.js +86 -0
  107. package/dist/components/ui/sonner.mjs +7 -0
  108. package/dist/components/ui/spinner.js +93 -0
  109. package/dist/components/ui/spinner.mjs +10 -0
  110. package/dist/components/ui/switch.js +178 -0
  111. package/dist/components/ui/switch.mjs +11 -0
  112. package/dist/components/ui/table.js +184 -0
  113. package/dist/components/ui/table.mjs +22 -0
  114. package/dist/components/ui/tabs.js +181 -0
  115. package/dist/components/ui/tabs.mjs +16 -0
  116. package/dist/components/ui/textarea.js +79 -0
  117. package/dist/components/ui/textarea.mjs +8 -0
  118. package/dist/components/ui/toggle-group.js +184 -0
  119. package/dist/components/ui/toggle-group.mjs +12 -0
  120. package/dist/components/ui/toggle.js +108 -0
  121. package/dist/components/ui/toggle.mjs +11 -0
  122. package/dist/components/ui/tooltip.js +140 -0
  123. package/dist/components/ui/tooltip.mjs +16 -0
  124. package/dist/index.js +4312 -90
  125. package/dist/index.mjs +459 -158
  126. package/dist/lib/colors.js +84 -0
  127. package/dist/lib/colors.mjs +13 -0
  128. package/dist/lib/theme-provider.js +150 -0
  129. package/dist/lib/theme-provider.mjs +13 -0
  130. package/dist/lib/typography.js +157 -0
  131. package/dist/lib/typography.mjs +25 -0
  132. package/dist/lib/utils.js +34 -0
  133. package/dist/lib/utils.mjs +7 -0
  134. package/dist/styles.css +1 -1
  135. package/package.json +228 -11
  136. package/scripts/build-css.ts +15 -9
  137. package/src/components/index.tsx +443 -0
  138. package/src/components/ui/accordion.tsx +99 -0
  139. package/src/components/ui/alert-dialog.tsx +239 -0
  140. package/src/components/ui/alert.tsx +81 -0
  141. package/src/components/ui/avatar.tsx +130 -0
  142. package/src/components/ui/badge.tsx +57 -0
  143. package/src/components/ui/button.tsx +69 -37
  144. package/src/components/ui/calendar.tsx +252 -0
  145. package/src/components/ui/card.tsx +106 -0
  146. package/src/components/ui/checkbox.tsx +111 -0
  147. package/src/components/ui/chip.tsx +65 -0
  148. package/src/components/ui/data-table.tsx +490 -0
  149. package/src/components/ui/date-picker.tsx +133 -0
  150. package/src/components/ui/dialog.tsx +195 -0
  151. package/src/components/ui/drawer.tsx +169 -0
  152. package/src/components/ui/dropdown-menu.tsx +315 -0
  153. package/src/components/ui/empty.tsx +128 -0
  154. package/src/components/ui/field.tsx +273 -0
  155. package/src/components/ui/input-group.tsx +190 -0
  156. package/src/components/ui/input-otp.tsx +90 -0
  157. package/src/components/ui/input.tsx +28 -0
  158. package/src/components/ui/label.tsx +24 -0
  159. package/src/components/ui/pagination.tsx +148 -0
  160. package/src/components/ui/popover.tsx +112 -0
  161. package/src/components/ui/progress.tsx +40 -0
  162. package/src/components/ui/radio-group.tsx +129 -0
  163. package/src/components/ui/select.tsx +201 -0
  164. package/src/components/ui/separator.tsx +26 -0
  165. package/src/components/ui/sheet.tsx +182 -0
  166. package/src/components/ui/skeleton.tsx +22 -0
  167. package/src/components/ui/sonner.tsx +48 -0
  168. package/src/components/ui/spinner.tsx +41 -0
  169. package/src/components/ui/switch.tsx +126 -0
  170. package/src/components/ui/table.tsx +143 -0
  171. package/src/components/ui/tabs.tsx +119 -0
  172. package/src/components/ui/textarea.tsx +28 -0
  173. package/src/components/ui/toggle-group.tsx +94 -0
  174. package/src/components/ui/toggle.tsx +59 -0
  175. package/src/components/ui/tooltip.tsx +80 -0
  176. package/src/index.ts +15 -3
  177. package/src/lib/colors.ts +74 -0
  178. package/src/lib/slot.tsx +68 -0
  179. package/src/lib/theme-provider.tsx +134 -0
  180. package/src/lib/typography.ts +153 -0
  181. package/src/lib/utils.ts +1 -1
  182. package/src/styles/globals.css +377 -107
  183. package/src/styles/styles-css.ts +1 -1
  184. package/tsup.config.ts +48 -2
  185. package/dist/index.d.mts +0 -27
  186. package/dist/index.d.ts +0 -27
  187. package/src/provider/ShadcnProvider.tsx +0 -89
  188. 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 }