minka-ds 0.1.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/package.json +25 -0
- package/src/components/ui/badge.tsx +63 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button-group.tsx +89 -0
- package/src/components/ui/button.tsx +65 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/cell.tsx +104 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/combobox.tsx +310 -0
- package/src/components/ui/data-table.tsx +275 -0
- package/src/components/ui/dialog.tsx +160 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input-group.tsx +170 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/select.tsx +198 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +145 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/table.tsx +142 -0
- package/src/components/ui/tabs.tsx +109 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +28 -0
- package/src/lib/utils.ts +33 -0
- package/tokens/primitives.css +227 -0
- package/tokens/text-utilities.css +44 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
|
|
5
|
+
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils"
|
|
8
|
+
import { Button } from "./button"
|
|
9
|
+
import {
|
|
10
|
+
InputGroup,
|
|
11
|
+
InputGroupAddon,
|
|
12
|
+
InputGroupButton,
|
|
13
|
+
InputGroupInput,
|
|
14
|
+
} from "./input-group"
|
|
15
|
+
|
|
16
|
+
const Combobox = ComboboxPrimitive.Root
|
|
17
|
+
|
|
18
|
+
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
|
|
19
|
+
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ComboboxTrigger({
|
|
23
|
+
className,
|
|
24
|
+
children,
|
|
25
|
+
...props
|
|
26
|
+
}: ComboboxPrimitive.Trigger.Props) {
|
|
27
|
+
return (
|
|
28
|
+
<ComboboxPrimitive.Trigger
|
|
29
|
+
data-slot="combobox-trigger"
|
|
30
|
+
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
<ChevronDownIcon
|
|
35
|
+
data-slot="combobox-trigger-icon"
|
|
36
|
+
className="pointer-events-none size-4 text-[var(--color-text-muted)]"
|
|
37
|
+
/>
|
|
38
|
+
</ComboboxPrimitive.Trigger>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
|
43
|
+
return (
|
|
44
|
+
<ComboboxPrimitive.Clear
|
|
45
|
+
data-slot="combobox-clear"
|
|
46
|
+
render={<InputGroupButton variant="ghost" size="icon-xs" />}
|
|
47
|
+
className={cn(className)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
<XIcon className="pointer-events-none" />
|
|
51
|
+
</ComboboxPrimitive.Clear>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ComboboxInput({
|
|
56
|
+
className,
|
|
57
|
+
children,
|
|
58
|
+
disabled = false,
|
|
59
|
+
showTrigger = true,
|
|
60
|
+
showClear = false,
|
|
61
|
+
...props
|
|
62
|
+
}: ComboboxPrimitive.Input.Props & {
|
|
63
|
+
showTrigger?: boolean
|
|
64
|
+
showClear?: boolean
|
|
65
|
+
}) {
|
|
66
|
+
return (
|
|
67
|
+
<InputGroup className={cn("w-auto", className)}>
|
|
68
|
+
<ComboboxPrimitive.Input
|
|
69
|
+
render={<InputGroupInput disabled={disabled} />}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
<InputGroupAddon align="inline-end">
|
|
73
|
+
{showTrigger && (
|
|
74
|
+
<InputGroupButton
|
|
75
|
+
size="icon-xs"
|
|
76
|
+
variant="ghost"
|
|
77
|
+
asChild
|
|
78
|
+
data-slot="input-group-button"
|
|
79
|
+
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
>
|
|
82
|
+
<ComboboxTrigger />
|
|
83
|
+
</InputGroupButton>
|
|
84
|
+
)}
|
|
85
|
+
{showClear && <ComboboxClear disabled={disabled} />}
|
|
86
|
+
</InputGroupAddon>
|
|
87
|
+
{children}
|
|
88
|
+
</InputGroup>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ComboboxContent({
|
|
93
|
+
className,
|
|
94
|
+
side = "bottom",
|
|
95
|
+
sideOffset = 6,
|
|
96
|
+
align = "start",
|
|
97
|
+
alignOffset = 0,
|
|
98
|
+
anchor,
|
|
99
|
+
...props
|
|
100
|
+
}: ComboboxPrimitive.Popup.Props &
|
|
101
|
+
Pick<
|
|
102
|
+
ComboboxPrimitive.Positioner.Props,
|
|
103
|
+
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
|
|
104
|
+
>) {
|
|
105
|
+
return (
|
|
106
|
+
<ComboboxPrimitive.Portal>
|
|
107
|
+
<ComboboxPrimitive.Positioner
|
|
108
|
+
side={side}
|
|
109
|
+
sideOffset={sideOffset}
|
|
110
|
+
align={align}
|
|
111
|
+
alignOffset={alignOffset}
|
|
112
|
+
anchor={anchor}
|
|
113
|
+
className="isolate [z-index:var(--z-dropdown)]"
|
|
114
|
+
>
|
|
115
|
+
<ComboboxPrimitive.Popup
|
|
116
|
+
data-slot="combobox-content"
|
|
117
|
+
data-chips={!!anchor}
|
|
118
|
+
className={cn(
|
|
119
|
+
"group/combobox-content relative max-h-96 w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden [border-radius:var(--radius-popover)] bg-[var(--color-bg-overlay)] text-[var(--color-text-default)] shadow-[var(--shadow-popover)] ring-1 ring-[var(--color-border-subtle)] duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-[var(--color-border-default)]/30 *:data-[slot=input-group]:bg-[var(--color-bg-canvas)]/60 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
120
|
+
className
|
|
121
|
+
)}
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
</ComboboxPrimitive.Positioner>
|
|
125
|
+
</ComboboxPrimitive.Portal>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
|
|
130
|
+
return (
|
|
131
|
+
<ComboboxPrimitive.List
|
|
132
|
+
data-slot="combobox-list"
|
|
133
|
+
className={cn(
|
|
134
|
+
"max-h-[min(calc(--spacing(96)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto p-1 data-empty:p-0",
|
|
135
|
+
className
|
|
136
|
+
)}
|
|
137
|
+
{...props}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ComboboxItem({
|
|
143
|
+
className,
|
|
144
|
+
children,
|
|
145
|
+
...props
|
|
146
|
+
}: ComboboxPrimitive.Item.Props) {
|
|
147
|
+
return (
|
|
148
|
+
<ComboboxPrimitive.Item
|
|
149
|
+
data-slot="combobox-item"
|
|
150
|
+
className={cn(
|
|
151
|
+
"relative flex cursor-default items-center gap-2 [border-radius:var(--radius-tag)] mx-1 first:mt-1 last:mb-1 py-1.5 pr-8 pl-2 text-body-sm outline-hidden select-none data-highlighted:bg-[var(--color-action-ghost-hover)] data-highlighted:text-[var(--color-text-default)] data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
152
|
+
className
|
|
153
|
+
)}
|
|
154
|
+
{...props}
|
|
155
|
+
>
|
|
156
|
+
{children}
|
|
157
|
+
<ComboboxPrimitive.ItemIndicator
|
|
158
|
+
data-slot="combobox-item-indicator"
|
|
159
|
+
render={
|
|
160
|
+
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
|
161
|
+
}
|
|
162
|
+
>
|
|
163
|
+
<CheckIcon className="pointer-events-none size-4 pointer-coarse:size-5" />
|
|
164
|
+
</ComboboxPrimitive.ItemIndicator>
|
|
165
|
+
</ComboboxPrimitive.Item>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
|
|
170
|
+
return (
|
|
171
|
+
<ComboboxPrimitive.Group
|
|
172
|
+
data-slot="combobox-group"
|
|
173
|
+
className={cn(className)}
|
|
174
|
+
{...props}
|
|
175
|
+
/>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ComboboxLabel({
|
|
180
|
+
className,
|
|
181
|
+
...props
|
|
182
|
+
}: ComboboxPrimitive.GroupLabel.Props) {
|
|
183
|
+
return (
|
|
184
|
+
<ComboboxPrimitive.GroupLabel
|
|
185
|
+
data-slot="combobox-label"
|
|
186
|
+
className={cn(
|
|
187
|
+
"px-2 py-1.5 text-caption text-[var(--color-text-muted)] pointer-coarse:px-3 pointer-coarse:py-2 pointer-coarse:text-body-sm",
|
|
188
|
+
className
|
|
189
|
+
)}
|
|
190
|
+
{...props}
|
|
191
|
+
/>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
|
|
196
|
+
return (
|
|
197
|
+
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
|
202
|
+
return (
|
|
203
|
+
<ComboboxPrimitive.Empty
|
|
204
|
+
data-slot="combobox-empty"
|
|
205
|
+
className={cn(
|
|
206
|
+
"hidden w-full justify-center py-2 text-center text-body-sm text-[var(--color-text-muted)] group-data-empty/combobox-content:flex",
|
|
207
|
+
className
|
|
208
|
+
)}
|
|
209
|
+
{...props}
|
|
210
|
+
/>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function ComboboxSeparator({
|
|
215
|
+
className,
|
|
216
|
+
...props
|
|
217
|
+
}: ComboboxPrimitive.Separator.Props) {
|
|
218
|
+
return (
|
|
219
|
+
<ComboboxPrimitive.Separator
|
|
220
|
+
data-slot="combobox-separator"
|
|
221
|
+
className={cn("-mx-1 my-1 h-px bg-[var(--color-border-default)]", className)}
|
|
222
|
+
{...props}
|
|
223
|
+
/>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function ComboboxChips({
|
|
228
|
+
className,
|
|
229
|
+
...props
|
|
230
|
+
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
|
|
231
|
+
ComboboxPrimitive.Chips.Props) {
|
|
232
|
+
return (
|
|
233
|
+
<ComboboxPrimitive.Chips
|
|
234
|
+
data-slot="combobox-chips"
|
|
235
|
+
className={cn(
|
|
236
|
+
"flex min-h-9 flex-wrap items-center gap-1.5 [border-radius:var(--radius-input)] border border-[var(--color-border-default)] bg-[var(--color-bg-raised)] bg-clip-padding px-2.5 py-1.5 text-body-sm shadow-xs transition-[color,box-shadow] focus-within:border-[var(--color-border-focus)] focus-within:ring-[3px] focus-within:ring-[var(--color-border-focus)]/50 has-aria-invalid:border-[var(--color-border-error)] has-aria-invalid:ring-[3px] has-aria-invalid:ring-[var(--color-border-error)]/20 has-data-[slot=combobox-chip]:px-1.5",
|
|
237
|
+
className
|
|
238
|
+
)}
|
|
239
|
+
{...props}
|
|
240
|
+
/>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function ComboboxChip({
|
|
245
|
+
className,
|
|
246
|
+
children,
|
|
247
|
+
showRemove = true,
|
|
248
|
+
...props
|
|
249
|
+
}: ComboboxPrimitive.Chip.Props & {
|
|
250
|
+
showRemove?: boolean
|
|
251
|
+
}) {
|
|
252
|
+
return (
|
|
253
|
+
<ComboboxPrimitive.Chip
|
|
254
|
+
data-slot="combobox-chip"
|
|
255
|
+
className={cn(
|
|
256
|
+
"flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 [border-radius:var(--radius-button)] bg-[var(--color-bg-disabled)] px-1.5 text-caption font-medium whitespace-nowrap text-[var(--color-text-default)] has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
|
|
257
|
+
className
|
|
258
|
+
)}
|
|
259
|
+
{...props}
|
|
260
|
+
>
|
|
261
|
+
{children}
|
|
262
|
+
{showRemove && (
|
|
263
|
+
<ComboboxPrimitive.ChipRemove
|
|
264
|
+
render={<Button variant="ghost" size="icon-xs" />}
|
|
265
|
+
className="-ml-1 opacity-50 hover:opacity-100"
|
|
266
|
+
data-slot="combobox-chip-remove"
|
|
267
|
+
>
|
|
268
|
+
<XIcon className="pointer-events-none" />
|
|
269
|
+
</ComboboxPrimitive.ChipRemove>
|
|
270
|
+
)}
|
|
271
|
+
</ComboboxPrimitive.Chip>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function ComboboxChipsInput({
|
|
276
|
+
className,
|
|
277
|
+
children,
|
|
278
|
+
...props
|
|
279
|
+
}: ComboboxPrimitive.Input.Props) {
|
|
280
|
+
return (
|
|
281
|
+
<ComboboxPrimitive.Input
|
|
282
|
+
data-slot="combobox-chip-input"
|
|
283
|
+
className={cn("min-w-16 flex-1 outline-none", className)}
|
|
284
|
+
{...props}
|
|
285
|
+
/>
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function useComboboxAnchor() {
|
|
290
|
+
return React.useRef<HTMLDivElement | null>(null)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export {
|
|
294
|
+
Combobox,
|
|
295
|
+
ComboboxInput,
|
|
296
|
+
ComboboxContent,
|
|
297
|
+
ComboboxList,
|
|
298
|
+
ComboboxItem,
|
|
299
|
+
ComboboxGroup,
|
|
300
|
+
ComboboxLabel,
|
|
301
|
+
ComboboxCollection,
|
|
302
|
+
ComboboxEmpty,
|
|
303
|
+
ComboboxSeparator,
|
|
304
|
+
ComboboxChips,
|
|
305
|
+
ComboboxChip,
|
|
306
|
+
ComboboxChipsInput,
|
|
307
|
+
ComboboxTrigger,
|
|
308
|
+
ComboboxValue,
|
|
309
|
+
useComboboxAnchor,
|
|
310
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import {
|
|
5
|
+
ColumnDef,
|
|
6
|
+
SortingState,
|
|
7
|
+
VisibilityState,
|
|
8
|
+
flexRender,
|
|
9
|
+
getCoreRowModel,
|
|
10
|
+
getPaginationRowModel,
|
|
11
|
+
getSortedRowModel,
|
|
12
|
+
useReactTable,
|
|
13
|
+
type Table as TanstackTable,
|
|
14
|
+
} from "@tanstack/react-table"
|
|
15
|
+
import { ChevronsUpDown, ChevronUp, ChevronDown, Columns3Cog } from "lucide-react"
|
|
16
|
+
|
|
17
|
+
import { cn } from "../../lib/utils"
|
|
18
|
+
import { Button } from "./button"
|
|
19
|
+
import {
|
|
20
|
+
Pagination,
|
|
21
|
+
PaginationContent,
|
|
22
|
+
PaginationEllipsis,
|
|
23
|
+
PaginationItem,
|
|
24
|
+
PaginationLink,
|
|
25
|
+
PaginationNext,
|
|
26
|
+
PaginationPrevious,
|
|
27
|
+
} from "./pagination"
|
|
28
|
+
import {
|
|
29
|
+
DropdownMenu,
|
|
30
|
+
DropdownMenuCheckboxItem,
|
|
31
|
+
DropdownMenuContent,
|
|
32
|
+
DropdownMenuLabel,
|
|
33
|
+
DropdownMenuSeparator,
|
|
34
|
+
DropdownMenuTrigger,
|
|
35
|
+
} from "./dropdown-menu"
|
|
36
|
+
import {
|
|
37
|
+
Table,
|
|
38
|
+
TableBody,
|
|
39
|
+
TableCell,
|
|
40
|
+
TableHead,
|
|
41
|
+
TableHeader,
|
|
42
|
+
TableRow,
|
|
43
|
+
} from "./table"
|
|
44
|
+
|
|
45
|
+
// ── Column header with sort control ──────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
interface DataTableColumnHeaderProps<TData, TValue>
|
|
48
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
49
|
+
column: TanstackTable<TData>["getColumn"] extends (id: string) => infer C ? NonNullable<C> : never
|
|
50
|
+
title: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function DataTableColumnHeader<TData, TValue>({
|
|
54
|
+
column,
|
|
55
|
+
title,
|
|
56
|
+
className,
|
|
57
|
+
}: {
|
|
58
|
+
column: ReturnType<TanstackTable<TData>["getColumn"]>
|
|
59
|
+
title: string
|
|
60
|
+
className?: string
|
|
61
|
+
}) {
|
|
62
|
+
if (!column?.getCanSort()) {
|
|
63
|
+
return <span className={cn("text-label-sm", className)}>{title}</span>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<button
|
|
68
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
69
|
+
className={cn(
|
|
70
|
+
"inline-flex items-center gap-1 text-label-sm text-[var(--color-text-default)] hover:text-[var(--color-text-muted)] transition-colors",
|
|
71
|
+
className
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{title}
|
|
75
|
+
{column.getIsSorted() === "asc" ? (
|
|
76
|
+
<ChevronUp className="size-3.5" />
|
|
77
|
+
) : column.getIsSorted() === "desc" ? (
|
|
78
|
+
<ChevronDown className="size-3.5" />
|
|
79
|
+
) : (
|
|
80
|
+
<ChevronsUpDown className="size-3.5 text-[var(--color-text-disabled)]" />
|
|
81
|
+
)}
|
|
82
|
+
</button>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Column visibility toggle ──────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function DataTableColumnToggle<TData>({
|
|
89
|
+
table,
|
|
90
|
+
}: {
|
|
91
|
+
table: TanstackTable<TData>
|
|
92
|
+
}) {
|
|
93
|
+
return (
|
|
94
|
+
<DropdownMenu>
|
|
95
|
+
<DropdownMenuTrigger asChild>
|
|
96
|
+
<Button variant="outline" size="icon-sm" className="my-2">
|
|
97
|
+
<Columns3Cog />
|
|
98
|
+
</Button>
|
|
99
|
+
</DropdownMenuTrigger>
|
|
100
|
+
<DropdownMenuContent align="end">
|
|
101
|
+
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
|
102
|
+
<DropdownMenuSeparator />
|
|
103
|
+
{table
|
|
104
|
+
.getAllColumns()
|
|
105
|
+
.filter((col) => col.getCanHide())
|
|
106
|
+
.map((col) => (
|
|
107
|
+
<DropdownMenuCheckboxItem
|
|
108
|
+
key={col.id}
|
|
109
|
+
checked={col.getIsVisible()}
|
|
110
|
+
onCheckedChange={(val) => col.toggleVisibility(!!val)}
|
|
111
|
+
>
|
|
112
|
+
{col.id}
|
|
113
|
+
</DropdownMenuCheckboxItem>
|
|
114
|
+
))}
|
|
115
|
+
</DropdownMenuContent>
|
|
116
|
+
</DropdownMenu>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Pagination ────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
function getPageNumbers(currentPage: number, totalPages: number): (number | "ellipsis")[] {
|
|
123
|
+
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1)
|
|
124
|
+
const pages: (number | "ellipsis")[] = [1]
|
|
125
|
+
if (currentPage > 3) pages.push("ellipsis")
|
|
126
|
+
const start = Math.max(2, currentPage - 1)
|
|
127
|
+
const end = Math.min(totalPages - 1, currentPage + 1)
|
|
128
|
+
for (let i = start; i <= end; i++) pages.push(i)
|
|
129
|
+
if (currentPage < totalPages - 2) pages.push("ellipsis")
|
|
130
|
+
pages.push(totalPages)
|
|
131
|
+
return pages
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function DataTablePagination<TData>({
|
|
135
|
+
table,
|
|
136
|
+
}: {
|
|
137
|
+
table: TanstackTable<TData>
|
|
138
|
+
}) {
|
|
139
|
+
const currentPage = table.getState().pagination.pageIndex + 1
|
|
140
|
+
const totalPages = table.getPageCount()
|
|
141
|
+
const pages = getPageNumbers(currentPage, totalPages)
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className="flex items-center justify-between">
|
|
145
|
+
<p className="text-body-sm text-[var(--color-text-muted)]">
|
|
146
|
+
Page {currentPage} of {totalPages}
|
|
147
|
+
</p>
|
|
148
|
+
<Pagination className="mx-0 w-auto justify-end">
|
|
149
|
+
<PaginationContent>
|
|
150
|
+
<PaginationItem>
|
|
151
|
+
<PaginationPrevious
|
|
152
|
+
onClick={() => table.previousPage()}
|
|
153
|
+
aria-disabled={!table.getCanPreviousPage()}
|
|
154
|
+
className={cn(!table.getCanPreviousPage() && "pointer-events-none opacity-50")}
|
|
155
|
+
/>
|
|
156
|
+
</PaginationItem>
|
|
157
|
+
{pages.map((page, i) =>
|
|
158
|
+
page === "ellipsis" ? (
|
|
159
|
+
<PaginationItem key={`ellipsis-${i}`}>
|
|
160
|
+
<PaginationEllipsis />
|
|
161
|
+
</PaginationItem>
|
|
162
|
+
) : (
|
|
163
|
+
<PaginationItem key={page}>
|
|
164
|
+
<PaginationLink
|
|
165
|
+
isActive={page === currentPage}
|
|
166
|
+
onClick={() => table.setPageIndex(page - 1)}
|
|
167
|
+
className="cursor-pointer"
|
|
168
|
+
>
|
|
169
|
+
{page}
|
|
170
|
+
</PaginationLink>
|
|
171
|
+
</PaginationItem>
|
|
172
|
+
)
|
|
173
|
+
)}
|
|
174
|
+
<PaginationItem>
|
|
175
|
+
<PaginationNext
|
|
176
|
+
onClick={() => table.nextPage()}
|
|
177
|
+
aria-disabled={!table.getCanNextPage()}
|
|
178
|
+
className={cn(!table.getCanNextPage() && "pointer-events-none opacity-50")}
|
|
179
|
+
/>
|
|
180
|
+
</PaginationItem>
|
|
181
|
+
</PaginationContent>
|
|
182
|
+
</Pagination>
|
|
183
|
+
</div>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── DataTable ─────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
interface DataTableProps<TData, TValue> {
|
|
190
|
+
columns: ColumnDef<TData, TValue>[]
|
|
191
|
+
data: TData[]
|
|
192
|
+
pageSize?: number
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function DataTable<TData, TValue>({
|
|
196
|
+
columns,
|
|
197
|
+
data,
|
|
198
|
+
pageSize = 10,
|
|
199
|
+
}: DataTableProps<TData, TValue>) {
|
|
200
|
+
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
201
|
+
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
|
|
202
|
+
|
|
203
|
+
const table = useReactTable({
|
|
204
|
+
data,
|
|
205
|
+
columns,
|
|
206
|
+
getCoreRowModel: getCoreRowModel(),
|
|
207
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
208
|
+
getSortedRowModel: getSortedRowModel(),
|
|
209
|
+
onSortingChange: setSorting,
|
|
210
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
211
|
+
initialState: { pagination: { pageSize } },
|
|
212
|
+
state: { sorting, columnVisibility },
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div className="space-y-4">
|
|
217
|
+
<div className="rounded-[var(--radius-card)] border border-[var(--color-border-default)] bg-[var(--color-bg-raised)] overflow-hidden">
|
|
218
|
+
<Table className="[&_th:first-child]:pl-4 [&_td:first-child]:pl-4">
|
|
219
|
+
<TableHeader className="bg-[var(--color-bg-base)]">
|
|
220
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
221
|
+
<TableRow key={headerGroup.id}>
|
|
222
|
+
{headerGroup.headers.map((header, index) => (
|
|
223
|
+
<TableHead key={header.id}>
|
|
224
|
+
{index === headerGroup.headers.length - 1 ? (
|
|
225
|
+
<div className="flex items-center justify-between gap-2">
|
|
226
|
+
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
|
227
|
+
<DataTableColumnToggle table={table} />
|
|
228
|
+
</div>
|
|
229
|
+
) : (
|
|
230
|
+
header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())
|
|
231
|
+
)}
|
|
232
|
+
</TableHead>
|
|
233
|
+
))}
|
|
234
|
+
</TableRow>
|
|
235
|
+
))}
|
|
236
|
+
</TableHeader>
|
|
237
|
+
<TableBody>
|
|
238
|
+
{table.getRowModel().rows?.length ? (
|
|
239
|
+
table.getRowModel().rows.map((row) => (
|
|
240
|
+
<TableRow
|
|
241
|
+
key={row.id}
|
|
242
|
+
data-state={row.getIsSelected() ? "selected" : undefined}
|
|
243
|
+
>
|
|
244
|
+
{row.getVisibleCells().map((cell) => (
|
|
245
|
+
<TableCell key={cell.id}>
|
|
246
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
247
|
+
</TableCell>
|
|
248
|
+
))}
|
|
249
|
+
</TableRow>
|
|
250
|
+
))
|
|
251
|
+
) : (
|
|
252
|
+
<TableRow>
|
|
253
|
+
<TableCell
|
|
254
|
+
colSpan={columns.length}
|
|
255
|
+
className="h-24 text-center text-[var(--color-text-muted)]"
|
|
256
|
+
>
|
|
257
|
+
No results.
|
|
258
|
+
</TableCell>
|
|
259
|
+
</TableRow>
|
|
260
|
+
)}
|
|
261
|
+
</TableBody>
|
|
262
|
+
</Table>
|
|
263
|
+
</div>
|
|
264
|
+
<DataTablePagination table={table} />
|
|
265
|
+
</div>
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export {
|
|
271
|
+
DataTable,
|
|
272
|
+
DataTableColumnHeader,
|
|
273
|
+
DataTableColumnToggle,
|
|
274
|
+
DataTablePagination,
|
|
275
|
+
}
|