@marianmeres/stuic 3.66.0 → 3.67.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/dist/actions/autoscroll.d.ts +7 -0
- package/dist/actions/autoscroll.js +7 -0
- package/dist/actions/focus-trap.d.ts +7 -0
- package/dist/actions/focus-trap.js +8 -3
- package/dist/actions/typeahead.svelte.js +40 -4
- package/dist/components/Carousel/Carousel.svelte +9 -2
- package/dist/components/Carousel/README.md +8 -2
- package/dist/components/Cart/Cart.svelte +3 -0
- package/dist/components/Cart/README.md +18 -1
- package/dist/components/Checkout/CheckoutOrderReview.svelte +4 -14
- package/dist/components/Checkout/README.md +184 -0
- package/dist/components/Checkout/_internal/checkout-utils.d.ts +6 -0
- package/dist/components/Checkout/_internal/checkout-utils.js +24 -0
- package/dist/components/Checkout/index.d.ts +1 -1
- package/dist/components/Checkout/index.js +1 -1
- package/dist/components/CommandMenu/CommandMenu.svelte +23 -7
- package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +2 -0
- package/dist/components/CronInput/CronInput.svelte +44 -9
- package/dist/components/CronInput/CronInput.svelte.d.ts +2 -0
- package/dist/components/CronInput/README.md +145 -0
- package/dist/components/CronInput/cron-next-run.svelte.d.ts +11 -0
- package/dist/components/CronInput/cron-next-run.svelte.js +11 -0
- package/dist/components/CronInput/index.css +0 -8
- package/dist/components/DataTable/DataTable.svelte +99 -62
- package/dist/components/DataTable/DataTable.svelte.d.ts +13 -3
- package/dist/components/DataTable/README.md +79 -25
- package/dist/components/DataTable/index.css +7 -0
- package/dist/components/DropdownMenu/DropdownMenu.svelte +43 -26
- package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +5 -1
- package/dist/components/DropdownMenu/README.md +37 -9
- package/dist/components/Input/FieldAssets.svelte +9 -7
- package/dist/components/Input/FieldAssets.svelte.d.ts +3 -7
- package/dist/components/Input/FieldFile.svelte +13 -7
- package/dist/components/Input/FieldFile.svelte.d.ts +4 -7
- package/dist/components/Input/FieldInput.svelte +10 -8
- package/dist/components/Input/FieldInput.svelte.d.ts +3 -8
- package/dist/components/Input/FieldInputLocalized.svelte +8 -7
- package/dist/components/Input/FieldInputLocalized.svelte.d.ts +2 -7
- package/dist/components/Input/FieldKeyValues.svelte +8 -7
- package/dist/components/Input/FieldKeyValues.svelte.d.ts +2 -7
- package/dist/components/Input/FieldLikeButton.svelte +9 -7
- package/dist/components/Input/FieldLikeButton.svelte.d.ts +3 -7
- package/dist/components/Input/FieldObject.svelte +8 -7
- package/dist/components/Input/FieldObject.svelte.d.ts +2 -7
- package/dist/components/Input/FieldOptions.svelte +9 -7
- package/dist/components/Input/FieldOptions.svelte.d.ts +3 -7
- package/dist/components/Input/FieldPhoneNumber.svelte +7 -8
- package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +3 -8
- package/dist/components/Input/FieldSelect.svelte +9 -8
- package/dist/components/Input/FieldSelect.svelte.d.ts +3 -8
- package/dist/components/Input/FieldSwitch.svelte +9 -7
- package/dist/components/Input/FieldSwitch.svelte.d.ts +3 -7
- package/dist/components/Input/FieldTextarea.svelte +7 -8
- package/dist/components/Input/FieldTextarea.svelte.d.ts +3 -8
- package/dist/components/Input/README.md +20 -0
- package/dist/components/Input/_internal/InputWrap.svelte +2 -10
- package/dist/components/Input/_internal/InputWrap.svelte.d.ts +2 -10
- package/dist/components/Input/types.d.ts +28 -0
- package/dist/components/Nav/Nav.svelte +5 -4
- package/dist/components/Nav/Nav.svelte.d.ts +2 -2
- package/dist/components/Nav/README.md +2 -2
- package/dist/components/Nav/index.css +4 -0
- package/dist/components/Tree/README.md +189 -0
- package/dist/components/Tree/Tree.svelte +46 -2
- package/dist/components/Tree/Tree.svelte.d.ts +5 -0
- package/dist/utils/input-history.svelte.d.ts +12 -0
- package/dist/utils/input-history.svelte.js +12 -0
- package/dist/utils/observe-exists.svelte.d.ts +1 -0
- package/dist/utils/observe-exists.svelte.js +11 -3
- package/dist/utils/switch.svelte.d.ts +12 -0
- package/dist/utils/switch.svelte.js +12 -1
- package/docs/architecture.md +0 -1
- package/docs/testing.md +72 -0
- package/docs/upgrading.md +281 -0
- package/package.json +18 -19
|
@@ -19,7 +19,7 @@
|
|
|
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
24
|
};
|
|
25
25
|
let out = m[k] ?? fallback ?? k;
|
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
selected?: Set<string | number>;
|
|
72
72
|
/** Toggle row selection when clicking anywhere on the row */
|
|
73
73
|
selectOnRowClick?: boolean;
|
|
74
|
+
/** Return true to disable selection for a specific row */
|
|
75
|
+
selectDisabledBy?: (row: T, index: number) => boolean;
|
|
74
76
|
|
|
75
77
|
/** Callback when a row is clicked */
|
|
76
78
|
onRowClick?: (row: T, index: number) => void;
|
|
@@ -78,9 +80,28 @@
|
|
|
78
80
|
/** Show loading state (spinner overlay + reduced opacity) */
|
|
79
81
|
loading?: boolean;
|
|
80
82
|
|
|
81
|
-
/** Custom cell renderer snippet */
|
|
83
|
+
/** Custom cell renderer snippet (rendered in both desktop table and mobile card layouts; use `variant` to tell them apart) */
|
|
82
84
|
cell?: Snippet<
|
|
83
|
-
[
|
|
85
|
+
[
|
|
86
|
+
{
|
|
87
|
+
column: DataTableColumn<T>;
|
|
88
|
+
row: T;
|
|
89
|
+
value: any;
|
|
90
|
+
rowIndex: number;
|
|
91
|
+
variant: "desktop" | "mobile";
|
|
92
|
+
},
|
|
93
|
+
]
|
|
94
|
+
>;
|
|
95
|
+
/** Custom desktop row renderer — replaces the entire `<tr>` */
|
|
96
|
+
row?: Snippet<
|
|
97
|
+
[
|
|
98
|
+
{
|
|
99
|
+
row: T;
|
|
100
|
+
columns: DataTableColumn<T>[];
|
|
101
|
+
rowIndex: number;
|
|
102
|
+
isSelected: boolean;
|
|
103
|
+
},
|
|
104
|
+
]
|
|
84
105
|
>;
|
|
85
106
|
/** Batch actions bar snippet (shown when items are selected) */
|
|
86
107
|
batchActions?: Snippet<
|
|
@@ -96,8 +117,6 @@
|
|
|
96
117
|
empty?: Snippet;
|
|
97
118
|
/** Custom mobile row card snippet */
|
|
98
119
|
mobileRow?: Snippet<[{ row: T; columns: DataTableColumn<T>[]; rowIndex: number }]>;
|
|
99
|
-
/** Default children snippet (not used directly) */
|
|
100
|
-
children?: Snippet;
|
|
101
120
|
|
|
102
121
|
/** Optional translate function */
|
|
103
122
|
t?: TranslateFn;
|
|
@@ -130,13 +149,14 @@
|
|
|
130
149
|
selectable = false,
|
|
131
150
|
selected = $bindable(new Set()),
|
|
132
151
|
selectOnRowClick = false,
|
|
152
|
+
selectDisabledBy,
|
|
133
153
|
onRowClick,
|
|
134
154
|
loading = false,
|
|
135
155
|
cell,
|
|
156
|
+
row,
|
|
136
157
|
batchActions,
|
|
137
158
|
empty,
|
|
138
159
|
mobileRow,
|
|
139
|
-
children,
|
|
140
160
|
t = t_default,
|
|
141
161
|
small = false,
|
|
142
162
|
unstyled = false,
|
|
@@ -154,15 +174,21 @@
|
|
|
154
174
|
|
|
155
175
|
// --- Selection ---
|
|
156
176
|
let allRowIds = $derived(data.map((row, i) => getRowId(row, i)));
|
|
177
|
+
let selectableRowIds = $derived.by(() => {
|
|
178
|
+
if (!selectDisabledBy) return allRowIds;
|
|
179
|
+
return data
|
|
180
|
+
.map((row, i) => (selectDisabledBy(row, i) ? null : getRowId(row, i)))
|
|
181
|
+
.filter((id): id is string | number => id !== null);
|
|
182
|
+
});
|
|
157
183
|
|
|
158
184
|
let allOnPageSelected = $derived.by(() => {
|
|
159
|
-
if (!selectable ||
|
|
160
|
-
return
|
|
185
|
+
if (!selectable || selectableRowIds.length === 0) return false;
|
|
186
|
+
return selectableRowIds.every((id) => selected.has(id));
|
|
161
187
|
});
|
|
162
188
|
|
|
163
189
|
let someOnPageSelected = $derived.by(() => {
|
|
164
|
-
if (!selectable ||
|
|
165
|
-
return
|
|
190
|
+
if (!selectable || selectableRowIds.length === 0) return false;
|
|
191
|
+
return selectableRowIds.some((id) => selected.has(id)) && !allOnPageSelected;
|
|
166
192
|
});
|
|
167
193
|
|
|
168
194
|
let selectedRows = $derived.by(() => {
|
|
@@ -173,11 +199,11 @@
|
|
|
173
199
|
function toggleSelectAll() {
|
|
174
200
|
if (allOnPageSelected) {
|
|
175
201
|
const next = new Set(selected);
|
|
176
|
-
for (const id of
|
|
202
|
+
for (const id of selectableRowIds) next.delete(id);
|
|
177
203
|
selected = next;
|
|
178
204
|
} else {
|
|
179
205
|
const next = new Set(selected);
|
|
180
|
-
for (const id of
|
|
206
|
+
for (const id of selectableRowIds) next.add(id);
|
|
181
207
|
selected = next;
|
|
182
208
|
}
|
|
183
209
|
}
|
|
@@ -248,7 +274,7 @@
|
|
|
248
274
|
<thead>
|
|
249
275
|
<tr>
|
|
250
276
|
{#if selectable}
|
|
251
|
-
<th data-checkbox class="stuic-checkbox">
|
|
277
|
+
<th scope="col" data-checkbox class="stuic-checkbox">
|
|
252
278
|
<input
|
|
253
279
|
type="checkbox"
|
|
254
280
|
checked={allOnPageSelected}
|
|
@@ -260,6 +286,7 @@
|
|
|
260
286
|
{/if}
|
|
261
287
|
{#each columns as col (col.key)}
|
|
262
288
|
<th
|
|
289
|
+
scope="col"
|
|
263
290
|
class={col.classHeader}
|
|
264
291
|
data-align={!unstyled && col.align ? col.align : undefined}
|
|
265
292
|
style={col.width ? `width: ${col.width}` : undefined}
|
|
@@ -274,46 +301,53 @@
|
|
|
274
301
|
</tr>
|
|
275
302
|
</thead>
|
|
276
303
|
<tbody>
|
|
277
|
-
{#each data as
|
|
278
|
-
{@const rowId = getRowId(
|
|
304
|
+
{#each data as rowData, rowIndex (getRowId(rowData, rowIndex))}
|
|
305
|
+
{@const rowId = getRowId(rowData, rowIndex)}
|
|
279
306
|
{@const isSelected = selectable && selected.has(rowId)}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
{
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
307
|
+
{@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
|
|
308
|
+
{#if row}
|
|
309
|
+
{@render row({ row: rowData, columns, rowIndex, isSelected })}
|
|
310
|
+
{:else}
|
|
311
|
+
<tr
|
|
312
|
+
data-hoverable={!unstyled ? "true" : undefined}
|
|
313
|
+
data-clickable={!unstyled && (onRowClick || selectOnRowClick)
|
|
314
|
+
? "true"
|
|
315
|
+
: undefined}
|
|
316
|
+
data-selected={!unstyled && isSelected ? "true" : undefined}
|
|
317
|
+
onclick={(e) => handleRowClick(rowData, rowIndex, e)}
|
|
318
|
+
>
|
|
319
|
+
{#if selectable}
|
|
320
|
+
<td data-checkbox class="stuic-checkbox">
|
|
321
|
+
<input
|
|
322
|
+
type="checkbox"
|
|
323
|
+
checked={isSelected}
|
|
324
|
+
disabled={selectDisabled}
|
|
325
|
+
onchange={() => toggleSelectRow(rowId)}
|
|
326
|
+
aria-label={t("select_row")}
|
|
327
|
+
/>
|
|
328
|
+
</td>
|
|
329
|
+
{/if}
|
|
330
|
+
{#each columns as col (col.key)}
|
|
331
|
+
{@const value = getCellValue(rowData, col)}
|
|
332
|
+
<td
|
|
333
|
+
class={col.class}
|
|
334
|
+
data-align={!unstyled && col.align ? col.align : undefined}
|
|
335
|
+
>
|
|
336
|
+
{#if cell}
|
|
337
|
+
{@render cell({
|
|
338
|
+
column: col,
|
|
339
|
+
row: rowData,
|
|
340
|
+
value,
|
|
341
|
+
rowIndex,
|
|
342
|
+
variant: "desktop",
|
|
343
|
+
})}
|
|
344
|
+
{:else}
|
|
345
|
+
{getCellDisplay(rowData, col)}
|
|
346
|
+
{/if}
|
|
347
|
+
</td>
|
|
348
|
+
{/each}
|
|
349
|
+
</tr>
|
|
350
|
+
{/if}
|
|
317
351
|
{:else}
|
|
318
352
|
<tr>
|
|
319
353
|
<td
|
|
@@ -337,12 +371,13 @@
|
|
|
337
371
|
class={!unstyled ? "stuic-data-table-cards" : undefined}
|
|
338
372
|
data-loading={!unstyled && loading ? "true" : undefined}
|
|
339
373
|
>
|
|
340
|
-
{#each data as
|
|
341
|
-
{@const rowId = getRowId(
|
|
374
|
+
{#each data as rowData, rowIndex (getRowId(rowData, rowIndex))}
|
|
375
|
+
{@const rowId = getRowId(rowData, rowIndex)}
|
|
342
376
|
{@const isSelected = selectable && selected.has(rowId)}
|
|
377
|
+
{@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
|
|
343
378
|
{#if mobileRow}
|
|
344
379
|
{@render mobileRow({
|
|
345
|
-
row,
|
|
380
|
+
row: rowData,
|
|
346
381
|
columns: mobileColumns,
|
|
347
382
|
rowIndex,
|
|
348
383
|
})}
|
|
@@ -357,7 +392,7 @@
|
|
|
357
392
|
data-selected={!unstyled && isSelected ? "true" : undefined}
|
|
358
393
|
role={onRowClick || selectOnRowClick ? "button" : undefined}
|
|
359
394
|
tabindex={onRowClick || selectOnRowClick ? 0 : undefined}
|
|
360
|
-
onclick={(e) => handleRowClick(
|
|
395
|
+
onclick={(e) => handleRowClick(rowData, rowIndex, e)}
|
|
361
396
|
onkeydown={(e) => {
|
|
362
397
|
if (
|
|
363
398
|
(onRowClick || selectOnRowClick) &&
|
|
@@ -367,22 +402,23 @@
|
|
|
367
402
|
if (selectable && selectOnRowClick) {
|
|
368
403
|
toggleSelectRow(rowId);
|
|
369
404
|
}
|
|
370
|
-
onRowClick?.(
|
|
405
|
+
onRowClick?.(rowData, rowIndex);
|
|
371
406
|
}
|
|
372
407
|
}}
|
|
373
408
|
>
|
|
374
409
|
{#if selectable}
|
|
375
|
-
<div class="stuic-checkbox
|
|
410
|
+
<div class={!unstyled ? "stuic-checkbox stuic-data-table-card-checkbox" : undefined}>
|
|
376
411
|
<input
|
|
377
412
|
type="checkbox"
|
|
378
413
|
checked={isSelected}
|
|
414
|
+
disabled={selectDisabled}
|
|
379
415
|
onchange={() => toggleSelectRow(rowId)}
|
|
380
416
|
aria-label={t("select_row")}
|
|
381
417
|
/>
|
|
382
418
|
</div>
|
|
383
419
|
{/if}
|
|
384
420
|
{#each mobileColumns as col (col.key)}
|
|
385
|
-
{@const value = getCellValue(
|
|
421
|
+
{@const value = getCellValue(rowData, col)}
|
|
386
422
|
<div class={!unstyled ? "stuic-data-table-card-row" : undefined}>
|
|
387
423
|
<span class={!unstyled ? "stuic-data-table-card-label" : undefined}>
|
|
388
424
|
{#if isTHCNotEmpty(col.label)}
|
|
@@ -395,12 +431,13 @@
|
|
|
395
431
|
{#if cell}
|
|
396
432
|
{@render cell({
|
|
397
433
|
column: col,
|
|
398
|
-
row,
|
|
434
|
+
row: rowData,
|
|
399
435
|
value,
|
|
400
436
|
rowIndex,
|
|
437
|
+
variant: "mobile",
|
|
401
438
|
})}
|
|
402
439
|
{:else}
|
|
403
|
-
{getCellDisplay(
|
|
440
|
+
{getCellDisplay(rowData, col)}
|
|
404
441
|
{/if}
|
|
405
442
|
</span>
|
|
406
443
|
</div>
|
|
@@ -412,7 +449,7 @@
|
|
|
412
449
|
{#if empty}
|
|
413
450
|
{@render empty()}
|
|
414
451
|
{:else}
|
|
415
|
-
|
|
452
|
+
{t("no_data")}
|
|
416
453
|
{/if}
|
|
417
454
|
</div>
|
|
418
455
|
{/each}
|
|
@@ -38,17 +38,29 @@ 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;
|
|
41
43
|
/** Callback when a row is clicked */
|
|
42
44
|
onRowClick?: (row: T, index: number) => void;
|
|
43
45
|
/** Show loading state (spinner overlay + reduced opacity) */
|
|
44
46
|
loading?: boolean;
|
|
45
|
-
/** Custom cell renderer snippet */
|
|
47
|
+
/** Custom cell renderer snippet (rendered in both desktop table and mobile card layouts; use `variant` to tell them apart) */
|
|
46
48
|
cell?: Snippet<[
|
|
47
49
|
{
|
|
48
50
|
column: DataTableColumn<T>;
|
|
49
51
|
row: T;
|
|
50
52
|
value: any;
|
|
51
53
|
rowIndex: number;
|
|
54
|
+
variant: "desktop" | "mobile";
|
|
55
|
+
}
|
|
56
|
+
]>;
|
|
57
|
+
/** Custom desktop row renderer — replaces the entire `<tr>` */
|
|
58
|
+
row?: Snippet<[
|
|
59
|
+
{
|
|
60
|
+
row: T;
|
|
61
|
+
columns: DataTableColumn<T>[];
|
|
62
|
+
rowIndex: number;
|
|
63
|
+
isSelected: boolean;
|
|
52
64
|
}
|
|
53
65
|
]>;
|
|
54
66
|
/** Batch actions bar snippet (shown when items are selected) */
|
|
@@ -67,8 +79,6 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
|
|
|
67
79
|
columns: DataTableColumn<T>[];
|
|
68
80
|
rowIndex: number;
|
|
69
81
|
}]>;
|
|
70
|
-
/** Default children snippet (not used directly) */
|
|
71
|
-
children?: Snippet;
|
|
72
82
|
/** Optional translate function */
|
|
73
83
|
t?: TranslateFn;
|
|
74
84
|
/** Force mobile/card layout regardless of breakpoint */
|
|
@@ -28,14 +28,21 @@ loading state, and mobile card layout.
|
|
|
28
28
|
|
|
29
29
|
### With Paging
|
|
30
30
|
|
|
31
|
+
DataTable integrates with [`@marianmeres/paging-store`](https://github.com/marianmeres/paging-store). Pass its computed result and receive offsets back via `onPageChange`:
|
|
32
|
+
|
|
31
33
|
```svelte
|
|
34
|
+
<script lang="ts">
|
|
35
|
+
import { createPagingStore } from "@marianmeres/paging-store";
|
|
36
|
+
|
|
37
|
+
const paging = createPagingStore({ pageSize: 20, totalItems: 100 });
|
|
38
|
+
$: pagingResult = $paging; // or via $derived in Svelte 5 rune mode
|
|
39
|
+
</script>
|
|
40
|
+
|
|
32
41
|
<DataTable
|
|
33
42
|
{columns}
|
|
34
43
|
{data}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
totalItems={100}
|
|
38
|
-
onPageChange={(p) => fetchPage(p)}
|
|
44
|
+
paging={pagingResult}
|
|
45
|
+
onPageChange={(offset) => paging.setOffset(offset)}
|
|
39
46
|
/>
|
|
40
47
|
```
|
|
41
48
|
|
|
@@ -57,11 +64,15 @@ loading state, and mobile card layout.
|
|
|
57
64
|
|
|
58
65
|
### Custom Cell Rendering
|
|
59
66
|
|
|
67
|
+
The `cell` snippet is used for both desktop and mobile layouts. Use the `variant` param if rendering differs per layout.
|
|
68
|
+
|
|
60
69
|
```svelte
|
|
61
70
|
<DataTable {columns} {data}>
|
|
62
|
-
{#snippet cell({ column, row, value })}
|
|
71
|
+
{#snippet cell({ column, row, value, rowIndex, variant })}
|
|
63
72
|
{#if column.key === "status"}
|
|
64
73
|
<span class="badge">{value}</span>
|
|
74
|
+
{:else if variant === "mobile"}
|
|
75
|
+
<em>{value}</em>
|
|
65
76
|
{:else}
|
|
66
77
|
{value}
|
|
67
78
|
{/if}
|
|
@@ -69,6 +80,34 @@ loading state, and mobile card layout.
|
|
|
69
80
|
</DataTable>
|
|
70
81
|
```
|
|
71
82
|
|
|
83
|
+
### Custom Desktop Row
|
|
84
|
+
|
|
85
|
+
Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable does not render the default row markup (checkbox, cells) — you own it all. Use for custom expandable rows, row grouping, etc.
|
|
86
|
+
|
|
87
|
+
```svelte
|
|
88
|
+
<DataTable {columns} {data}>
|
|
89
|
+
{#snippet row({ row, columns, rowIndex, isSelected })}
|
|
90
|
+
<tr data-custom-row>
|
|
91
|
+
{#each columns as col}
|
|
92
|
+
<td>{row[col.key]}</td>
|
|
93
|
+
{/each}
|
|
94
|
+
</tr>
|
|
95
|
+
{/snippet}
|
|
96
|
+
</DataTable>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Disabling Selection Per Row
|
|
100
|
+
|
|
101
|
+
```svelte
|
|
102
|
+
<DataTable
|
|
103
|
+
{columns}
|
|
104
|
+
{data}
|
|
105
|
+
selectable
|
|
106
|
+
bind:selected
|
|
107
|
+
selectDisabledBy={(row) => row.locked === true}
|
|
108
|
+
/>
|
|
109
|
+
```
|
|
110
|
+
|
|
72
111
|
### Custom Mobile Layout
|
|
73
112
|
|
|
74
113
|
```svelte
|
|
@@ -84,26 +123,41 @@ loading state, and mobile card layout.
|
|
|
84
123
|
|
|
85
124
|
## Props
|
|
86
125
|
|
|
87
|
-
| Prop
|
|
88
|
-
|
|
|
89
|
-
| `columns`
|
|
90
|
-
| `data`
|
|
91
|
-
| `getRowId`
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `onRowClick`
|
|
99
|
-
| `loading`
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
126
|
+
| Prop | Type | Default | Description |
|
|
127
|
+
| ------------------- | ------------------------------------------ | ------------- | ----------------------------------------------------------------- |
|
|
128
|
+
| `columns` | `DataTableColumn<T>[]` | required | Column definitions |
|
|
129
|
+
| `data` | `T[]` | required | Array of row data |
|
|
130
|
+
| `getRowId` | `(row, index) => string \| number` | `(_, i) => i` | Row ID extractor |
|
|
131
|
+
| `paging` | `PagingCalcResult` | - | Paging state (from `@marianmeres/paging-store`) |
|
|
132
|
+
| `onPageChange` | `(offset: number) => void` | - | Called with the new offset when the user navigates pages |
|
|
133
|
+
| `selectable` | `boolean` | `false` | Enable selection checkboxes |
|
|
134
|
+
| `selected` | `Set<string \| number>` | `new Set()` | Selected row IDs (bindable) |
|
|
135
|
+
| `selectOnRowClick` | `boolean` | `false` | Clicking anywhere on a row toggles its selection |
|
|
136
|
+
| `selectDisabledBy` | `(row, index) => boolean` | - | Return `true` to disable selection for a specific row |
|
|
137
|
+
| `onRowClick` | `(row, index) => void` | - | Row click callback |
|
|
138
|
+
| `loading` | `boolean` | `false` | Show loading overlay |
|
|
139
|
+
| `small` | `boolean` | `false` | Force mobile/card layout regardless of viewport |
|
|
140
|
+
| `t` | `TranslateFn` | built-in | Optional translation function |
|
|
141
|
+
| `cell` | `Snippet` | - | Custom cell renderer (desktop + mobile) |
|
|
142
|
+
| `row` | `Snippet` | - | Custom desktop `<tr>` renderer (overrides default row) |
|
|
143
|
+
| `mobileRow` | `Snippet` | - | Custom mobile card renderer |
|
|
144
|
+
| `batchActions` | `Snippet` | - | Batch action bar content |
|
|
145
|
+
| `empty` | `Snippet` | - | Custom empty state |
|
|
146
|
+
| `unstyled` | `boolean` | `false` | Skip default styling |
|
|
147
|
+
| `class` | `string` | - | Additional CSS classes |
|
|
148
|
+
| `el` | `HTMLDivElement` | - | Bindable element ref |
|
|
149
|
+
|
|
150
|
+
### Snippet signatures
|
|
151
|
+
|
|
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` | — |
|
|
159
|
+
|
|
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".
|
|
107
161
|
|
|
108
162
|
## DataTableColumn
|
|
109
163
|
|
|
@@ -278,6 +278,13 @@
|
|
|
278
278
|
background: var(--stuic-data-table-card-bg-selected);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
.stuic-data-table-card-checkbox {
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
gap: 0.5rem;
|
|
285
|
+
margin-bottom: 0.25rem;
|
|
286
|
+
}
|
|
287
|
+
|
|
281
288
|
.stuic-data-table-card-row {
|
|
282
289
|
display: flex;
|
|
283
290
|
justify-content: space-between;
|
|
@@ -201,6 +201,10 @@
|
|
|
201
201
|
triggerEl?: HTMLButtonElement;
|
|
202
202
|
/** Reference to dropdown element */
|
|
203
203
|
dropdownEl?: HTMLDivElement;
|
|
204
|
+
/** Reference to the wrapper element */
|
|
205
|
+
el?: HTMLDivElement;
|
|
206
|
+
/** Opt out of stuic base classes for full styling control */
|
|
207
|
+
unstyled?: boolean;
|
|
204
208
|
/** Optional, used only when css positioning not supported (iPhone)*/
|
|
205
209
|
noScrollLock?: boolean;
|
|
206
210
|
}
|
|
@@ -298,6 +302,8 @@
|
|
|
298
302
|
onSelect,
|
|
299
303
|
triggerEl = $bindable(),
|
|
300
304
|
dropdownEl = $bindable(),
|
|
305
|
+
el = $bindable(),
|
|
306
|
+
unstyled = false,
|
|
301
307
|
scrollbarGutter,
|
|
302
308
|
noScrollLock,
|
|
303
309
|
...rest
|
|
@@ -309,7 +315,6 @@
|
|
|
309
315
|
const anchorName = `--dropdown-anchor-${triggerId}`;
|
|
310
316
|
|
|
311
317
|
// State
|
|
312
|
-
let wrapperEl: HTMLDivElement = $state()!;
|
|
313
318
|
let activeItemEl: HTMLButtonElement | undefined = $state();
|
|
314
319
|
const reducedMotion = prefersReducedMotion();
|
|
315
320
|
|
|
@@ -615,7 +620,7 @@
|
|
|
615
620
|
|
|
616
621
|
// Click outside handler — only active when open (prevents stale refs on destroy)
|
|
617
622
|
const _clickOutside = onClickOutside(
|
|
618
|
-
() =>
|
|
623
|
+
() => el,
|
|
619
624
|
() => {
|
|
620
625
|
if (closeOnClickOutside && isOpen) {
|
|
621
626
|
isOpen = false;
|
|
@@ -626,7 +631,7 @@
|
|
|
626
631
|
);
|
|
627
632
|
|
|
628
633
|
$effect(() => {
|
|
629
|
-
if (isOpen &&
|
|
634
|
+
if (isOpen && el) _clickOutside.start();
|
|
630
635
|
else _clickOutside.stop();
|
|
631
636
|
});
|
|
632
637
|
|
|
@@ -808,8 +813,8 @@
|
|
|
808
813
|
/>
|
|
809
814
|
|
|
810
815
|
<div
|
|
811
|
-
bind:this={
|
|
812
|
-
class={twMerge(DROPDOWN_MENU_BASE_CLASSES, classProp)}
|
|
816
|
+
bind:this={el}
|
|
817
|
+
class={unstyled ? classProp : twMerge(DROPDOWN_MENU_BASE_CLASSES, classProp)}
|
|
813
818
|
style:anchor-name={isSupported ? anchorName : undefined}
|
|
814
819
|
>
|
|
815
820
|
<!-- Trigger -->
|
|
@@ -830,7 +835,7 @@
|
|
|
830
835
|
<button
|
|
831
836
|
bind:this={triggerEl}
|
|
832
837
|
id={triggerId}
|
|
833
|
-
class={twMerge(DROPDOWN_MENU_TRIGGER_CLASSES, classTrigger)}
|
|
838
|
+
class={unstyled ? classTrigger : twMerge(DROPDOWN_MENU_TRIGGER_CLASSES, classTrigger)}
|
|
834
839
|
onclick={() => (isOpen = !isOpen)}
|
|
835
840
|
aria-haspopup="menu"
|
|
836
841
|
aria-expanded={isOpen}
|
|
@@ -851,15 +856,15 @@
|
|
|
851
856
|
|
|
852
857
|
<!-- Backdrop (fallback mode only) -->
|
|
853
858
|
{#if isOpen && !isSupported && showBackdrop}
|
|
859
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
854
860
|
<div
|
|
855
|
-
class={twMerge(DROPDOWN_MENU_BACKDROP_CLASSES, classBackdrop)}
|
|
861
|
+
class={unstyled ? classBackdrop : twMerge(DROPDOWN_MENU_BACKDROP_CLASSES, classBackdrop)}
|
|
856
862
|
onclick={() => {
|
|
857
863
|
if (closeOnClickOutside) {
|
|
858
864
|
isOpen = false;
|
|
859
865
|
triggerEl?.focus();
|
|
860
866
|
}
|
|
861
867
|
}}
|
|
862
|
-
onkeydown={() => {}}
|
|
863
868
|
role="presentation"
|
|
864
869
|
transition:fade={{ duration: transitionDuration }}
|
|
865
870
|
></div>
|
|
@@ -872,11 +877,13 @@
|
|
|
872
877
|
id={dropdownId}
|
|
873
878
|
role="menu"
|
|
874
879
|
aria-labelledby={triggerId}
|
|
875
|
-
class={
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
+
class={unstyled
|
|
881
|
+
? twMerge(!isSupported && "w-4/5 max-w-sm", classDropdown)
|
|
882
|
+
: twMerge(
|
|
883
|
+
DROPDOWN_MENU_DROPDOWN_CLASSES,
|
|
884
|
+
!isSupported && "w-4/5 max-w-sm",
|
|
885
|
+
classDropdown
|
|
886
|
+
)}
|
|
880
887
|
style={dropdownStyle}
|
|
881
888
|
transition:slide={{ duration: transitionDuration }}
|
|
882
889
|
>
|
|
@@ -886,7 +893,9 @@
|
|
|
886
893
|
<button
|
|
887
894
|
type="button"
|
|
888
895
|
aria-label="Close"
|
|
889
|
-
class=
|
|
896
|
+
class={unstyled
|
|
897
|
+
? "absolute right-0 top-0 pointer-events-auto"
|
|
898
|
+
: "stuic-close-button absolute right-0 top-0 pointer-events-auto"}
|
|
890
899
|
onclick={() => {
|
|
891
900
|
isOpen = false;
|
|
892
901
|
triggerEl?.focus();
|
|
@@ -957,12 +966,16 @@
|
|
|
957
966
|
{:else if item.type === "divider"}
|
|
958
967
|
<div
|
|
959
968
|
role="separator"
|
|
960
|
-
class={
|
|
969
|
+
class={unstyled
|
|
970
|
+
? twMerge(classDivider, item.class)
|
|
971
|
+
: twMerge(DROPDOWN_MENU_DIVIDER_CLASSES, classDivider, item.class)}
|
|
961
972
|
></div>
|
|
962
973
|
{:else if item.type === "header"}
|
|
963
974
|
<div
|
|
964
975
|
role="presentation"
|
|
965
|
-
class={
|
|
976
|
+
class={unstyled
|
|
977
|
+
? twMerge(classHeader, item.class)
|
|
978
|
+
: twMerge(DROPDOWN_MENU_HEADER_CLASSES, classHeader, item.class)}
|
|
966
979
|
>
|
|
967
980
|
<Thc thc={item.label} />
|
|
968
981
|
</div>
|
|
@@ -1043,20 +1056,24 @@
|
|
|
1043
1056
|
{#if childItem.type === "divider"}
|
|
1044
1057
|
<div
|
|
1045
1058
|
role="separator"
|
|
1046
|
-
class={
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1059
|
+
class={unstyled
|
|
1060
|
+
? twMerge(classDivider, childItem.class)
|
|
1061
|
+
: twMerge(
|
|
1062
|
+
DROPDOWN_MENU_DIVIDER_CLASSES,
|
|
1063
|
+
classDivider,
|
|
1064
|
+
childItem.class
|
|
1065
|
+
)}
|
|
1051
1066
|
></div>
|
|
1052
1067
|
{:else if childItem.type === "header"}
|
|
1053
1068
|
<div
|
|
1054
1069
|
role="presentation"
|
|
1055
|
-
class={
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1070
|
+
class={unstyled
|
|
1071
|
+
? twMerge(classHeader, childItem.class)
|
|
1072
|
+
: twMerge(
|
|
1073
|
+
DROPDOWN_MENU_HEADER_CLASSES,
|
|
1074
|
+
classHeader,
|
|
1075
|
+
childItem.class
|
|
1076
|
+
)}
|
|
1060
1077
|
>
|
|
1061
1078
|
<Thc thc={childItem.label} />
|
|
1062
1079
|
</div>
|