@manusiakemos/laravel-tanstack-react 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +284 -33
- package/dist/index.cjs +638 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +364 -4
- package/dist/index.d.ts +364 -4
- package/dist/index.js +623 -3
- package/dist/index.js.map +1 -1
- package/package.json +7 -1
- package/src/components/DataTable.tsx +166 -0
- package/src/components/DataTableFilter.tsx +217 -0
- package/src/components/DataTablePagination.tsx +271 -0
- package/src/components/DataTableSearch.tsx +173 -0
- package/src/components/layouts/DataTableSplitLayout.tsx +153 -0
- package/src/components/ui/button.tsx +49 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/select.tsx +27 -0
- package/src/components/ui/table.tsx +120 -0
- package/src/hooks/useDataTable.ts +1 -1
- package/src/index.ts +60 -2
- package/src/lib/cn.ts +10 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { Table } from '@tanstack/react-table'
|
|
2
|
+
import {
|
|
3
|
+
ChevronLeft,
|
|
4
|
+
ChevronRight,
|
|
5
|
+
ChevronsLeft,
|
|
6
|
+
ChevronsRight,
|
|
7
|
+
} from 'lucide-react'
|
|
8
|
+
import type { ButtonHTMLAttributes, ReactNode, SelectHTMLAttributes } from 'react'
|
|
9
|
+
|
|
10
|
+
import { cn } from '../lib/cn'
|
|
11
|
+
import type { DataTableMeta } from '../types'
|
|
12
|
+
import { Button } from './ui/button'
|
|
13
|
+
import { Select } from './ui/select'
|
|
14
|
+
|
|
15
|
+
export interface DataTablePaginationLabels {
|
|
16
|
+
previous?: ReactNode
|
|
17
|
+
next?: ReactNode
|
|
18
|
+
first?: ReactNode
|
|
19
|
+
last?: ReactNode
|
|
20
|
+
page?: string
|
|
21
|
+
of?: string
|
|
22
|
+
rowsPerPage?: string
|
|
23
|
+
/** Renderer for the row-count summary. */
|
|
24
|
+
summary?: (info: {
|
|
25
|
+
pageIndex: number
|
|
26
|
+
pageCount: number
|
|
27
|
+
pageSize: number
|
|
28
|
+
filtered: number
|
|
29
|
+
total: number | null
|
|
30
|
+
}) => ReactNode
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DataTablePaginationClassNames {
|
|
34
|
+
root?: string
|
|
35
|
+
info?: string
|
|
36
|
+
controls?: string
|
|
37
|
+
button?: string
|
|
38
|
+
select?: string
|
|
39
|
+
pageSize?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DataTablePaginationRenderProps<TData> {
|
|
43
|
+
table: Table<TData>
|
|
44
|
+
meta: DataTableMeta | null
|
|
45
|
+
pageIndex: number
|
|
46
|
+
pageCount: number
|
|
47
|
+
pageSize: number
|
|
48
|
+
canPreviousPage: boolean
|
|
49
|
+
canNextPage: boolean
|
|
50
|
+
goToFirst: () => void
|
|
51
|
+
goToPrevious: () => void
|
|
52
|
+
goToNext: () => void
|
|
53
|
+
goToLast: () => void
|
|
54
|
+
setPageSize: (size: number) => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DataTablePaginationProps<TData> {
|
|
58
|
+
table: Table<TData>
|
|
59
|
+
/** Meta from `useDataTable`. Used for the row-count summary. */
|
|
60
|
+
meta?: DataTableMeta | null
|
|
61
|
+
/** Show the page-size selector. Defaults to false. */
|
|
62
|
+
showPageSize?: boolean
|
|
63
|
+
/** Available page size options when `showPageSize` is true. */
|
|
64
|
+
pageSizeOptions?: number[]
|
|
65
|
+
/** Show first/last page buttons. Defaults to false. */
|
|
66
|
+
showFirstLast?: boolean
|
|
67
|
+
/** Hide the "Page X of Y · N rows" summary. */
|
|
68
|
+
hideSummary?: boolean
|
|
69
|
+
/** Override individual labels / the summary renderer. */
|
|
70
|
+
labels?: DataTablePaginationLabels
|
|
71
|
+
/** Class for the root container. */
|
|
72
|
+
className?: string
|
|
73
|
+
/** Fine-grained class names for sub-elements. */
|
|
74
|
+
classNames?: DataTablePaginationClassNames
|
|
75
|
+
/** Extra props for the prev/next/first/last buttons. */
|
|
76
|
+
buttonProps?: Omit<
|
|
77
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
78
|
+
'onClick' | 'disabled' | 'className' | 'type'
|
|
79
|
+
>
|
|
80
|
+
/** Extra props for the page-size <select>. */
|
|
81
|
+
selectProps?: Omit<
|
|
82
|
+
SelectHTMLAttributes<HTMLSelectElement>,
|
|
83
|
+
'value' | 'onChange' | 'className'
|
|
84
|
+
>
|
|
85
|
+
/**
|
|
86
|
+
* Full render override. Receives the pagination API so any custom UI can
|
|
87
|
+
* still drive the table.
|
|
88
|
+
*/
|
|
89
|
+
render?: (props: DataTablePaginationRenderProps<TData>) => ReactNode
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const DEFAULT_PAGE_SIZES = [10, 25, 50, 100]
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Shadcn-styled pagination controls (prev/next, optional first/last, optional
|
|
96
|
+
* page-size selector, row-count summary) wired to a TanStack `Table`.
|
|
97
|
+
*/
|
|
98
|
+
export function DataTablePagination<TData>(props: DataTablePaginationProps<TData>) {
|
|
99
|
+
const {
|
|
100
|
+
table,
|
|
101
|
+
meta = null,
|
|
102
|
+
showPageSize = false,
|
|
103
|
+
pageSizeOptions = DEFAULT_PAGE_SIZES,
|
|
104
|
+
showFirstLast = false,
|
|
105
|
+
hideSummary = false,
|
|
106
|
+
labels,
|
|
107
|
+
className,
|
|
108
|
+
classNames,
|
|
109
|
+
buttonProps,
|
|
110
|
+
selectProps,
|
|
111
|
+
render,
|
|
112
|
+
} = props
|
|
113
|
+
|
|
114
|
+
const state = table.getState().pagination
|
|
115
|
+
const pageIndex = state.pageIndex
|
|
116
|
+
const pageSize = state.pageSize
|
|
117
|
+
const pageCount = table.getPageCount()
|
|
118
|
+
const canPreviousPage = table.getCanPreviousPage()
|
|
119
|
+
const canNextPage = table.getCanNextPage()
|
|
120
|
+
|
|
121
|
+
const goToFirst = () => table.setPageIndex(0)
|
|
122
|
+
const goToPrevious = () => table.previousPage()
|
|
123
|
+
const goToNext = () => table.nextPage()
|
|
124
|
+
const goToLast = () => table.setPageIndex(Math.max(0, pageCount - 1))
|
|
125
|
+
const setPageSize = (size: number) => table.setPageSize(size)
|
|
126
|
+
|
|
127
|
+
if (render) {
|
|
128
|
+
return (
|
|
129
|
+
<>
|
|
130
|
+
{render({
|
|
131
|
+
table,
|
|
132
|
+
meta,
|
|
133
|
+
pageIndex,
|
|
134
|
+
pageCount,
|
|
135
|
+
pageSize,
|
|
136
|
+
canPreviousPage,
|
|
137
|
+
canNextPage,
|
|
138
|
+
goToFirst,
|
|
139
|
+
goToPrevious,
|
|
140
|
+
goToNext,
|
|
141
|
+
goToLast,
|
|
142
|
+
setPageSize,
|
|
143
|
+
})}
|
|
144
|
+
</>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const filtered = meta?.filtered ?? 0
|
|
149
|
+
const total = meta?.total ?? null
|
|
150
|
+
|
|
151
|
+
const summaryNode = labels?.summary
|
|
152
|
+
? labels.summary({ pageIndex, pageCount, pageSize, filtered, total })
|
|
153
|
+
: (
|
|
154
|
+
<>
|
|
155
|
+
{labels?.page ?? 'Page'} {pageIndex + 1} {labels?.of ?? 'of'}{' '}
|
|
156
|
+
{Math.max(1, pageCount)}
|
|
157
|
+
{meta ? ` · ${filtered} rows` : null}
|
|
158
|
+
</>
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
className={cn(
|
|
164
|
+
'flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between',
|
|
165
|
+
className,
|
|
166
|
+
classNames?.root,
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
169
|
+
{!hideSummary && (
|
|
170
|
+
<div
|
|
171
|
+
className={cn(
|
|
172
|
+
'text-sm text-muted-foreground',
|
|
173
|
+
classNames?.info,
|
|
174
|
+
)}
|
|
175
|
+
>
|
|
176
|
+
{summaryNode}
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
<div
|
|
181
|
+
className={cn(
|
|
182
|
+
'flex items-center gap-2',
|
|
183
|
+
classNames?.controls,
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
{showPageSize && (
|
|
187
|
+
<label
|
|
188
|
+
className={cn(
|
|
189
|
+
'flex items-center gap-2 text-sm text-muted-foreground',
|
|
190
|
+
classNames?.pageSize,
|
|
191
|
+
)}
|
|
192
|
+
>
|
|
193
|
+
<span>{labels?.rowsPerPage ?? 'Rows per page:'}</span>
|
|
194
|
+
<Select
|
|
195
|
+
value={pageSize}
|
|
196
|
+
onChange={(e) => setPageSize(Number(e.target.value))}
|
|
197
|
+
className={cn('h-9 w-[5rem]', classNames?.select)}
|
|
198
|
+
{...selectProps}
|
|
199
|
+
>
|
|
200
|
+
{pageSizeOptions.map((size) => (
|
|
201
|
+
<option key={size} value={size}>
|
|
202
|
+
{size}
|
|
203
|
+
</option>
|
|
204
|
+
))}
|
|
205
|
+
</Select>
|
|
206
|
+
</label>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{showFirstLast && (
|
|
210
|
+
<Button
|
|
211
|
+
variant="outline"
|
|
212
|
+
size="icon"
|
|
213
|
+
onClick={goToFirst}
|
|
214
|
+
disabled={!canPreviousPage}
|
|
215
|
+
className={classNames?.button}
|
|
216
|
+
aria-label="Go to first page"
|
|
217
|
+
{...buttonProps}
|
|
218
|
+
>
|
|
219
|
+
{labels?.first ?? <ChevronsLeft className="h-4 w-4" />}
|
|
220
|
+
</Button>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
<Button
|
|
224
|
+
variant="outline"
|
|
225
|
+
onClick={goToPrevious}
|
|
226
|
+
disabled={!canPreviousPage}
|
|
227
|
+
className={classNames?.button}
|
|
228
|
+
aria-label="Go to previous page"
|
|
229
|
+
{...buttonProps}
|
|
230
|
+
>
|
|
231
|
+
{labels?.previous ?? (
|
|
232
|
+
<>
|
|
233
|
+
<ChevronLeft className="h-4 w-4" />
|
|
234
|
+
<span>Previous</span>
|
|
235
|
+
</>
|
|
236
|
+
)}
|
|
237
|
+
</Button>
|
|
238
|
+
|
|
239
|
+
<Button
|
|
240
|
+
variant="outline"
|
|
241
|
+
onClick={goToNext}
|
|
242
|
+
disabled={!canNextPage}
|
|
243
|
+
className={classNames?.button}
|
|
244
|
+
aria-label="Go to next page"
|
|
245
|
+
{...buttonProps}
|
|
246
|
+
>
|
|
247
|
+
{labels?.next ?? (
|
|
248
|
+
<>
|
|
249
|
+
<span>Next</span>
|
|
250
|
+
<ChevronRight className="h-4 w-4" />
|
|
251
|
+
</>
|
|
252
|
+
)}
|
|
253
|
+
</Button>
|
|
254
|
+
|
|
255
|
+
{showFirstLast && (
|
|
256
|
+
<Button
|
|
257
|
+
variant="outline"
|
|
258
|
+
size="icon"
|
|
259
|
+
onClick={goToLast}
|
|
260
|
+
disabled={!canNextPage}
|
|
261
|
+
className={classNames?.button}
|
|
262
|
+
aria-label="Go to last page"
|
|
263
|
+
{...buttonProps}
|
|
264
|
+
>
|
|
265
|
+
{labels?.last ?? <ChevronsRight className="h-4 w-4" />}
|
|
266
|
+
</Button>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Search as SearchIcon } from 'lucide-react'
|
|
2
|
+
import type { Table } from '@tanstack/react-table'
|
|
3
|
+
import {
|
|
4
|
+
type FormEvent,
|
|
5
|
+
type InputHTMLAttributes,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
useEffect,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
10
|
+
|
|
11
|
+
import { cn } from '../lib/cn'
|
|
12
|
+
import { useDebouncedValue } from '../utils/useDebouncedValue'
|
|
13
|
+
import { Button } from './ui/button'
|
|
14
|
+
import { Input } from './ui/input'
|
|
15
|
+
|
|
16
|
+
export interface DataTableSearchRenderProps {
|
|
17
|
+
/** Current uncommitted input value (pre-debounce / pre-submit). */
|
|
18
|
+
value: string
|
|
19
|
+
/** Update the input value (debounced commit if `debounce` is on). */
|
|
20
|
+
setValue: (next: string) => void
|
|
21
|
+
/** Immediately commit the current value to the table's global filter. */
|
|
22
|
+
submit: () => void
|
|
23
|
+
/** The value currently applied to the table's global filter. */
|
|
24
|
+
appliedValue: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type DataTableSearchDebounce = boolean | number
|
|
28
|
+
|
|
29
|
+
export interface DataTableSearchProps<TData> {
|
|
30
|
+
table: Table<TData>
|
|
31
|
+
/**
|
|
32
|
+
* Debounce behavior:
|
|
33
|
+
* - `true` → debounced with default delay (300ms)
|
|
34
|
+
* - number → debounced with the given delay in ms
|
|
35
|
+
* - `false` → no debounce; instead the component renders a Search button
|
|
36
|
+
* and only commits to the table on submit (button click / Enter)
|
|
37
|
+
*
|
|
38
|
+
* Defaults to `true`.
|
|
39
|
+
*/
|
|
40
|
+
debounce?: DataTableSearchDebounce
|
|
41
|
+
/** Placeholder text for the default input. */
|
|
42
|
+
placeholder?: string
|
|
43
|
+
/** Label for the search submit button (when `debounce` is `false`). */
|
|
44
|
+
submitLabel?: ReactNode
|
|
45
|
+
/** Class for the root wrapper element (ignored when `render` is provided). */
|
|
46
|
+
className?: string
|
|
47
|
+
/** Class merged into the underlying shadcn Input. */
|
|
48
|
+
inputClassName?: string
|
|
49
|
+
/** Class merged into the submit Button (when `debounce` is `false`). */
|
|
50
|
+
buttonClassName?: string
|
|
51
|
+
/** Extra props spread onto the underlying Input. */
|
|
52
|
+
inputProps?: Omit<
|
|
53
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
54
|
+
'value' | 'onChange' | 'type' | 'className'
|
|
55
|
+
>
|
|
56
|
+
/** Fired whenever the search value is committed to the table. */
|
|
57
|
+
onSearch?: (value: string) => void
|
|
58
|
+
/**
|
|
59
|
+
* Full render override. Receives the controlled value, setter, and a manual
|
|
60
|
+
* `submit()` so you can drop in any custom UI.
|
|
61
|
+
*/
|
|
62
|
+
render?: (props: DataTableSearchRenderProps) => ReactNode
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DEFAULT_DEBOUNCE_MS = 300
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Debounced (or manual-submit) global search wired to a TanStack `Table`.
|
|
69
|
+
*
|
|
70
|
+
* @example Debounced (default)
|
|
71
|
+
* <DataTableSearch table={table} placeholder="Search users..." />
|
|
72
|
+
*
|
|
73
|
+
* @example Manual submit (search button)
|
|
74
|
+
* <DataTableSearch table={table} debounce={false} placeholder="Search..." />
|
|
75
|
+
*
|
|
76
|
+
* @example Custom UI
|
|
77
|
+
* <DataTableSearch
|
|
78
|
+
* table={table}
|
|
79
|
+
* render={({ value, setValue, submit }) => (
|
|
80
|
+
* <MyCombobox value={value} onChange={setValue} onSubmit={submit} />
|
|
81
|
+
* )}
|
|
82
|
+
* />
|
|
83
|
+
*/
|
|
84
|
+
export function DataTableSearch<TData>(props: DataTableSearchProps<TData>) {
|
|
85
|
+
const {
|
|
86
|
+
table,
|
|
87
|
+
debounce = true,
|
|
88
|
+
placeholder = 'Search...',
|
|
89
|
+
submitLabel,
|
|
90
|
+
className,
|
|
91
|
+
inputClassName,
|
|
92
|
+
buttonClassName,
|
|
93
|
+
inputProps,
|
|
94
|
+
onSearch,
|
|
95
|
+
render,
|
|
96
|
+
} = props
|
|
97
|
+
|
|
98
|
+
const isDebounced = debounce !== false
|
|
99
|
+
const debounceMs =
|
|
100
|
+
typeof debounce === 'number' ? debounce : DEFAULT_DEBOUNCE_MS
|
|
101
|
+
|
|
102
|
+
const appliedValue = (table.getState().globalFilter as string | undefined) ?? ''
|
|
103
|
+
const [value, setValue] = useState(appliedValue)
|
|
104
|
+
const debounced = useDebouncedValue(value, debounceMs)
|
|
105
|
+
|
|
106
|
+
const commit = (next: string) => {
|
|
107
|
+
if (next === appliedValue) return
|
|
108
|
+
table.setGlobalFilter(next)
|
|
109
|
+
onSearch?.(next)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Debounced auto-commit. Disabled entirely when `debounce={false}`.
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!isDebounced) return
|
|
115
|
+
commit(debounced)
|
|
116
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
117
|
+
}, [debounced, isDebounced, table])
|
|
118
|
+
|
|
119
|
+
// Keep local state in sync if the table's filter is changed elsewhere.
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
setValue(appliedValue)
|
|
122
|
+
}, [appliedValue])
|
|
123
|
+
|
|
124
|
+
const submit = () => commit(value)
|
|
125
|
+
|
|
126
|
+
if (render) {
|
|
127
|
+
return <>{render({ value, setValue, submit, appliedValue })}</>
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const handleFormSubmit = (e: FormEvent<HTMLFormElement>) => {
|
|
131
|
+
e.preventDefault()
|
|
132
|
+
submit()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Debounced mode → just an input, no form / button needed.
|
|
136
|
+
if (isDebounced) {
|
|
137
|
+
return (
|
|
138
|
+
<Input
|
|
139
|
+
type="search"
|
|
140
|
+
value={value}
|
|
141
|
+
onChange={(e) => setValue(e.target.value)}
|
|
142
|
+
placeholder={placeholder}
|
|
143
|
+
className={cn(className, inputClassName)}
|
|
144
|
+
{...inputProps}
|
|
145
|
+
/>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Manual mode → input + submit button, wrapped in a form so Enter submits.
|
|
150
|
+
return (
|
|
151
|
+
<form
|
|
152
|
+
onSubmit={handleFormSubmit}
|
|
153
|
+
className={cn('flex w-full max-w-md items-center gap-2', className)}
|
|
154
|
+
>
|
|
155
|
+
<Input
|
|
156
|
+
type="search"
|
|
157
|
+
value={value}
|
|
158
|
+
onChange={(e) => setValue(e.target.value)}
|
|
159
|
+
placeholder={placeholder}
|
|
160
|
+
className={inputClassName}
|
|
161
|
+
{...inputProps}
|
|
162
|
+
/>
|
|
163
|
+
<Button type="submit" className={buttonClassName}>
|
|
164
|
+
{submitLabel ?? (
|
|
165
|
+
<>
|
|
166
|
+
<SearchIcon className="h-4 w-4" aria-hidden="true" />
|
|
167
|
+
<span>Search</span>
|
|
168
|
+
</>
|
|
169
|
+
)}
|
|
170
|
+
</Button>
|
|
171
|
+
</form>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Table as TanStackTable } from '@tanstack/react-table'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../lib/cn'
|
|
5
|
+
import type { DataTableMeta } from '../../types'
|
|
6
|
+
import { DataTable, type DataTableProps } from '../DataTable'
|
|
7
|
+
import {
|
|
8
|
+
DataTablePagination,
|
|
9
|
+
type DataTablePaginationProps,
|
|
10
|
+
} from '../DataTablePagination'
|
|
11
|
+
import {
|
|
12
|
+
DataTableSearch,
|
|
13
|
+
type DataTableSearchProps,
|
|
14
|
+
} from '../DataTableSearch'
|
|
15
|
+
|
|
16
|
+
export interface DataTableSplitLayoutClassNames {
|
|
17
|
+
root?: string
|
|
18
|
+
toolbar?: string
|
|
19
|
+
toolbarLeft?: string
|
|
20
|
+
toolbarRight?: string
|
|
21
|
+
body?: string
|
|
22
|
+
footer?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DataTableSplitLayoutProps<TData> {
|
|
26
|
+
table: TanStackTable<TData>
|
|
27
|
+
meta?: DataTableMeta | null
|
|
28
|
+
loading?: boolean
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Right-side toolbar slot — typically one or more `<DataTableFilter />`s.
|
|
32
|
+
* Rendered to the right of the search input on the same row.
|
|
33
|
+
*/
|
|
34
|
+
filters?: ReactNode
|
|
35
|
+
/**
|
|
36
|
+
* Optional far-right toolbar slot for page-level actions (e.g. an
|
|
37
|
+
* "+ Add" button). Appears after `filters`.
|
|
38
|
+
*/
|
|
39
|
+
actions?: ReactNode
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Override the entire search slot. By default, a `<DataTableSearch />`
|
|
43
|
+
* wired to `table` is rendered with sensible defaults.
|
|
44
|
+
*/
|
|
45
|
+
search?: ReactNode
|
|
46
|
+
|
|
47
|
+
/** Forwarded to the auto-rendered `<DataTableSearch />`. */
|
|
48
|
+
searchProps?: Omit<DataTableSearchProps<TData>, 'table'>
|
|
49
|
+
/** Forwarded to the auto-rendered `<DataTable />`. */
|
|
50
|
+
tableProps?: Omit<DataTableProps<TData>, 'table' | 'loading'>
|
|
51
|
+
/** Forwarded to the auto-rendered `<DataTablePagination />`. */
|
|
52
|
+
paginationProps?: Omit<DataTablePaginationProps<TData>, 'table' | 'meta'>
|
|
53
|
+
|
|
54
|
+
className?: string
|
|
55
|
+
classNames?: DataTableSplitLayoutClassNames
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Pre-built "split toolbar" layout: search on the left, filters/actions on
|
|
60
|
+
* the right; pagination info on the left, controls on the right.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* <DataTableSplitLayout
|
|
64
|
+
* table={table}
|
|
65
|
+
* meta={meta}
|
|
66
|
+
* loading={loading}
|
|
67
|
+
* searchProps={{ placeholder: 'Search users...' }}
|
|
68
|
+
* filters={
|
|
69
|
+
* <>
|
|
70
|
+
* <DataTableFilter table={table} columnId="status" options={statusOpts} />
|
|
71
|
+
* <DataTableFilter table={table} columnId="role" type="multiselect" options={roleOpts} />
|
|
72
|
+
* </>
|
|
73
|
+
* }
|
|
74
|
+
* actions={<Button>+ Add user</Button>}
|
|
75
|
+
* paginationProps={{ showPageSize: true, showFirstLast: true }}
|
|
76
|
+
* />
|
|
77
|
+
*/
|
|
78
|
+
export function DataTableSplitLayout<TData>(
|
|
79
|
+
props: DataTableSplitLayoutProps<TData>,
|
|
80
|
+
) {
|
|
81
|
+
const {
|
|
82
|
+
table,
|
|
83
|
+
meta = null,
|
|
84
|
+
loading = false,
|
|
85
|
+
filters,
|
|
86
|
+
actions,
|
|
87
|
+
search,
|
|
88
|
+
searchProps,
|
|
89
|
+
tableProps,
|
|
90
|
+
paginationProps,
|
|
91
|
+
className,
|
|
92
|
+
classNames,
|
|
93
|
+
} = props
|
|
94
|
+
|
|
95
|
+
const searchNode = search ?? (
|
|
96
|
+
<DataTableSearch
|
|
97
|
+
table={table}
|
|
98
|
+
placeholder="Search..."
|
|
99
|
+
className="w-full max-w-md"
|
|
100
|
+
{...searchProps}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
className={cn(
|
|
107
|
+
'flex flex-col gap-4',
|
|
108
|
+
className,
|
|
109
|
+
classNames?.root,
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
<div
|
|
113
|
+
className={cn(
|
|
114
|
+
'flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between',
|
|
115
|
+
classNames?.toolbar,
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
<div
|
|
119
|
+
className={cn(
|
|
120
|
+
'flex-1 min-w-0',
|
|
121
|
+
classNames?.toolbarLeft,
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{searchNode}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{(filters || actions) && (
|
|
128
|
+
<div
|
|
129
|
+
className={cn(
|
|
130
|
+
'flex flex-wrap items-center gap-2',
|
|
131
|
+
classNames?.toolbarRight,
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
{filters}
|
|
135
|
+
{actions}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div className={classNames?.body}>
|
|
141
|
+
<DataTable table={table} loading={loading} {...tableProps} />
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div className={classNames?.footer}>
|
|
145
|
+
<DataTablePagination
|
|
146
|
+
table={table}
|
|
147
|
+
meta={meta}
|
|
148
|
+
{...paginationProps}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
2
|
+
import { type ButtonHTMLAttributes, forwardRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../lib/cn'
|
|
5
|
+
|
|
6
|
+
export const buttonVariants = cva(
|
|
7
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
12
|
+
destructive:
|
|
13
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
14
|
+
outline:
|
|
15
|
+
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
16
|
+
secondary:
|
|
17
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
18
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
19
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
default: 'h-10 px-4 py-2',
|
|
23
|
+
sm: 'h-9 rounded-md px-3',
|
|
24
|
+
lg: 'h-11 rounded-md px-8',
|
|
25
|
+
icon: 'h-10 w-10',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: 'default',
|
|
30
|
+
size: 'default',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
export interface ButtonProps
|
|
36
|
+
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
|
37
|
+
VariantProps<typeof buttonVariants> {}
|
|
38
|
+
|
|
39
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
40
|
+
({ className, variant, size, type = 'button', ...props }, ref) => (
|
|
41
|
+
<button
|
|
42
|
+
ref={ref}
|
|
43
|
+
type={type}
|
|
44
|
+
className={cn(buttonVariants({ variant, size }), className)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
Button.displayName = 'Button'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { forwardRef, type InputHTMLAttributes } from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/cn'
|
|
4
|
+
|
|
5
|
+
export type InputProps = InputHTMLAttributes<HTMLInputElement>
|
|
6
|
+
|
|
7
|
+
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
8
|
+
({ className, type = 'text', ...props }, ref) => (
|
|
9
|
+
<input
|
|
10
|
+
ref={ref}
|
|
11
|
+
type={type}
|
|
12
|
+
className={cn(
|
|
13
|
+
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
Input.displayName = 'Input'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { forwardRef, type SelectHTMLAttributes } from 'react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/cn'
|
|
4
|
+
|
|
5
|
+
export type SelectProps = SelectHTMLAttributes<HTMLSelectElement>
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shadcn-styled native <select>. We use the native element (not Radix) to
|
|
9
|
+
* keep the dependency footprint minimal; consumers who want a Radix-based
|
|
10
|
+
* popover select can swap it in via the `render` prop on the parent
|
|
11
|
+
* component.
|
|
12
|
+
*/
|
|
13
|
+
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
|
14
|
+
({ className, multiple, ...props }, ref) => (
|
|
15
|
+
<select
|
|
16
|
+
ref={ref}
|
|
17
|
+
multiple={multiple}
|
|
18
|
+
className={cn(
|
|
19
|
+
'flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
20
|
+
multiple ? 'min-h-[6rem]' : 'h-10',
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
Select.displayName = 'Select'
|