@marianmeres/stuic 3.66.1 → 3.68.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 (75) hide show
  1. package/dist/actions/autoscroll.d.ts +7 -0
  2. package/dist/actions/autoscroll.js +7 -0
  3. package/dist/actions/focus-trap.d.ts +7 -0
  4. package/dist/actions/focus-trap.js +8 -3
  5. package/dist/actions/typeahead.svelte.js +40 -4
  6. package/dist/components/Carousel/Carousel.svelte +9 -2
  7. package/dist/components/Carousel/README.md +8 -2
  8. package/dist/components/Cart/Cart.svelte +3 -0
  9. package/dist/components/Cart/README.md +18 -1
  10. package/dist/components/Checkout/CheckoutOrderReview.svelte +4 -14
  11. package/dist/components/Checkout/README.md +184 -0
  12. package/dist/components/Checkout/_internal/checkout-utils.d.ts +6 -0
  13. package/dist/components/Checkout/_internal/checkout-utils.js +24 -0
  14. package/dist/components/Checkout/index.d.ts +1 -1
  15. package/dist/components/Checkout/index.js +1 -1
  16. package/dist/components/CommandMenu/CommandMenu.svelte +23 -7
  17. package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +2 -0
  18. package/dist/components/CronInput/CronInput.svelte +44 -9
  19. package/dist/components/CronInput/CronInput.svelte.d.ts +2 -0
  20. package/dist/components/CronInput/README.md +145 -0
  21. package/dist/components/CronInput/cron-next-run.svelte.d.ts +11 -0
  22. package/dist/components/CronInput/cron-next-run.svelte.js +11 -0
  23. package/dist/components/CronInput/index.css +0 -8
  24. package/dist/components/DataTable/DataTable.svelte +276 -83
  25. package/dist/components/DataTable/DataTable.svelte.d.ts +58 -6
  26. package/dist/components/DataTable/README.md +155 -25
  27. package/dist/components/DataTable/index.css +31 -0
  28. package/dist/components/DropdownMenu/DropdownMenu.svelte +43 -26
  29. package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +5 -1
  30. package/dist/components/DropdownMenu/README.md +37 -9
  31. package/dist/components/Input/FieldAssets.svelte +9 -7
  32. package/dist/components/Input/FieldAssets.svelte.d.ts +3 -7
  33. package/dist/components/Input/FieldFile.svelte +13 -7
  34. package/dist/components/Input/FieldFile.svelte.d.ts +4 -7
  35. package/dist/components/Input/FieldInput.svelte +10 -8
  36. package/dist/components/Input/FieldInput.svelte.d.ts +3 -8
  37. package/dist/components/Input/FieldInputLocalized.svelte +8 -7
  38. package/dist/components/Input/FieldInputLocalized.svelte.d.ts +2 -7
  39. package/dist/components/Input/FieldKeyValues.svelte +8 -7
  40. package/dist/components/Input/FieldKeyValues.svelte.d.ts +2 -7
  41. package/dist/components/Input/FieldLikeButton.svelte +9 -7
  42. package/dist/components/Input/FieldLikeButton.svelte.d.ts +3 -7
  43. package/dist/components/Input/FieldObject.svelte +8 -7
  44. package/dist/components/Input/FieldObject.svelte.d.ts +2 -7
  45. package/dist/components/Input/FieldOptions.svelte +9 -7
  46. package/dist/components/Input/FieldOptions.svelte.d.ts +3 -7
  47. package/dist/components/Input/FieldPhoneNumber.svelte +7 -8
  48. package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +3 -8
  49. package/dist/components/Input/FieldSelect.svelte +9 -8
  50. package/dist/components/Input/FieldSelect.svelte.d.ts +3 -8
  51. package/dist/components/Input/FieldSwitch.svelte +9 -7
  52. package/dist/components/Input/FieldSwitch.svelte.d.ts +3 -7
  53. package/dist/components/Input/FieldTextarea.svelte +7 -8
  54. package/dist/components/Input/FieldTextarea.svelte.d.ts +3 -8
  55. package/dist/components/Input/README.md +20 -0
  56. package/dist/components/Input/_internal/InputWrap.svelte +2 -10
  57. package/dist/components/Input/_internal/InputWrap.svelte.d.ts +2 -10
  58. package/dist/components/Input/types.d.ts +28 -0
  59. package/dist/components/Nav/Nav.svelte +5 -4
  60. package/dist/components/Nav/Nav.svelte.d.ts +2 -2
  61. package/dist/components/Nav/README.md +2 -2
  62. package/dist/components/Nav/index.css +4 -0
  63. package/dist/components/Tree/README.md +189 -0
  64. package/dist/components/Tree/Tree.svelte +46 -2
  65. package/dist/components/Tree/Tree.svelte.d.ts +5 -0
  66. package/dist/utils/input-history.svelte.d.ts +12 -0
  67. package/dist/utils/input-history.svelte.js +12 -0
  68. package/dist/utils/observe-exists.svelte.d.ts +1 -0
  69. package/dist/utils/observe-exists.svelte.js +11 -3
  70. package/dist/utils/switch.svelte.d.ts +12 -0
  71. package/dist/utils/switch.svelte.js +12 -1
  72. package/docs/architecture.md +0 -1
  73. package/docs/testing.md +72 -0
  74. package/docs/upgrading.md +281 -0
  75. package/package.json +12 -13
@@ -19,8 +19,12 @@
19
19
  next_page: "Next",
20
20
  page_x_of_y: "Page {page} of {pageCount}",
21
21
  no_data: "No data",
22
- select_all_rows: "Select all rows",
22
+ select_all_rows: "Select all rows on this page",
23
23
  select_row: "Select row",
24
+ select_all_on_page_x: "All {count} on this page selected.",
25
+ select_all_results: "Select all {totalCount} results",
26
+ all_results_selected: "All {totalCount} results selected.",
27
+ clear_selection: "Clear selection",
24
28
  };
25
29
  let out = m[k] ?? fallback ?? k;
26
30
  return isPlainObject(values)
@@ -71,6 +75,25 @@
71
75
  selected?: Set<string | number>;
72
76
  /** Toggle row selection when clicking anywhere on the row */
73
77
  selectOnRowClick?: boolean;
78
+ /** Return true to disable selection for a specific row */
79
+ selectDisabledBy?: (row: T, index: number) => boolean;
80
+
81
+ /**
82
+ * Allow the user to opt into "select all results across all pages" mode.
83
+ * When enabled and `paging.total > data.length`, a banner offers to expand
84
+ * selection beyond the current page. Consumers must execute batch operations
85
+ * as server-side filter queries (not by iterating row IDs) since off-page rows
86
+ * are not available locally.
87
+ */
88
+ allowSelectAllPages?: boolean;
89
+ /**
90
+ * All-pages selection mode (bindable). When true, selection semantics invert:
91
+ * `excluded` holds deselected IDs, and every row not in `excluded` is selected.
92
+ * Newly inserted rows are implicitly selected in this mode.
93
+ */
94
+ selectedAll?: boolean;
95
+ /** Set of row IDs explicitly deselected while in all-pages mode (bindable) */
96
+ excluded?: Set<string | number>;
74
97
 
75
98
  /** Callback when a row is clicked */
76
99
  onRowClick?: (row: T, index: number) => void;
@@ -78,16 +101,63 @@
78
101
  /** Show loading state (spinner overlay + reduced opacity) */
79
102
  loading?: boolean;
80
103
 
81
- /** Custom cell renderer snippet */
104
+ /** Custom cell renderer snippet (rendered in both desktop table and mobile card layouts; use `variant` to tell them apart) */
82
105
  cell?: Snippet<
83
- [{ column: DataTableColumn<T>; row: T; value: any; rowIndex: number }]
106
+ [
107
+ {
108
+ column: DataTableColumn<T>;
109
+ row: T;
110
+ value: any;
111
+ rowIndex: number;
112
+ variant: "desktop" | "mobile";
113
+ },
114
+ ]
84
115
  >;
85
- /** Batch actions bar snippet (shown when items are selected) */
116
+ /** Custom desktop row renderer replaces the entire `<tr>` */
117
+ row?: Snippet<
118
+ [
119
+ {
120
+ row: T;
121
+ columns: DataTableColumn<T>[];
122
+ rowIndex: number;
123
+ isSelected: boolean;
124
+ },
125
+ ]
126
+ >;
127
+ /**
128
+ * Batch actions bar snippet (shown when items are selected).
129
+ *
130
+ * Note: in all-pages mode (`selectedAll === true`) `selectedRows` only contains
131
+ * rows from the current page that aren't excluded. Off-page rows are not
132
+ * materialized — execute batch operations server-side using the active filter
133
+ * minus `excluded`.
134
+ */
86
135
  batchActions?: Snippet<
87
136
  [
88
137
  {
89
138
  selected: Set<string | number>;
90
139
  selectedRows: T[];
140
+ selectedAll: boolean;
141
+ excluded: Set<string | number>;
142
+ /** `selected.size` in normal mode, or `totalItems - excluded.size` in all-pages mode */
143
+ effectiveCount: number;
144
+ totalCount: number | null;
145
+ clearSelection: () => void;
146
+ },
147
+ ]
148
+ >;
149
+ /**
150
+ * Custom "select all results across pages" banner. When omitted, a default
151
+ * banner is rendered.
152
+ */
153
+ selectAllBanner?: Snippet<
154
+ [
155
+ {
156
+ selectedAll: boolean;
157
+ effectiveCount: number;
158
+ totalCount: number;
159
+ pageCount: number;
160
+ selectAll: () => void;
91
161
  clearSelection: () => void;
92
162
  },
93
163
  ]
@@ -96,8 +166,6 @@
96
166
  empty?: Snippet;
97
167
  /** Custom mobile row card snippet */
98
168
  mobileRow?: Snippet<[{ row: T; columns: DataTableColumn<T>[]; rowIndex: number }]>;
99
- /** Default children snippet (not used directly) */
100
- children?: Snippet;
101
169
 
102
170
  /** Optional translate function */
103
171
  t?: TranslateFn;
@@ -130,13 +198,18 @@
130
198
  selectable = false,
131
199
  selected = $bindable(new Set()),
132
200
  selectOnRowClick = false,
201
+ selectDisabledBy,
202
+ allowSelectAllPages = false,
203
+ selectedAll = $bindable(false),
204
+ excluded = $bindable(new Set()),
133
205
  onRowClick,
134
206
  loading = false,
135
207
  cell,
208
+ row,
136
209
  batchActions,
210
+ selectAllBanner,
137
211
  empty,
138
212
  mobileRow,
139
- children,
140
213
  t = t_default,
141
214
  small = false,
142
215
  unstyled = false,
@@ -154,48 +227,121 @@
154
227
 
155
228
  // --- Selection ---
156
229
  let allRowIds = $derived(data.map((row, i) => getRowId(row, i)));
230
+ let selectableRowIds = $derived.by(() => {
231
+ if (!selectDisabledBy) return allRowIds;
232
+ return data
233
+ .map((row, i) => (selectDisabledBy(row, i) ? null : getRowId(row, i)))
234
+ .filter((id): id is string | number => id !== null);
235
+ });
236
+
237
+ function isRowSelected(id: string | number): boolean {
238
+ return selectedAll ? !excluded.has(id) : selected.has(id);
239
+ }
240
+
241
+ // Batch variant avoids creating one Set per row for shift-range / select-all.
242
+ function setRowsSelected(ids: Array<string | number>, on: boolean) {
243
+ if (selectedAll) {
244
+ const next = new Set(excluded);
245
+ for (const id of ids) {
246
+ if (on) next.delete(id);
247
+ else next.add(id);
248
+ }
249
+ excluded = next;
250
+ } else {
251
+ const next = new Set(selected);
252
+ for (const id of ids) {
253
+ if (on) next.add(id);
254
+ else next.delete(id);
255
+ }
256
+ selected = next;
257
+ }
258
+ }
157
259
 
158
260
  let allOnPageSelected = $derived.by(() => {
159
- if (!selectable || allRowIds.length === 0) return false;
160
- return allRowIds.every((id) => selected.has(id));
261
+ if (!selectable || selectableRowIds.length === 0) return false;
262
+ return selectableRowIds.every((id) => isRowSelected(id));
161
263
  });
162
264
 
163
265
  let someOnPageSelected = $derived.by(() => {
164
- if (!selectable || allRowIds.length === 0) return false;
165
- return allRowIds.some((id) => selected.has(id)) && !allOnPageSelected;
266
+ if (!selectable || selectableRowIds.length === 0) return false;
267
+ return selectableRowIds.some((id) => isRowSelected(id)) && !allOnPageSelected;
268
+ });
269
+
270
+ let totalCount = $derived(paging?.total ?? null);
271
+ let effectiveCount = $derived.by(() => {
272
+ if (selectedAll) {
273
+ const base = totalCount ?? data.length;
274
+ return Math.max(0, base - excluded.size);
275
+ }
276
+ return selected.size;
166
277
  });
167
278
 
168
279
  let selectedRows = $derived.by(() => {
169
- if (!selectable || selected.size === 0) return [] as T[];
280
+ if (!selectable) return [] as T[];
281
+ if (selectedAll) {
282
+ return data.filter((row, i) => !excluded.has(getRowId(row, i)));
283
+ }
284
+ if (selected.size === 0) return [] as T[];
170
285
  return data.filter((row, i) => selected.has(getRowId(row, i)));
171
286
  });
172
287
 
173
288
  function toggleSelectAll() {
174
- if (allOnPageSelected) {
175
- const next = new Set(selected);
176
- for (const id of allRowIds) next.delete(id);
177
- selected = next;
178
- } else {
179
- const next = new Set(selected);
180
- for (const id of allRowIds) next.add(id);
181
- selected = next;
289
+ // In all-mode the header checkbox exits the mode entirely.
290
+ if (selectedAll) {
291
+ clearAllSelection();
292
+ return;
182
293
  }
294
+ setRowsSelected(selectableRowIds, !allOnPageSelected);
183
295
  }
184
296
 
185
297
  function toggleSelectRow(id: string | number) {
186
- const next = new Set(selected);
187
- if (next.has(id)) {
188
- next.delete(id);
189
- } else {
190
- next.add(id);
191
- }
192
- selected = next;
298
+ setRowsSelected([id], !isRowSelected(id));
193
299
  }
194
300
 
195
- function clearSelection() {
301
+ function enterSelectAll() {
196
302
  selected = new Set();
303
+ excluded = new Set();
304
+ selectedAll = true;
197
305
  }
198
306
 
307
+ function clearAllSelection() {
308
+ selectedAll = false;
309
+ excluded = new Set();
310
+ selected = new Set();
311
+ lastClickedIndex = null;
312
+ }
313
+
314
+ // Anchor for shift+click range selection; reset when data reference changes.
315
+ let lastClickedIndex: number | null = null;
316
+ $effect(() => {
317
+ data;
318
+ lastClickedIndex = null;
319
+ });
320
+
321
+ function handleCheckboxClick(rowIndex: number, e: MouseEvent) {
322
+ const newChecked = (e.currentTarget as HTMLInputElement).checked;
323
+ if (e.shiftKey && lastClickedIndex !== null && lastClickedIndex !== rowIndex) {
324
+ const start = Math.min(lastClickedIndex, rowIndex);
325
+ const end = Math.max(lastClickedIndex, rowIndex);
326
+ const ids: Array<string | number> = [];
327
+ for (let i = start; i <= end; i++) {
328
+ if (selectDisabledBy?.(data[i], i)) continue;
329
+ ids.push(getRowId(data[i], i));
330
+ }
331
+ setRowsSelected(ids, newChecked);
332
+ } else {
333
+ setRowsSelected([getRowId(data[rowIndex], rowIndex)], newChecked);
334
+ }
335
+ lastClickedIndex = rowIndex;
336
+ }
337
+
338
+ let showSelectAllBanner = $derived.by(() => {
339
+ if (!allowSelectAllPages || !selectable || !paging) return false;
340
+ if (paging.total <= data.length) return false;
341
+ if (selectedAll) return true;
342
+ return allOnPageSelected;
343
+ });
344
+
199
345
  // --- Row click ---
200
346
  function handleRowClick(row: T, index: number, e: MouseEvent) {
201
347
  const target = e.target as HTMLElement;
@@ -230,12 +376,48 @@
230
376
  </script>
231
377
 
232
378
  <!-- Batch action bar -->
233
- {#if selectable && selected.size > 0 && batchActions}
379
+ {#if selectable && effectiveCount > 0 && batchActions}
234
380
  <div class={!unstyled ? "stuic-data-table-batch" : undefined}>
235
- {@render batchActions({ selected, selectedRows, clearSelection })}
381
+ {@render batchActions({
382
+ selected,
383
+ selectedRows,
384
+ selectedAll,
385
+ excluded,
386
+ effectiveCount,
387
+ totalCount,
388
+ clearSelection: clearAllSelection,
389
+ })}
236
390
  </div>
237
391
  {/if}
238
392
 
393
+ <!-- Select-all-across-pages banner -->
394
+ {#if showSelectAllBanner && paging}
395
+ {#if selectAllBanner}
396
+ {@render selectAllBanner({
397
+ selectedAll,
398
+ effectiveCount,
399
+ totalCount: paging.total,
400
+ pageCount: data.length,
401
+ selectAll: enterSelectAll,
402
+ clearSelection: clearAllSelection,
403
+ })}
404
+ {:else}
405
+ <div class={!unstyled ? "stuic-data-table-select-all-banner" : undefined}>
406
+ {#if selectedAll}
407
+ <span>{t("all_results_selected", { totalCount: paging.total })}</span>
408
+ <Button variant="ghost" size="sm" onclick={clearAllSelection}>
409
+ {t("clear_selection")}
410
+ </Button>
411
+ {:else}
412
+ <span>{t("select_all_on_page_x", { count: data.length })}</span>
413
+ <Button variant="ghost" size="sm" onclick={enterSelectAll}>
414
+ {t("select_all_results", { totalCount: paging.total })}
415
+ </Button>
416
+ {/if}
417
+ </div>
418
+ {/if}
419
+ {/if}
420
+
239
421
  <!-- Root container -->
240
422
  <div bind:this={el} class={rootClass} {...rest}>
241
423
  {#if isDesktop}
@@ -248,7 +430,7 @@
248
430
  <thead>
249
431
  <tr>
250
432
  {#if selectable}
251
- <th data-checkbox class="stuic-checkbox">
433
+ <th scope="col" data-checkbox class="stuic-checkbox">
252
434
  <input
253
435
  type="checkbox"
254
436
  checked={allOnPageSelected}
@@ -260,6 +442,7 @@
260
442
  {/if}
261
443
  {#each columns as col (col.key)}
262
444
  <th
445
+ scope="col"
263
446
  class={col.classHeader}
264
447
  data-align={!unstyled && col.align ? col.align : undefined}
265
448
  style={col.width ? `width: ${col.width}` : undefined}
@@ -274,46 +457,53 @@
274
457
  </tr>
275
458
  </thead>
276
459
  <tbody>
277
- {#each data as row, rowIndex (getRowId(row, rowIndex))}
278
- {@const rowId = getRowId(row, rowIndex)}
279
- {@const isSelected = selectable && selected.has(rowId)}
280
- <tr
281
- data-hoverable={!unstyled ? "true" : undefined}
282
- data-clickable={!unstyled && (onRowClick || selectOnRowClick)
283
- ? "true"
284
- : undefined}
285
- data-selected={!unstyled && isSelected ? "true" : undefined}
286
- onclick={(e) => handleRowClick(row, rowIndex, e)}
287
- >
288
- {#if selectable}
289
- <td data-checkbox class="stuic-checkbox">
290
- <input
291
- type="checkbox"
292
- checked={isSelected}
293
- onchange={() => toggleSelectRow(rowId)}
294
- aria-label={t("select_row")}
295
- />
296
- </td>
297
- {/if}
298
- {#each columns as col (col.key)}
299
- {@const value = getCellValue(row, col)}
300
- <td
301
- class={col.class}
302
- data-align={!unstyled && col.align ? col.align : undefined}
303
- >
304
- {#if cell}
305
- {@render cell({
306
- column: col,
307
- row,
308
- value,
309
- rowIndex,
310
- })}
311
- {:else}
312
- {getCellDisplay(row, col)}
313
- {/if}
314
- </td>
315
- {/each}
316
- </tr>
460
+ {#each data as rowData, rowIndex (getRowId(rowData, rowIndex))}
461
+ {@const rowId = getRowId(rowData, rowIndex)}
462
+ {@const isSelected = selectable && isRowSelected(rowId)}
463
+ {@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
464
+ {#if row}
465
+ {@render row({ row: rowData, columns, rowIndex, isSelected })}
466
+ {:else}
467
+ <tr
468
+ data-hoverable={!unstyled ? "true" : undefined}
469
+ data-clickable={!unstyled && (onRowClick || selectOnRowClick)
470
+ ? "true"
471
+ : undefined}
472
+ data-selected={!unstyled && isSelected ? "true" : undefined}
473
+ onclick={(e) => handleRowClick(rowData, rowIndex, e)}
474
+ >
475
+ {#if selectable}
476
+ <td data-checkbox class="stuic-checkbox">
477
+ <input
478
+ type="checkbox"
479
+ checked={isSelected}
480
+ disabled={selectDisabled}
481
+ onclick={(e) => handleCheckboxClick(rowIndex, e)}
482
+ aria-label={t("select_row")}
483
+ />
484
+ </td>
485
+ {/if}
486
+ {#each columns as col (col.key)}
487
+ {@const value = getCellValue(rowData, col)}
488
+ <td
489
+ class={col.class}
490
+ data-align={!unstyled && col.align ? col.align : undefined}
491
+ >
492
+ {#if cell}
493
+ {@render cell({
494
+ column: col,
495
+ row: rowData,
496
+ value,
497
+ rowIndex,
498
+ variant: "desktop",
499
+ })}
500
+ {:else}
501
+ {getCellDisplay(rowData, col)}
502
+ {/if}
503
+ </td>
504
+ {/each}
505
+ </tr>
506
+ {/if}
317
507
  {:else}
318
508
  <tr>
319
509
  <td
@@ -337,12 +527,13 @@
337
527
  class={!unstyled ? "stuic-data-table-cards" : undefined}
338
528
  data-loading={!unstyled && loading ? "true" : undefined}
339
529
  >
340
- {#each data as row, rowIndex (getRowId(row, rowIndex))}
341
- {@const rowId = getRowId(row, rowIndex)}
342
- {@const isSelected = selectable && selected.has(rowId)}
530
+ {#each data as rowData, rowIndex (getRowId(rowData, rowIndex))}
531
+ {@const rowId = getRowId(rowData, rowIndex)}
532
+ {@const isSelected = selectable && isRowSelected(rowId)}
533
+ {@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
343
534
  {#if mobileRow}
344
535
  {@render mobileRow({
345
- row,
536
+ row: rowData,
346
537
  columns: mobileColumns,
347
538
  rowIndex,
348
539
  })}
@@ -357,7 +548,7 @@
357
548
  data-selected={!unstyled && isSelected ? "true" : undefined}
358
549
  role={onRowClick || selectOnRowClick ? "button" : undefined}
359
550
  tabindex={onRowClick || selectOnRowClick ? 0 : undefined}
360
- onclick={(e) => handleRowClick(row, rowIndex, e)}
551
+ onclick={(e) => handleRowClick(rowData, rowIndex, e)}
361
552
  onkeydown={(e) => {
362
553
  if (
363
554
  (onRowClick || selectOnRowClick) &&
@@ -367,22 +558,23 @@
367
558
  if (selectable && selectOnRowClick) {
368
559
  toggleSelectRow(rowId);
369
560
  }
370
- onRowClick?.(row, rowIndex);
561
+ onRowClick?.(rowData, rowIndex);
371
562
  }
372
563
  }}
373
564
  >
374
565
  {#if selectable}
375
- <div class="stuic-checkbox flex items-center gap-2 mb-1">
566
+ <div class={!unstyled ? "stuic-checkbox stuic-data-table-card-checkbox" : undefined}>
376
567
  <input
377
568
  type="checkbox"
378
569
  checked={isSelected}
379
- onchange={() => toggleSelectRow(rowId)}
570
+ disabled={selectDisabled}
571
+ onclick={(e) => handleCheckboxClick(rowIndex, e)}
380
572
  aria-label={t("select_row")}
381
573
  />
382
574
  </div>
383
575
  {/if}
384
576
  {#each mobileColumns as col (col.key)}
385
- {@const value = getCellValue(row, col)}
577
+ {@const value = getCellValue(rowData, col)}
386
578
  <div class={!unstyled ? "stuic-data-table-card-row" : undefined}>
387
579
  <span class={!unstyled ? "stuic-data-table-card-label" : undefined}>
388
580
  {#if isTHCNotEmpty(col.label)}
@@ -395,12 +587,13 @@
395
587
  {#if cell}
396
588
  {@render cell({
397
589
  column: col,
398
- row,
590
+ row: rowData,
399
591
  value,
400
592
  rowIndex,
593
+ variant: "mobile",
401
594
  })}
402
595
  {:else}
403
- {getCellDisplay(row, col)}
596
+ {getCellDisplay(rowData, col)}
404
597
  {/if}
405
598
  </span>
406
599
  </div>
@@ -412,7 +605,7 @@
412
605
  {#if empty}
413
606
  {@render empty()}
414
607
  {:else}
415
- No data
608
+ {t("no_data")}
416
609
  {/if}
417
610
  </div>
418
611
  {/each}
@@ -38,24 +38,78 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
38
38
  selected?: Set<string | number>;
39
39
  /** Toggle row selection when clicking anywhere on the row */
40
40
  selectOnRowClick?: boolean;
41
+ /** Return true to disable selection for a specific row */
42
+ selectDisabledBy?: (row: T, index: number) => boolean;
43
+ /**
44
+ * Allow the user to opt into "select all results across all pages" mode.
45
+ * When enabled and `paging.total > data.length`, a banner offers to expand
46
+ * selection beyond the current page. Consumers must execute batch operations
47
+ * as server-side filter queries (not by iterating row IDs) since off-page rows
48
+ * are not available locally.
49
+ */
50
+ allowSelectAllPages?: boolean;
51
+ /**
52
+ * All-pages selection mode (bindable). When true, selection semantics invert:
53
+ * `excluded` holds deselected IDs, and every row not in `excluded` is selected.
54
+ * Newly inserted rows are implicitly selected in this mode.
55
+ */
56
+ selectedAll?: boolean;
57
+ /** Set of row IDs explicitly deselected while in all-pages mode (bindable) */
58
+ excluded?: Set<string | number>;
41
59
  /** Callback when a row is clicked */
42
60
  onRowClick?: (row: T, index: number) => void;
43
61
  /** Show loading state (spinner overlay + reduced opacity) */
44
62
  loading?: boolean;
45
- /** Custom cell renderer snippet */
63
+ /** Custom cell renderer snippet (rendered in both desktop table and mobile card layouts; use `variant` to tell them apart) */
46
64
  cell?: Snippet<[
47
65
  {
48
66
  column: DataTableColumn<T>;
49
67
  row: T;
50
68
  value: any;
51
69
  rowIndex: number;
70
+ variant: "desktop" | "mobile";
52
71
  }
53
72
  ]>;
54
- /** Batch actions bar snippet (shown when items are selected) */
73
+ /** Custom desktop row renderer replaces the entire `<tr>` */
74
+ row?: Snippet<[
75
+ {
76
+ row: T;
77
+ columns: DataTableColumn<T>[];
78
+ rowIndex: number;
79
+ isSelected: boolean;
80
+ }
81
+ ]>;
82
+ /**
83
+ * Batch actions bar snippet (shown when items are selected).
84
+ *
85
+ * Note: in all-pages mode (`selectedAll === true`) `selectedRows` only contains
86
+ * rows from the current page that aren't excluded. Off-page rows are not
87
+ * materialized — execute batch operations server-side using the active filter
88
+ * minus `excluded`.
89
+ */
55
90
  batchActions?: Snippet<[
56
91
  {
57
92
  selected: Set<string | number>;
58
93
  selectedRows: T[];
94
+ selectedAll: boolean;
95
+ excluded: Set<string | number>;
96
+ /** `selected.size` in normal mode, or `totalItems - excluded.size` in all-pages mode */
97
+ effectiveCount: number;
98
+ totalCount: number | null;
99
+ clearSelection: () => void;
100
+ }
101
+ ]>;
102
+ /**
103
+ * Custom "select all results across pages" banner. When omitted, a default
104
+ * banner is rendered.
105
+ */
106
+ selectAllBanner?: Snippet<[
107
+ {
108
+ selectedAll: boolean;
109
+ effectiveCount: number;
110
+ totalCount: number;
111
+ pageCount: number;
112
+ selectAll: () => void;
59
113
  clearSelection: () => void;
60
114
  }
61
115
  ]>;
@@ -67,8 +121,6 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
67
121
  columns: DataTableColumn<T>[];
68
122
  rowIndex: number;
69
123
  }]>;
70
- /** Default children snippet (not used directly) */
71
- children?: Snippet;
72
124
  /** Optional translate function */
73
125
  t?: TranslateFn;
74
126
  /** Force mobile/card layout regardless of breakpoint */
@@ -83,7 +135,7 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
83
135
  declare function $$render<T extends Record<string, any> = Record<string, any>>(): {
84
136
  props: Props<T>;
85
137
  exports: {};
86
- bindings: "el" | "selected";
138
+ bindings: "el" | "selected" | "selectedAll" | "excluded";
87
139
  slots: {};
88
140
  events: {};
89
141
  };
@@ -91,7 +143,7 @@ declare class __sveltets_Render<T extends Record<string, any> = Record<string, a
91
143
  props(): ReturnType<typeof $$render<T>>['props'];
92
144
  events(): ReturnType<typeof $$render<T>>['events'];
93
145
  slots(): ReturnType<typeof $$render<T>>['slots'];
94
- bindings(): "el" | "selected";
146
+ bindings(): "el" | "selected" | "selectedAll" | "excluded";
95
147
  exports(): {};
96
148
  }
97
149
  interface $$IsomorphicComponent {