@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.
Files changed (29) hide show
  1. package/package.json +3 -3
  2. package/src/organisms/AppSidebar/AppSidebar.test.tsx +36 -0
  3. package/src/organisms/AppSidebar/AppSidebar.tsx +57 -12
  4. package/src/organisms/AuthCard/AuthCard.tsx +41 -32
  5. package/src/organisms/CommandPalette/CommandPalette.tsx +13 -2
  6. package/src/organisms/DataState/DataState.test.tsx +45 -0
  7. package/src/organisms/DataState/DataState.tsx +111 -0
  8. package/src/organisms/DataState/index.ts +6 -0
  9. package/src/organisms/DataTable/DataTable.test.tsx +73 -1
  10. package/src/organisms/DataTable/DataTable.tsx +426 -148
  11. package/src/organisms/DetailHeader/DetailHeader.tsx +9 -1
  12. package/src/organisms/FilterBar/FilterBar.test.tsx +2 -1
  13. package/src/organisms/FilterBar/FilterBar.tsx +24 -5
  14. package/src/organisms/KanbanBoard/KanbanBoard.tsx +220 -165
  15. package/src/organisms/MobileNav/MobileNav.tsx +1 -1
  16. package/src/organisms/PageToolbar/PageToolbar.tsx +63 -14
  17. package/src/organisms/Section/Section.test.tsx +41 -0
  18. package/src/organisms/Section/Section.tsx +108 -0
  19. package/src/organisms/Section/index.ts +1 -0
  20. package/src/organisms/SelectionBar/SelectionBar.test.tsx +35 -0
  21. package/src/organisms/SelectionBar/SelectionBar.tsx +84 -0
  22. package/src/organisms/SelectionBar/index.ts +5 -0
  23. package/src/organisms/SettingsDialog/SettingsDialog.tsx +13 -4
  24. package/src/organisms/Sidebar/Sidebar.tsx +17 -10
  25. package/src/organisms/StatGrid/StatGrid.test.tsx +13 -1
  26. package/src/organisms/StatGrid/StatGrid.tsx +98 -71
  27. package/src/organisms/VirtualList/VirtualList.tsx +56 -22
  28. package/src/organisms/index.ts +3 -0
  29. 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
- const selectable = () => props.selectable !== false
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 { align?: string } | undefined)?.align === 'end'
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 class="flex flex-wrap items-center justify-between gap-control-gap p-card-pad-compact">
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
- <Button
174
- variant="outline"
175
- onClick={() => {
176
- props.onRowsAction?.(selected())
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
- <details class="relative">
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
- <ScrollTable label={`${props.title} table`}>
206
- <table class="w-full min-w-[720px] border-collapse text-start">
207
- <ScrollTableHead>
208
- <For each={table.getHeaderGroups()}>
209
- {(group) => (
210
- <tr>
211
- <For each={group.headers}>
212
- {(header) => (
213
- <th
214
- scope="col"
215
- class={
216
- alignEnd(header.column.columnDef.meta)
217
- ? 'px-4 py-3 text-end'
218
- : 'px-4 py-3 text-start'
219
- }
220
- aria-sort={
221
- header.column.getIsSorted() === 'asc'
222
- ? 'ascending'
223
- : header.column.getIsSorted() === 'desc'
224
- ? 'descending'
225
- : undefined
226
- }
227
- >
228
- <Show when={!header.isPlaceholder}>
229
- <Show
230
- when={header.column.getCanSort()}
231
- fallback={flexRender(
232
- header.column.columnDef.header,
233
- header.getContext(),
234
- )}
235
- >
236
- <button
237
- type="button"
238
- class="inline-flex items-center gap-2 rounded-3 focus-visible:focus-ring"
239
- onClick={header.column.getToggleSortingHandler()}
240
- >
241
- {flexRender(
242
- header.column.columnDef.header,
243
- header.getContext(),
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
- <span aria-hidden="true">
246
- <span
247
- aria-hidden="true"
248
- class="lucide-arrow-up-down inline-block size-4 align-middle"
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
- </button>
252
- </Show>
253
- </Show>
254
- </th>
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
- </tr>
258
- )}
259
- </For>
260
- </ScrollTableHead>
261
- <tbody>
262
- <For each={table.getRowModel().rows}>
263
- {(row) => (
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
- {flexRender(
278
- cell.column.columnDef.cell,
279
- cell.getContext(),
280
- )}
281
- </td>
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
- </tr>
285
- )}
286
- </For>
287
- </tbody>
288
- </table>
289
- </ScrollTable>
290
- <Show when={table.getFilteredRowModel().rows.length === 0}>
291
- <div class="p-10 text-center">
292
- <h3 class="font-semibold">
293
- {props.emptyTitle ?? 'No matching records'}
294
- </h3>
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
- {(clear) => (
307
- <Button class="mt-3" onClick={() => clear()()}>
308
- Reset filters
309
- </Button>
310
- )}
311
- </Show>
312
- </div>
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
- <div class="flex flex-wrap items-center justify-between gap-control-gap p-card-pad-compact">
315
- <span class="text-caption text-ink-gray-7" role="status">
316
- <Show when={selectable()}>{selected().length} selected · </Show>
317
- {table.getFilteredRowModel().rows.length}{' '}
318
- {props.recordsLabel ?? 'records'}
319
- </span>
320
- <Pagination.Root
321
- page={table.atoms.pagination.get().pageIndex + 1}
322
- pageSize={table.atoms.pagination.get().pageSize}
323
- count={table.getFilteredRowModel().rows.length}
324
- onPageChange={(page) => table.setPageIndex(page - 1)}
325
- >
326
- <Pagination.Range />
327
- <Pagination.PrevTrigger />
328
- <Pagination.Pages />
329
- <Pagination.NextTrigger />
330
- </Pagination.Root>
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
  )