@marianmeres/stuic 3.67.0 → 3.69.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 (30) hide show
  1. package/dist/components/DataTable/DataTable.svelte +182 -26
  2. package/dist/components/DataTable/DataTable.svelte.d.ts +45 -3
  3. package/dist/components/DataTable/README.md +84 -8
  4. package/dist/components/DataTable/index.css +24 -0
  5. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +223 -0
  6. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +68 -0
  7. package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte +184 -0
  8. package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte.d.ts +55 -0
  9. package/dist/components/LoginOrRegisterForm/_internal/login-or-register-form-i18n-defaults.d.ts +1 -0
  10. package/dist/components/LoginOrRegisterForm/_internal/login-or-register-form-i18n-defaults.js +17 -0
  11. package/dist/components/LoginOrRegisterForm/index.css +66 -0
  12. package/dist/components/LoginOrRegisterForm/index.d.ts +2 -0
  13. package/dist/components/LoginOrRegisterForm/index.js +2 -0
  14. package/dist/components/RegisterForm/RegisterForm.svelte +367 -0
  15. package/dist/components/RegisterForm/RegisterForm.svelte.d.ts +77 -0
  16. package/dist/components/RegisterForm/RegisterFormModal.svelte +212 -0
  17. package/dist/components/RegisterForm/RegisterFormModal.svelte.d.ts +82 -0
  18. package/dist/components/RegisterForm/_internal/register-form-i18n-defaults.d.ts +1 -0
  19. package/dist/components/RegisterForm/_internal/register-form-i18n-defaults.js +30 -0
  20. package/dist/components/RegisterForm/_internal/register-form-types.d.ts +35 -0
  21. package/dist/components/RegisterForm/_internal/register-form-types.js +1 -0
  22. package/dist/components/RegisterForm/_internal/register-form-utils.d.ts +7 -0
  23. package/dist/components/RegisterForm/_internal/register-form-utils.js +55 -0
  24. package/dist/components/RegisterForm/index.css +70 -0
  25. package/dist/components/RegisterForm/index.d.ts +4 -0
  26. package/dist/components/RegisterForm/index.js +3 -0
  27. package/dist/index.css +2 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +2 -0
  30. package/package.json +1 -1
@@ -21,6 +21,10 @@
21
21
  no_data: "No data",
22
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)
@@ -74,6 +78,23 @@
74
78
  /** Return true to disable selection for a specific row */
75
79
  selectDisabledBy?: (row: T, index: number) => boolean;
76
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>;
97
+
77
98
  /** Callback when a row is clicked */
78
99
  onRowClick?: (row: T, index: number) => void;
79
100
 
@@ -103,12 +124,40 @@
103
124
  },
104
125
  ]
105
126
  >;
106
- /** Batch actions bar snippet (shown when items are selected) */
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
+ */
107
135
  batchActions?: Snippet<
108
136
  [
109
137
  {
110
138
  selected: Set<string | number>;
111
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;
112
161
  clearSelection: () => void;
113
162
  },
114
163
  ]
@@ -150,11 +199,15 @@
150
199
  selected = $bindable(new Set()),
151
200
  selectOnRowClick = false,
152
201
  selectDisabledBy,
202
+ allowSelectAllPages = false,
203
+ selectedAll = $bindable(false),
204
+ excluded = $bindable(new Set()),
153
205
  onRowClick,
154
206
  loading = false,
155
207
  cell,
156
208
  row,
157
209
  batchActions,
210
+ selectAllBanner,
158
211
  empty,
159
212
  mobileRow,
160
213
  t = t_default,
@@ -181,47 +234,114 @@
181
234
  .filter((id): id is string | number => id !== null);
182
235
  });
183
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
+ }
259
+
184
260
  let allOnPageSelected = $derived.by(() => {
185
261
  if (!selectable || selectableRowIds.length === 0) return false;
186
- return selectableRowIds.every((id) => selected.has(id));
262
+ return selectableRowIds.every((id) => isRowSelected(id));
187
263
  });
188
264
 
189
265
  let someOnPageSelected = $derived.by(() => {
190
266
  if (!selectable || selectableRowIds.length === 0) return false;
191
- return selectableRowIds.some((id) => selected.has(id)) && !allOnPageSelected;
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;
192
277
  });
193
278
 
194
279
  let selectedRows = $derived.by(() => {
195
- 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[];
196
285
  return data.filter((row, i) => selected.has(getRowId(row, i)));
197
286
  });
198
287
 
199
288
  function toggleSelectAll() {
200
- if (allOnPageSelected) {
201
- const next = new Set(selected);
202
- for (const id of selectableRowIds) next.delete(id);
203
- selected = next;
204
- } else {
205
- const next = new Set(selected);
206
- for (const id of selectableRowIds) next.add(id);
207
- selected = next;
289
+ // In all-mode the header checkbox exits the mode entirely.
290
+ if (selectedAll) {
291
+ clearAllSelection();
292
+ return;
208
293
  }
294
+ setRowsSelected(selectableRowIds, !allOnPageSelected);
209
295
  }
210
296
 
211
297
  function toggleSelectRow(id: string | number) {
212
- const next = new Set(selected);
213
- if (next.has(id)) {
214
- next.delete(id);
215
- } else {
216
- next.add(id);
217
- }
218
- selected = next;
298
+ setRowsSelected([id], !isRowSelected(id));
219
299
  }
220
300
 
221
- function clearSelection() {
301
+ function enterSelectAll() {
222
302
  selected = new Set();
303
+ excluded = new Set();
304
+ selectedAll = true;
305
+ }
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;
223
336
  }
224
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
+
225
345
  // --- Row click ---
226
346
  function handleRowClick(row: T, index: number, e: MouseEvent) {
227
347
  const target = e.target as HTMLElement;
@@ -256,12 +376,48 @@
256
376
  </script>
257
377
 
258
378
  <!-- Batch action bar -->
259
- {#if selectable && selected.size > 0 && batchActions}
379
+ {#if selectable && effectiveCount > 0 && batchActions}
260
380
  <div class={!unstyled ? "stuic-data-table-batch" : undefined}>
261
- {@render batchActions({ selected, selectedRows, clearSelection })}
381
+ {@render batchActions({
382
+ selected,
383
+ selectedRows,
384
+ selectedAll,
385
+ excluded,
386
+ effectiveCount,
387
+ totalCount,
388
+ clearSelection: clearAllSelection,
389
+ })}
262
390
  </div>
263
391
  {/if}
264
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
+
265
421
  <!-- Root container -->
266
422
  <div bind:this={el} class={rootClass} {...rest}>
267
423
  {#if isDesktop}
@@ -303,7 +459,7 @@
303
459
  <tbody>
304
460
  {#each data as rowData, rowIndex (getRowId(rowData, rowIndex))}
305
461
  {@const rowId = getRowId(rowData, rowIndex)}
306
- {@const isSelected = selectable && selected.has(rowId)}
462
+ {@const isSelected = selectable && isRowSelected(rowId)}
307
463
  {@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
308
464
  {#if row}
309
465
  {@render row({ row: rowData, columns, rowIndex, isSelected })}
@@ -322,7 +478,7 @@
322
478
  type="checkbox"
323
479
  checked={isSelected}
324
480
  disabled={selectDisabled}
325
- onchange={() => toggleSelectRow(rowId)}
481
+ onclick={(e) => handleCheckboxClick(rowIndex, e)}
326
482
  aria-label={t("select_row")}
327
483
  />
328
484
  </td>
@@ -373,7 +529,7 @@
373
529
  >
374
530
  {#each data as rowData, rowIndex (getRowId(rowData, rowIndex))}
375
531
  {@const rowId = getRowId(rowData, rowIndex)}
376
- {@const isSelected = selectable && selected.has(rowId)}
532
+ {@const isSelected = selectable && isRowSelected(rowId)}
377
533
  {@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
378
534
  {#if mobileRow}
379
535
  {@render mobileRow({
@@ -412,7 +568,7 @@
412
568
  type="checkbox"
413
569
  checked={isSelected}
414
570
  disabled={selectDisabled}
415
- onchange={() => toggleSelectRow(rowId)}
571
+ onclick={(e) => handleCheckboxClick(rowIndex, e)}
416
572
  aria-label={t("select_row")}
417
573
  />
418
574
  </div>
@@ -40,6 +40,22 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
40
40
  selectOnRowClick?: boolean;
41
41
  /** Return true to disable selection for a specific row */
42
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>;
43
59
  /** Callback when a row is clicked */
44
60
  onRowClick?: (row: T, index: number) => void;
45
61
  /** Show loading state (spinner overlay + reduced opacity) */
@@ -63,11 +79,37 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
63
79
  isSelected: boolean;
64
80
  }
65
81
  ]>;
66
- /** Batch actions bar snippet (shown when items are selected) */
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
+ */
67
90
  batchActions?: Snippet<[
68
91
  {
69
92
  selected: Set<string | number>;
70
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;
71
113
  clearSelection: () => void;
72
114
  }
73
115
  ]>;
@@ -93,7 +135,7 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
93
135
  declare function $$render<T extends Record<string, any> = Record<string, any>>(): {
94
136
  props: Props<T>;
95
137
  exports: {};
96
- bindings: "el" | "selected";
138
+ bindings: "el" | "selected" | "selectedAll" | "excluded";
97
139
  slots: {};
98
140
  events: {};
99
141
  };
@@ -101,7 +143,7 @@ declare class __sveltets_Render<T extends Record<string, any> = Record<string, a
101
143
  props(): ReturnType<typeof $$render<T>>['props'];
102
144
  events(): ReturnType<typeof $$render<T>>['events'];
103
145
  slots(): ReturnType<typeof $$render<T>>['slots'];
104
- bindings(): "el" | "selected";
146
+ bindings(): "el" | "selected" | "selectedAll" | "excluded";
105
147
  exports(): {};
106
148
  }
107
149
  interface $$IsomorphicComponent {
@@ -62,6 +62,72 @@ DataTable integrates with [`@marianmeres/paging-store`](https://github.com/maria
62
62
  </DataTable>
63
63
  ```
64
64
 
65
+ Shift+click a checkbox to toggle a range of rows from the last clicked checkbox to the current one. Disabled rows in the range are skipped. The anchor resets when `data` changes (e.g. on page navigation).
66
+
67
+ ### Select All Across Pages
68
+
69
+ When your data is paged, DataTable can offer a "select all results" affordance that expands selection beyond the current page. Enable it with `allowSelectAllPages`. When the user opts in, selection flips to **all-pages mode**: every row is implicitly selected, and `excluded` holds IDs the user has explicitly deselected.
70
+
71
+ ```svelte
72
+ <script lang="ts">
73
+ let selected = $state(new Set<string | number>());
74
+ let selectedAll = $state(false);
75
+ let excluded = $state(new Set<string | number>());
76
+ </script>
77
+
78
+ <DataTable
79
+ {columns}
80
+ {data}
81
+ {paging}
82
+ {onPageChange}
83
+ selectable
84
+ allowSelectAllPages
85
+ bind:selected
86
+ bind:selectedAll
87
+ bind:excluded
88
+ getRowId={(row) => row.id}
89
+ >
90
+ {#snippet batchActions({ selectedAll, excluded, effectiveCount, totalCount, clearSelection })}
91
+ <span>{effectiveCount} selected{selectedAll ? ` of ${totalCount}` : ""}</span>
92
+ <Button onclick={() => deleteSelection({ selectedAll, excluded })}>Delete</Button>
93
+ <Button variant="ghost" onclick={clearSelection}>Clear</Button>
94
+ {/snippet}
95
+ </DataTable>
96
+ ```
97
+
98
+ **Important — executing batch operations:** in all-pages mode the off-page rows are not loaded locally, so consumers cannot iterate IDs. Execute operations server-side using the active filter minus `excluded`:
99
+
100
+ ```ts
101
+ function deleteSelection({ selectedAll, excluded }) {
102
+ if (selectedAll) {
103
+ // Server-side: DELETE FROM rows WHERE <currentFilter> AND id NOT IN excluded
104
+ return api.delete({ filter: currentFilter, exclude: [...excluded] });
105
+ }
106
+ return api.delete({ ids: [...selected] });
107
+ }
108
+ ```
109
+
110
+ New records inserted while all-pages mode is active are implicitly selected (they aren't in `excluded`). This matches the conventional intent — "delete everything matching X" should include matches that arrive before the operation runs. If you need snapshot-at-click semantics, capture a timestamp or ID list in your consumer.
111
+
112
+ **Customising the banner:** the default banner uses the built-in `t()` keys `select_all_on_page_x`, `select_all_results`, `all_results_selected`, and `clear_selection`. Override markup entirely with the `selectAllBanner` snippet:
113
+
114
+ ```svelte
115
+ <DataTable {columns} {data} {paging} selectable allowSelectAllPages bind:selected bind:selectedAll bind:excluded>
116
+ {#snippet selectAllBanner({ selectedAll, totalCount, selectAll, clearSelection })}
117
+ <div class="my-banner">
118
+ {#if selectedAll}
119
+ <span>All {totalCount} selected.</span>
120
+ <button onclick={clearSelection}>Undo</button>
121
+ {:else}
122
+ <button onclick={selectAll}>Select all {totalCount}</button>
123
+ {/if}
124
+ </div>
125
+ {/snippet}
126
+ </DataTable>
127
+ ```
128
+
129
+ **Filter changes:** when filters change in the consumer, reset the bound selection stores (`selected`, `selectedAll`, `excluded`) explicitly — DataTable doesn't track which filter produced the current state.
130
+
65
131
  ### Custom Cell Rendering
66
132
 
67
133
  The `cell` snippet is used for both desktop and mobile layouts. Use the `variant` param if rendering differs per layout.
@@ -134,6 +200,9 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
134
200
  | `selected` | `Set<string \| number>` | `new Set()` | Selected row IDs (bindable) |
135
201
  | `selectOnRowClick` | `boolean` | `false` | Clicking anywhere on a row toggles its selection |
136
202
  | `selectDisabledBy` | `(row, index) => boolean` | - | Return `true` to disable selection for a specific row |
203
+ | `allowSelectAllPages` | `boolean` | `false` | Show a banner offering "select all results" across paged data |
204
+ | `selectedAll` | `boolean` | `false` | All-pages mode flag (bindable). In this mode `excluded` drives selection |
205
+ | `excluded` | `Set<string \| number>` | `new Set()` | Deselected row IDs while in all-pages mode (bindable) |
137
206
  | `onRowClick` | `(row, index) => void` | - | Row click callback |
138
207
  | `loading` | `boolean` | `false` | Show loading overlay |
139
208
  | `small` | `boolean` | `false` | Force mobile/card layout regardless of viewport |
@@ -142,6 +211,7 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
142
211
  | `row` | `Snippet` | - | Custom desktop `<tr>` renderer (overrides default row) |
143
212
  | `mobileRow` | `Snippet` | - | Custom mobile card renderer |
144
213
  | `batchActions` | `Snippet` | - | Batch action bar content |
214
+ | `selectAllBanner` | `Snippet` | - | Override default "select all across pages" banner |
145
215
  | `empty` | `Snippet` | - | Custom empty state |
146
216
  | `unstyled` | `boolean` | `false` | Skip default styling |
147
217
  | `class` | `string` | - | Additional CSS classes |
@@ -149,15 +219,18 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
149
219
 
150
220
  ### Snippet signatures
151
221
 
152
- | Snippet | Props |
153
- | -------------- | ------------------------------------------------------------------------------------------------ |
154
- | `cell` | `{ column, row, value, rowIndex, variant: "desktop" \| "mobile" }` |
155
- | `row` | `{ row, columns, rowIndex, isSelected }` — desktop only |
156
- | `mobileRow` | `{ row, columns, rowIndex }` — mobile only |
157
- | `batchActions` | `{ selected, selectedRows, clearSelection }` |
158
- | `empty` | |
222
+ | Snippet | Props |
223
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------- |
224
+ | `cell` | `{ column, row, value, rowIndex, variant: "desktop" \| "mobile" }` |
225
+ | `row` | `{ row, columns, rowIndex, isSelected }` — desktop only |
226
+ | `mobileRow` | `{ row, columns, rowIndex }` — mobile only |
227
+ | `batchActions` | `{ selected, selectedRows, selectedAll, excluded, effectiveCount, totalCount, clearSelection }` |
228
+ | `selectAllBanner` | `{ selectedAll, effectiveCount, totalCount, pageCount, selectAll, clearSelection }` |
229
+ | `empty` | — |
159
230
 
160
- > **Note:** "Select all rows" affects only the rows currently in `data` (i.e. the current page when using external paging). Rows for which `selectDisabledBy` returns `true` are excluded from "select all".
231
+ > **Note:** "Select all rows" affects only the rows currently in `data` (i.e. the current page when using external paging). Rows for which `selectDisabledBy` returns `true` are excluded from "select all". To select across pages, enable `allowSelectAllPages` and use the banner that appears.
232
+ >
233
+ > **Note:** `clearSelection` (exposed by the `batchActions` snippet) resets all selection state — `selected`, `selectedAll`, and `excluded` — including exiting all-pages mode.
161
234
 
162
235
  ## DataTableColumn
163
236
 
@@ -197,3 +270,6 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
197
270
  | `--stuic-data-table-card-radius` | `var(--radius-md)` | Mobile card radius |
198
271
  | `--stuic-data-table-card-padding` | `0.75rem` | Mobile card padding |
199
272
  | `--stuic-data-table-card-gap` | `0.5rem` | Gap between mobile cards |
273
+ | `--stuic-data-table-select-all-bg` | `color-mix(primary 10%)` | Select-all banner background |
274
+ | `--stuic-data-table-select-all-padding-x` | `0.75rem` | Banner horizontal padding |
275
+ | `--stuic-data-table-select-all-padding-y` | `0.5rem` | Banner vertical padding |
@@ -38,6 +38,11 @@
38
38
  --stuic-data-table-batch-padding-x: 0.75rem;
39
39
  --stuic-data-table-batch-padding-y: 0.5rem;
40
40
 
41
+ /* Select-all-across-pages banner */
42
+ --stuic-data-table-select-all-bg: color-mix(in srgb, var(--stuic-color-primary) 10%, var(--stuic-color-background));
43
+ --stuic-data-table-select-all-padding-x: 0.75rem;
44
+ --stuic-data-table-select-all-padding-y: 0.5rem;
45
+
41
46
  /* Checkbox column */
42
47
  --stuic-data-table-checkbox-width: 3rem;
43
48
 
@@ -210,6 +215,25 @@
210
215
  margin-bottom: 0.5rem;
211
216
  }
212
217
 
218
+ /* ============================================================================
219
+ SELECT-ALL-ACROSS-PAGES BANNER
220
+ ============================================================================ */
221
+
222
+ .stuic-data-table-select-all-banner {
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ gap: var(--stuic-data-table-paging-gap);
227
+ padding: var(--stuic-data-table-select-all-padding-y)
228
+ var(--stuic-data-table-select-all-padding-x);
229
+ background: var(--stuic-data-table-select-all-bg);
230
+ border-radius: var(--stuic-data-table-radius, var(--stuic-radius));
231
+ margin-bottom: 0.5rem;
232
+ font-size: var(--stuic-data-table-cell-font-size);
233
+ color: var(--stuic-data-table-header-color);
234
+ text-align: center;
235
+ }
236
+
213
237
  /* ============================================================================
214
238
  LOADING SPINNER OVERLAY
215
239
  ============================================================================ */