@talelk/newui-organisms 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/organisms/AppSidebar/AppSidebar.test.tsx +36 -0
- package/src/organisms/AppSidebar/AppSidebar.tsx +57 -12
- package/src/organisms/AuthCard/AuthCard.tsx +41 -32
- package/src/organisms/CommandPalette/CommandPalette.tsx +13 -2
- package/src/organisms/DataState/DataState.test.tsx +45 -0
- package/src/organisms/DataState/DataState.tsx +111 -0
- package/src/organisms/DataState/index.ts +6 -0
- package/src/organisms/DataTable/DataTable.test.tsx +73 -1
- package/src/organisms/DataTable/DataTable.tsx +426 -148
- package/src/organisms/DetailHeader/DetailHeader.tsx +9 -1
- package/src/organisms/FilterBar/FilterBar.test.tsx +2 -1
- package/src/organisms/FilterBar/FilterBar.tsx +24 -5
- package/src/organisms/KanbanBoard/KanbanBoard.tsx +220 -165
- package/src/organisms/MobileNav/MobileNav.tsx +1 -1
- package/src/organisms/PageToolbar/PageToolbar.tsx +63 -14
- package/src/organisms/Section/Section.test.tsx +41 -0
- package/src/organisms/Section/Section.tsx +108 -0
- package/src/organisms/Section/index.ts +1 -0
- package/src/organisms/SelectionBar/SelectionBar.test.tsx +35 -0
- package/src/organisms/SelectionBar/SelectionBar.tsx +84 -0
- package/src/organisms/SelectionBar/index.ts +5 -0
- package/src/organisms/SettingsDialog/SettingsDialog.tsx +13 -4
- package/src/organisms/Sidebar/Sidebar.tsx +17 -10
- package/src/organisms/StatGrid/StatGrid.test.tsx +13 -1
- package/src/organisms/StatGrid/StatGrid.tsx +98 -71
- package/src/organisms/VirtualList/VirtualList.tsx +56 -22
- package/src/organisms/index.ts +3 -0
- package/src/organisms/Sidebar/__screenshots__/Sidebar.browser.test.tsx/Sidebar--browser--real-CSS--reserves-its-declared-width-1.png +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Button, Checkbox, TextInput } from '@talelk/newui-atoms'
|
|
1
|
+
import { Button, Checkbox, cn, TextInput } from '@talelk/newui-atoms'
|
|
2
2
|
import {
|
|
3
3
|
Pagination,
|
|
4
4
|
ScrollTable,
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
Show,
|
|
32
32
|
untrack,
|
|
33
33
|
} from 'solid-js'
|
|
34
|
+
import { DataState } from '../DataState'
|
|
35
|
+
import { SelectionBar } from '../SelectionBar'
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* Built on the first table, not when the module is evaluated.
|
|
@@ -62,6 +64,20 @@ type Features = ReturnType<typeof createFeatures>
|
|
|
62
64
|
|
|
63
65
|
export type DataTableColumn<T extends object> = ColumnDef<Features, T>
|
|
64
66
|
|
|
67
|
+
/** Where a column appears in the mobile card, when `meta.mobile` is set. */
|
|
68
|
+
export type DataTableMobileRole =
|
|
69
|
+
| 'title'
|
|
70
|
+
| 'subtitle'
|
|
71
|
+
| 'amount'
|
|
72
|
+
| 'status'
|
|
73
|
+
| 'meta'
|
|
74
|
+
|
|
75
|
+
/** The library reads `meta.align` and `meta.mobile`; both are optional. */
|
|
76
|
+
export interface DataTableColumnMeta {
|
|
77
|
+
align?: 'end'
|
|
78
|
+
mobile?: DataTableMobileRole
|
|
79
|
+
}
|
|
80
|
+
|
|
65
81
|
export interface DataTableProps<T extends { id: string }> {
|
|
66
82
|
title: string
|
|
67
83
|
rows: T[]
|
|
@@ -72,6 +88,21 @@ export interface DataTableProps<T extends { id: string }> {
|
|
|
72
88
|
hideSearch?: boolean
|
|
73
89
|
/** Selection checkboxes + bulk action. Default on; off for read tables. */
|
|
74
90
|
selectable?: boolean
|
|
91
|
+
/** Per-row eligibility; ineligible rows cannot be selected. */
|
|
92
|
+
rowSelectable?: (row: T) => boolean
|
|
93
|
+
/** Page-supplied context shown beside Search, e.g. "18 expenses · Sep 2026". */
|
|
94
|
+
caption?: JSX.Element
|
|
95
|
+
/** Skeleton state while the rows are in flight. */
|
|
96
|
+
loading?: boolean
|
|
97
|
+
/** Error state; the table is replaced by the shared DataState. */
|
|
98
|
+
error?: JSX.Element
|
|
99
|
+
onRetry?: () => void
|
|
100
|
+
/** `compact` uses 36px rows instead of 44px. */
|
|
101
|
+
density?: 'comfortable' | 'compact'
|
|
102
|
+
/** Width at which the table becomes cards. Default `sm` (640px). */
|
|
103
|
+
cardBreakpoint?: 'sm' | 'md' | 'lg'
|
|
104
|
+
/** Total or scope shown beside the count in the mobile selection bar. */
|
|
105
|
+
selectionSummary?: (ids: string[]) => JSX.Element
|
|
75
106
|
pageSize?: number
|
|
76
107
|
/** Footer noun: "32 transactions". Defaults to "records". */
|
|
77
108
|
recordsLabel?: string
|
|
@@ -91,7 +122,10 @@ export interface DataTableProps<T extends { id: string }> {
|
|
|
91
122
|
export function DataTable<T extends { id: string }>(
|
|
92
123
|
props: DataTableProps<T>,
|
|
93
124
|
): JSX.Element {
|
|
94
|
-
|
|
125
|
+
// Selection exists to feed the bulk action. A read table with no action
|
|
126
|
+
// would render a checkbox that can never do anything.
|
|
127
|
+
const selectable = () =>
|
|
128
|
+
props.selectable !== false && props.onRowsAction !== undefined
|
|
95
129
|
const selection: DataTableColumn<T> = {
|
|
96
130
|
id: 'select',
|
|
97
131
|
enableHiding: false,
|
|
@@ -110,6 +144,7 @@ export function DataTable<T extends { id: string }>(
|
|
|
110
144
|
<Checkbox
|
|
111
145
|
aria-label={`Select ${context.row.original.id}`}
|
|
112
146
|
checked={context.row.getIsSelected()}
|
|
147
|
+
disabled={!context.row.getCanSelect()}
|
|
113
148
|
onCheckedChange={(value) => context.row.toggleSelected(value)}
|
|
114
149
|
/>
|
|
115
150
|
),
|
|
@@ -126,6 +161,10 @@ export function DataTable<T extends { id: string }>(
|
|
|
126
161
|
return resolvedColumns()
|
|
127
162
|
},
|
|
128
163
|
getRowId: (row) => row.id,
|
|
164
|
+
get enableRowSelection() {
|
|
165
|
+
return (row: { original: T }) =>
|
|
166
|
+
props.rowSelectable?.(row.original) ?? true
|
|
167
|
+
},
|
|
129
168
|
initialState: {
|
|
130
169
|
// Initial only: later pageSize changes do not repaginate the table.
|
|
131
170
|
pagination: {
|
|
@@ -144,16 +183,111 @@ export function DataTable<T extends { id: string }>(
|
|
|
144
183
|
),
|
|
145
184
|
)
|
|
146
185
|
const alignEnd = (meta: unknown): boolean =>
|
|
147
|
-
(meta as
|
|
186
|
+
(meta as DataTableColumnMeta | undefined)?.align === 'end'
|
|
187
|
+
const mobileRole = (meta: unknown) =>
|
|
188
|
+
(meta as DataTableColumnMeta | undefined)?.mobile
|
|
189
|
+
const hasMobileCards = createMemo(() =>
|
|
190
|
+
props.columns.some((column) => mobileRole(column.meta)),
|
|
191
|
+
)
|
|
148
192
|
const selected = () =>
|
|
149
193
|
table.getFilteredSelectedRowModel().rows.map((row) => row.original.id)
|
|
194
|
+
const status = () =>
|
|
195
|
+
props.loading
|
|
196
|
+
? ('loading' as const)
|
|
197
|
+
: props.error
|
|
198
|
+
? ('error' as const)
|
|
199
|
+
: ('ready' as const)
|
|
200
|
+
const runAction = () => {
|
|
201
|
+
props.onRowsAction?.(selected())
|
|
202
|
+
table.resetRowSelection()
|
|
203
|
+
}
|
|
204
|
+
const cellPad = () =>
|
|
205
|
+
props.density === 'compact' ? 'px-4 py-2' : 'px-4 py-3'
|
|
206
|
+
const cardHiddenClass = () =>
|
|
207
|
+
props.cardBreakpoint === 'lg'
|
|
208
|
+
? 'lg:hidden'
|
|
209
|
+
: props.cardBreakpoint === 'md'
|
|
210
|
+
? 'md:hidden'
|
|
211
|
+
: 'sm:hidden'
|
|
212
|
+
const tableHiddenClass = () =>
|
|
213
|
+
props.cardBreakpoint === 'lg'
|
|
214
|
+
? 'hidden lg:block'
|
|
215
|
+
: props.cardBreakpoint === 'md'
|
|
216
|
+
? 'hidden md:block'
|
|
217
|
+
: 'hidden sm:block'
|
|
218
|
+
const emptyAction = () => {
|
|
219
|
+
// Do not offer to reset a filter this table does not own. With `hideSearch`
|
|
220
|
+
// the page's FilterBar is the only reset path, so it must provide one.
|
|
221
|
+
if (props.hideSearch && !props.onClearFilters) return undefined
|
|
222
|
+
return (
|
|
223
|
+
<Button
|
|
224
|
+
onClick={() => {
|
|
225
|
+
if (props.onClearFilters) props.onClearFilters()
|
|
226
|
+
else table.resetGlobalFilter()
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
{props.onClearFilters ? 'Reset filters' : 'Clear search'}
|
|
230
|
+
</Button>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The table's own controls. The toolbar is stable at every width: when an
|
|
235
|
+
* external FilterBar owns search and there is no caption, the table title
|
|
236
|
+
* fills the space, so a selection never inserts a row or moves the menu.
|
|
237
|
+
*/
|
|
238
|
+
const pageCount = () =>
|
|
239
|
+
Math.max(
|
|
240
|
+
1,
|
|
241
|
+
Math.ceil(
|
|
242
|
+
table.getFilteredRowModel().rows.length /
|
|
243
|
+
table.atoms.pagination.get().pageSize,
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
const columnLabel = (column: {
|
|
247
|
+
id: string
|
|
248
|
+
columnDef: { header?: unknown }
|
|
249
|
+
}) =>
|
|
250
|
+
typeof column.columnDef.header === 'string'
|
|
251
|
+
? column.columnDef.header
|
|
252
|
+
: column.id
|
|
253
|
+
const columnMenu = (placement: 'up' | 'down') => (
|
|
254
|
+
<details
|
|
255
|
+
data-slot="data-table-columns"
|
|
256
|
+
class="relative max-sm:hidden"
|
|
257
|
+
open={false}
|
|
258
|
+
>
|
|
259
|
+
<summary class="list-none cursor-pointer rounded-3 border border-outline-gray-2 px-3 py-1.5 text-interface focus-visible:focus-ring [&::-webkit-details-marker]:hidden">
|
|
260
|
+
Columns
|
|
261
|
+
</summary>
|
|
262
|
+
<div
|
|
263
|
+
class={`absolute end-0 z-floating flex min-w-44 flex-col gap-2 rounded-6 border border-outline-gray-1 bg-surface-elevation-2 p-3 shadow-md ${placement === 'up' ? 'bottom-full mb-2' : 'mt-2'}`}
|
|
264
|
+
>
|
|
265
|
+
<For
|
|
266
|
+
each={table
|
|
267
|
+
.getAllLeafColumns()
|
|
268
|
+
.filter((column) => column.getCanHide())}
|
|
269
|
+
>
|
|
270
|
+
{(column) => (
|
|
271
|
+
<Checkbox
|
|
272
|
+
label={columnLabel(column)}
|
|
273
|
+
checked={column.getIsVisible()}
|
|
274
|
+
onCheckedChange={(value) => column.toggleVisibility(value)}
|
|
275
|
+
/>
|
|
276
|
+
)}
|
|
277
|
+
</For>
|
|
278
|
+
</div>
|
|
279
|
+
</details>
|
|
280
|
+
)
|
|
150
281
|
return (
|
|
151
282
|
<section
|
|
152
283
|
class="min-w-0 rounded-card border border-outline-gray-1 bg-surface-elevation-1"
|
|
153
284
|
aria-label={props.title}
|
|
154
285
|
data-slot="data-table"
|
|
155
286
|
>
|
|
156
|
-
<div
|
|
287
|
+
<div
|
|
288
|
+
data-slot="data-table-toolbar"
|
|
289
|
+
class="flex flex-wrap items-center justify-between gap-control-gap p-card-pad-compact"
|
|
290
|
+
>
|
|
157
291
|
<Show when={!props.hideSearch}>
|
|
158
292
|
<div class="w-full sm:w-72">
|
|
159
293
|
<TextInput
|
|
@@ -168,166 +302,310 @@ export function DataTable<T extends { id: string }>(
|
|
|
168
302
|
/>
|
|
169
303
|
</div>
|
|
170
304
|
</Show>
|
|
305
|
+
<Show when={props.caption}>
|
|
306
|
+
<p class="min-w-0 text-caption text-ink-gray-7">{props.caption}</p>
|
|
307
|
+
</Show>
|
|
308
|
+
<Show when={props.hideSearch && !props.caption}>
|
|
309
|
+
<span class="min-w-0 text-interface font-medium text-ink-gray-8">
|
|
310
|
+
{props.title}
|
|
311
|
+
</span>
|
|
312
|
+
</Show>
|
|
171
313
|
<div class="ms-auto flex flex-wrap items-center gap-2">
|
|
172
314
|
<Show when={props.onRowsAction && selected().length}>
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
table.resetRowSelection()
|
|
178
|
-
}}
|
|
179
|
-
>
|
|
315
|
+
<span class="text-caption text-ink-gray-7 tabular-nums">
|
|
316
|
+
{props.selectionSummary?.(selected())}
|
|
317
|
+
</span>
|
|
318
|
+
<Button variant="outline" class="max-sm:hidden" onClick={runAction}>
|
|
180
319
|
{props.actionLabel ?? 'Update selected'} ({selected().length})
|
|
181
320
|
</Button>
|
|
182
321
|
</Show>
|
|
183
|
-
|
|
184
|
-
<summary class="cursor-pointer rounded-3 border border-outline-gray-2 px-3 py-1.5 text-interface focus-visible:focus-ring">
|
|
185
|
-
Columns
|
|
186
|
-
</summary>
|
|
187
|
-
<div class="absolute end-0 z-floating mt-2 flex min-w-44 flex-col gap-2 rounded-6 border border-outline-gray-1 bg-surface-elevation-2 p-3 shadow-md">
|
|
188
|
-
<For
|
|
189
|
-
each={table
|
|
190
|
-
.getAllLeafColumns()
|
|
191
|
-
.filter((column) => column.getCanHide())}
|
|
192
|
-
>
|
|
193
|
-
{(column) => (
|
|
194
|
-
<Checkbox
|
|
195
|
-
label={column.id}
|
|
196
|
-
checked={column.getIsVisible()}
|
|
197
|
-
onCheckedChange={(value) => column.toggleVisibility(value)}
|
|
198
|
-
/>
|
|
199
|
-
)}
|
|
200
|
-
</For>
|
|
201
|
-
</div>
|
|
202
|
-
</details>
|
|
322
|
+
{columnMenu('down')}
|
|
203
323
|
</div>
|
|
204
324
|
</div>
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
325
|
+
<Show
|
|
326
|
+
when={status() === 'ready'}
|
|
327
|
+
fallback={
|
|
328
|
+
<DataState
|
|
329
|
+
status={status()}
|
|
330
|
+
error={props.error}
|
|
331
|
+
onRetry={props.onRetry}
|
|
332
|
+
rows={6}
|
|
333
|
+
/>
|
|
334
|
+
}
|
|
335
|
+
>
|
|
336
|
+
<Show when={hasMobileCards() && table.getRowModel().rows.length > 0}>
|
|
337
|
+
<div
|
|
338
|
+
data-slot="data-table-cards"
|
|
339
|
+
class={`flex flex-col gap-2 p-card-pad-compact ${cardHiddenClass()}`}
|
|
340
|
+
>
|
|
341
|
+
<Show when={selectable()}>
|
|
342
|
+
<div class="flex items-center justify-between gap-2 px-1">
|
|
343
|
+
<Checkbox
|
|
344
|
+
aria-label={`Select all ${props.title} on this page`}
|
|
345
|
+
padded
|
|
346
|
+
class="size-10"
|
|
347
|
+
checked={table.getIsAllPageRowsSelected()}
|
|
348
|
+
indeterminate={table.getIsSomePageRowsSelected()}
|
|
349
|
+
onCheckedChange={(value) =>
|
|
350
|
+
table.toggleAllPageRowsSelected(value)
|
|
351
|
+
}
|
|
352
|
+
/>
|
|
353
|
+
<span class="text-caption text-ink-gray-7">
|
|
354
|
+
{selected().length} selected
|
|
355
|
+
</span>
|
|
356
|
+
</div>
|
|
357
|
+
</Show>
|
|
358
|
+
<For each={table.getRowModel().rows}>
|
|
359
|
+
{(row) => {
|
|
360
|
+
const cells = (role: DataTableMobileRole) =>
|
|
361
|
+
row
|
|
362
|
+
.getVisibleCells()
|
|
363
|
+
.filter(
|
|
364
|
+
(cell) => mobileRole(cell.column.columnDef.meta) === role,
|
|
365
|
+
)
|
|
366
|
+
return (
|
|
367
|
+
<div
|
|
368
|
+
class="rounded-4 border border-outline-gray-1 p-3"
|
|
369
|
+
classList={{ 'bg-surface-blue-1': row.getIsSelected() }}
|
|
370
|
+
data-selected={row.getIsSelected() ? '' : undefined}
|
|
371
|
+
>
|
|
372
|
+
<div class="flex items-start gap-2">
|
|
373
|
+
<Show when={selectable()}>
|
|
374
|
+
<Checkbox
|
|
375
|
+
aria-label={`Select ${row.original.id}`}
|
|
376
|
+
padded
|
|
377
|
+
class="size-10"
|
|
378
|
+
checked={row.getIsSelected()}
|
|
379
|
+
disabled={!row.getCanSelect()}
|
|
380
|
+
onCheckedChange={(value) => row.toggleSelected(value)}
|
|
381
|
+
/>
|
|
382
|
+
</Show>
|
|
383
|
+
<div class="min-w-0 flex-1">
|
|
384
|
+
<div class="flex items-start justify-between gap-3">
|
|
385
|
+
<div class="min-w-0">
|
|
386
|
+
<For each={cells('title')}>
|
|
387
|
+
{(cell) => (
|
|
388
|
+
<div class="text-interface font-medium [overflow-wrap:anywhere]">
|
|
389
|
+
{flexRender(
|
|
390
|
+
cell.column.columnDef.cell,
|
|
391
|
+
cell.getContext(),
|
|
392
|
+
)}
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
</For>
|
|
396
|
+
<For each={cells('subtitle')}>
|
|
397
|
+
{(cell) => (
|
|
398
|
+
<div class="text-caption text-ink-gray-7 [overflow-wrap:anywhere]">
|
|
399
|
+
{flexRender(
|
|
400
|
+
cell.column.columnDef.cell,
|
|
401
|
+
cell.getContext(),
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
244
404
|
)}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
405
|
+
</For>
|
|
406
|
+
</div>
|
|
407
|
+
<For each={cells('amount')}>
|
|
408
|
+
{(cell) => (
|
|
409
|
+
<span class="shrink-0 text-interface font-medium tabular-nums">
|
|
410
|
+
{flexRender(
|
|
411
|
+
cell.column.columnDef.cell,
|
|
412
|
+
cell.getContext(),
|
|
413
|
+
)}
|
|
250
414
|
</span>
|
|
251
|
-
|
|
252
|
-
</
|
|
253
|
-
</
|
|
254
|
-
|
|
415
|
+
)}
|
|
416
|
+
</For>
|
|
417
|
+
</div>
|
|
418
|
+
<div class="mt-1 flex flex-wrap items-center gap-2">
|
|
419
|
+
<For each={cells('status')}>
|
|
420
|
+
{(cell) =>
|
|
421
|
+
flexRender(
|
|
422
|
+
cell.column.columnDef.cell,
|
|
423
|
+
cell.getContext(),
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
</For>
|
|
427
|
+
<For each={cells('meta')}>
|
|
428
|
+
{(cell) => (
|
|
429
|
+
<span class="text-caption text-ink-gray-7">
|
|
430
|
+
{flexRender(
|
|
431
|
+
cell.column.columnDef.cell,
|
|
432
|
+
cell.getContext(),
|
|
433
|
+
)}
|
|
434
|
+
</span>
|
|
435
|
+
)}
|
|
436
|
+
</For>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
)
|
|
442
|
+
}}
|
|
443
|
+
</For>
|
|
444
|
+
</div>
|
|
445
|
+
</Show>
|
|
446
|
+
<Show when={table.getFilteredRowModel().rows.length > 0}>
|
|
447
|
+
<div class={hasMobileCards() ? tableHiddenClass() : ''}>
|
|
448
|
+
<ScrollTable label={`${props.title} table`} stickyHeader>
|
|
449
|
+
<table class="w-full min-w-[720px] border-collapse text-start">
|
|
450
|
+
<ScrollTableHead>
|
|
451
|
+
<For each={table.getHeaderGroups()}>
|
|
452
|
+
{(group) => (
|
|
453
|
+
<tr>
|
|
454
|
+
<For each={group.headers}>
|
|
455
|
+
{(header) => (
|
|
456
|
+
<th
|
|
457
|
+
scope="col"
|
|
458
|
+
class={
|
|
459
|
+
alignEnd(header.column.columnDef.meta)
|
|
460
|
+
? `${cellPad()} text-end`
|
|
461
|
+
: `${cellPad()} text-start`
|
|
462
|
+
}
|
|
463
|
+
aria-sort={
|
|
464
|
+
header.column.getIsSorted() === 'asc'
|
|
465
|
+
? 'ascending'
|
|
466
|
+
: header.column.getIsSorted() === 'desc'
|
|
467
|
+
? 'descending'
|
|
468
|
+
: undefined
|
|
469
|
+
}
|
|
470
|
+
>
|
|
471
|
+
<Show when={!header.isPlaceholder}>
|
|
472
|
+
<Show
|
|
473
|
+
when={header.column.getCanSort()}
|
|
474
|
+
fallback={flexRender(
|
|
475
|
+
header.column.columnDef.header,
|
|
476
|
+
header.getContext(),
|
|
477
|
+
)}
|
|
478
|
+
>
|
|
479
|
+
<button
|
|
480
|
+
type="button"
|
|
481
|
+
class="inline-flex items-center gap-2 rounded-3 focus-visible:focus-ring"
|
|
482
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
483
|
+
>
|
|
484
|
+
{flexRender(
|
|
485
|
+
header.column.columnDef.header,
|
|
486
|
+
header.getContext(),
|
|
487
|
+
)}
|
|
488
|
+
<span aria-hidden="true">
|
|
489
|
+
<span
|
|
490
|
+
aria-hidden="true"
|
|
491
|
+
class={cn(
|
|
492
|
+
'inline-block size-4 align-middle',
|
|
493
|
+
header.column.getIsSorted() === 'asc'
|
|
494
|
+
? 'lucide-arrow-up'
|
|
495
|
+
: header.column.getIsSorted() ===
|
|
496
|
+
'desc'
|
|
497
|
+
? 'lucide-arrow-down'
|
|
498
|
+
: 'lucide-arrow-up-down',
|
|
499
|
+
)}
|
|
500
|
+
/>
|
|
501
|
+
</span>
|
|
502
|
+
</button>
|
|
503
|
+
</Show>
|
|
504
|
+
</Show>
|
|
505
|
+
</th>
|
|
506
|
+
)}
|
|
507
|
+
</For>
|
|
508
|
+
</tr>
|
|
255
509
|
)}
|
|
256
510
|
</For>
|
|
257
|
-
</
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
<tr
|
|
265
|
-
class="border-b border-outline-gray-1 hover:bg-surface-gray-1"
|
|
266
|
-
data-selected={row.getIsSelected() ? '' : undefined}
|
|
267
|
-
>
|
|
268
|
-
<For each={row.getVisibleCells()}>
|
|
269
|
-
{(cell) => (
|
|
270
|
-
<td
|
|
271
|
-
class={
|
|
272
|
-
alignEnd(cell.column.columnDef.meta)
|
|
273
|
-
? 'px-4 py-3 text-end text-interface'
|
|
274
|
-
: 'px-4 py-3 text-interface'
|
|
275
|
-
}
|
|
511
|
+
</ScrollTableHead>
|
|
512
|
+
<tbody>
|
|
513
|
+
<For each={table.getRowModel().rows}>
|
|
514
|
+
{(row) => (
|
|
515
|
+
<tr
|
|
516
|
+
class="border-b border-outline-gray-1 hover:bg-surface-gray-1 data-[selected]:bg-surface-blue-1 data-[selected]:hover:bg-surface-blue-2"
|
|
517
|
+
data-selected={row.getIsSelected() ? '' : undefined}
|
|
276
518
|
>
|
|
277
|
-
{
|
|
278
|
-
cell
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
519
|
+
<For each={row.getVisibleCells()}>
|
|
520
|
+
{(cell) => (
|
|
521
|
+
<td
|
|
522
|
+
class={
|
|
523
|
+
alignEnd(cell.column.columnDef.meta)
|
|
524
|
+
? `${cellPad()} text-end text-interface`
|
|
525
|
+
: `${cellPad()} text-interface`
|
|
526
|
+
}
|
|
527
|
+
>
|
|
528
|
+
{flexRender(
|
|
529
|
+
cell.column.columnDef.cell,
|
|
530
|
+
cell.getContext(),
|
|
531
|
+
)}
|
|
532
|
+
</td>
|
|
533
|
+
)}
|
|
534
|
+
</For>
|
|
535
|
+
</tr>
|
|
282
536
|
)}
|
|
283
537
|
</For>
|
|
284
|
-
</
|
|
285
|
-
|
|
286
|
-
</
|
|
287
|
-
</
|
|
288
|
-
</
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
{
|
|
294
|
-
|
|
295
|
-
<p class="mt-1 text-ink-gray-7">
|
|
296
|
-
{props.emptyHint ?? 'Try a different search or clear your filters.'}
|
|
297
|
-
</p>
|
|
298
|
-
<Show
|
|
299
|
-
when={props.onClearFilters}
|
|
300
|
-
fallback={
|
|
301
|
-
<Button class="mt-3" onClick={() => table.resetGlobalFilter()}>
|
|
302
|
-
Clear search
|
|
303
|
-
</Button>
|
|
538
|
+
</tbody>
|
|
539
|
+
</table>
|
|
540
|
+
</ScrollTable>
|
|
541
|
+
</div>
|
|
542
|
+
</Show>
|
|
543
|
+
<Show when={table.getFilteredRowModel().rows.length === 0}>
|
|
544
|
+
<DataState
|
|
545
|
+
status="empty"
|
|
546
|
+
title={props.emptyTitle ?? 'No matching records'}
|
|
547
|
+
description={
|
|
548
|
+
props.emptyHint ?? 'Try a different search or clear your filters.'
|
|
304
549
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
550
|
+
action={emptyAction()}
|
|
551
|
+
/>
|
|
552
|
+
</Show>
|
|
553
|
+
<Show when={table.getFilteredRowModel().rows.length > 0}>
|
|
554
|
+
<div class="flex flex-wrap items-center justify-between gap-control-gap border-t border-outline-gray-1 p-card-pad-compact">
|
|
555
|
+
<span class="text-caption text-ink-gray-7" role="status">
|
|
556
|
+
<Show when={selectable()}>{selected().length} selected · </Show>
|
|
557
|
+
{table.getFilteredRowModel().rows.length}{' '}
|
|
558
|
+
{props.recordsLabel ?? 'records'}
|
|
559
|
+
</span>
|
|
560
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
561
|
+
<Pagination.Root
|
|
562
|
+
class="max-sm:flex-nowrap"
|
|
563
|
+
page={table.atoms.pagination.get().pageIndex + 1}
|
|
564
|
+
pageSize={table.atoms.pagination.get().pageSize}
|
|
565
|
+
count={table.getFilteredRowModel().rows.length}
|
|
566
|
+
onPageChange={(page) => table.setPageIndex(page - 1)}
|
|
567
|
+
>
|
|
568
|
+
<span class="contents max-sm:hidden">
|
|
569
|
+
<Pagination.Range />
|
|
570
|
+
</span>
|
|
571
|
+
<Show when={pageCount() > 1}>
|
|
572
|
+
<span class="text-caption text-ink-gray-7 tabular-nums sm:hidden">
|
|
573
|
+
Page {table.atoms.pagination.get().pageIndex + 1} of{' '}
|
|
574
|
+
{pageCount()}
|
|
575
|
+
</span>
|
|
576
|
+
<Pagination.PrevTrigger />
|
|
577
|
+
<span class="contents max-sm:hidden">
|
|
578
|
+
<Pagination.Pages />
|
|
579
|
+
</span>
|
|
580
|
+
<Pagination.NextTrigger />
|
|
581
|
+
</Show>
|
|
582
|
+
</Pagination.Root>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
</Show>
|
|
313
586
|
</Show>
|
|
314
|
-
<
|
|
315
|
-
<
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
587
|
+
<Show when={selectable() && selected().length > 0}>
|
|
588
|
+
<div class="h-16 sm:hidden" aria-hidden="true" />
|
|
589
|
+
</Show>
|
|
590
|
+
<div class="sm:hidden">
|
|
591
|
+
<SelectionBar
|
|
592
|
+
count={selected().length}
|
|
593
|
+
placement="sticky"
|
|
594
|
+
summary={
|
|
595
|
+
props.selectionSummary
|
|
596
|
+
? props.selectionSummary(selected())
|
|
597
|
+
: undefined
|
|
598
|
+
}
|
|
599
|
+
primary={
|
|
600
|
+
props.onRowsAction
|
|
601
|
+
? {
|
|
602
|
+
label: props.actionLabel ?? 'Update selected',
|
|
603
|
+
onClick: runAction,
|
|
604
|
+
}
|
|
605
|
+
: undefined
|
|
606
|
+
}
|
|
607
|
+
onClear={() => table.resetRowSelection()}
|
|
608
|
+
/>
|
|
331
609
|
</div>
|
|
332
610
|
</section>
|
|
333
611
|
)
|