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.
@@ -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
+ }