@juspay/svelte-ui-components 2.36.0 → 2.38.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/Table/Table.svelte +454 -24
- package/dist/Table/properties.d.ts +88 -0
- package/dist/Toast/Toast.svelte +25 -13
- package/dist/Toast/properties.d.ts +5 -2
- package/package.json +1 -1
package/dist/Table/Table.svelte
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { TableProperties } from './properties';
|
|
2
|
+
import type { TableProperties, TableCheckboxSelectionConfig } from './properties';
|
|
3
3
|
import type { JSONValue } from 'type-decoder';
|
|
4
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
4
5
|
import Button from '../Button/Button.svelte';
|
|
5
6
|
import chevronUpSvg from '../assets/chevron-up.svg?raw';
|
|
6
7
|
import chevronDownSvg from '../assets/chevron-down.svg?raw';
|
|
7
8
|
import sortDefaultSvg from '../assets/sort-default.svg?raw';
|
|
9
|
+
import searchSvg from '../assets/search.svg?raw';
|
|
10
|
+
import closeSvg from '../assets/close.svg?raw';
|
|
11
|
+
import checkmarkSvg from '../assets/checkmark.svg?raw';
|
|
12
|
+
import minusSvg from '../assets/minus.svg?raw';
|
|
8
13
|
|
|
9
14
|
let {
|
|
10
15
|
tableTitle = '',
|
|
@@ -24,16 +29,21 @@
|
|
|
24
29
|
empty,
|
|
25
30
|
onRowClick,
|
|
26
31
|
onSort,
|
|
32
|
+
onCellChange: _onCellChange,
|
|
27
33
|
classes,
|
|
28
34
|
paginatorSlot,
|
|
29
35
|
getRowTestId,
|
|
30
|
-
getCellTestId
|
|
36
|
+
getCellTestId,
|
|
37
|
+
checkboxSelection,
|
|
38
|
+
searchConfig,
|
|
39
|
+
onSearchChange
|
|
31
40
|
}: TableProperties = $props();
|
|
32
41
|
|
|
42
|
+
// ─── Sort state ──────────────────────────────────────────────────────────────
|
|
33
43
|
let sortColumn = $state<number | null>(null);
|
|
34
44
|
let sortDirection = $state<'asc' | 'desc'>('asc');
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
const isColumnSortable = (colIndex: number): boolean => {
|
|
37
47
|
if (!sortable) {
|
|
38
48
|
return false;
|
|
39
49
|
}
|
|
@@ -41,8 +51,61 @@
|
|
|
41
51
|
return sortableColumns.includes(colIndex);
|
|
42
52
|
}
|
|
43
53
|
return true;
|
|
44
|
-
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleSort = (colIndex: number): void => {
|
|
57
|
+
if (!isColumnSortable(colIndex)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (sortColumn === colIndex) {
|
|
62
|
+
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
|
63
|
+
} else {
|
|
64
|
+
sortColumn = colIndex;
|
|
65
|
+
sortDirection = 'asc';
|
|
66
|
+
}
|
|
67
|
+
onSort?.(colIndex, sortDirection);
|
|
68
|
+
};
|
|
45
69
|
|
|
70
|
+
// ─── Row click ───────────────────────────────────────────────────────────────
|
|
71
|
+
const handleRowClick = (rowIndex: number, rowData: JSONValue[]): void => {
|
|
72
|
+
onRowClick?.(rowIndex, rowData);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleRowKeydown = (event: KeyboardEvent, rowIndex: number, rowData: JSONValue[]): void => {
|
|
76
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
onRowClick?.(rowIndex, rowData);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let isRowClickable = $derived(typeof onRowClick === 'function');
|
|
83
|
+
let isStickyHeader = $derived(stickyHeader || isTableScrollable);
|
|
84
|
+
|
|
85
|
+
// ─── C2-3: Search ─────────────────────────────────────────────────────────
|
|
86
|
+
let searchTerm = $state('');
|
|
87
|
+
let hasSearchConfig = $derived(!!searchConfig);
|
|
88
|
+
let isServerSearch = $derived(typeof onSearchChange === 'function');
|
|
89
|
+
let searchInputRef = $state<HTMLInputElement | null>(null);
|
|
90
|
+
|
|
91
|
+
const handleSearchInput = (): void => {
|
|
92
|
+
if (!searchInputRef) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
searchTerm = searchInputRef.value;
|
|
96
|
+
if (isServerSearch) {
|
|
97
|
+
onSearchChange?.(searchTerm);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const clearSearch = (): void => {
|
|
102
|
+
searchTerm = '';
|
|
103
|
+
if (isServerSearch) {
|
|
104
|
+
onSearchChange?.('');
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// ─── Sort + Search pipeline ──────────────────────────────────────────────────
|
|
46
109
|
let sortedTableData = $derived.by(() => {
|
|
47
110
|
if (sortColumn === null) {
|
|
48
111
|
return [...tableData];
|
|
@@ -76,33 +139,158 @@
|
|
|
76
139
|
});
|
|
77
140
|
});
|
|
78
141
|
|
|
79
|
-
|
|
80
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Client-side filtered rows. When `onSearchChange` is provided (server
|
|
144
|
+
* delegation) or `searchConfig` is absent, the sorted data passes through
|
|
145
|
+
* untouched.
|
|
146
|
+
*/
|
|
147
|
+
let filteredTableData = $derived.by(() => {
|
|
148
|
+
if (!hasSearchConfig || isServerSearch || searchTerm.trim() === '') {
|
|
149
|
+
return sortedTableData;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const term = searchTerm.trim().toLowerCase();
|
|
153
|
+
const colIndices = searchConfig?.searchableColumnIndices;
|
|
154
|
+
|
|
155
|
+
return sortedTableData.filter((row) => {
|
|
156
|
+
const indicesToSearch = colIndices ? colIndices : row.map((_cell, idx) => idx);
|
|
157
|
+
return indicesToSearch.some((idx) => {
|
|
158
|
+
const cellValue = row[idx];
|
|
159
|
+
return cellValue !== null && String(cellValue).toLowerCase().includes(term);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ─── C2-1: Checkbox selection ─────────────────────────────────────────────
|
|
165
|
+
const resolveRowId = (
|
|
166
|
+
cfg: TableCheckboxSelectionConfig,
|
|
167
|
+
row: JSONValue[],
|
|
168
|
+
rowIndex: number
|
|
169
|
+
): string => {
|
|
170
|
+
return cfg.getRowId ? cfg.getRowId(row, rowIndex) : String(rowIndex);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let selectedIds = new SvelteSet<string>();
|
|
174
|
+
|
|
175
|
+
const isRowDisabled = (rowId: string): boolean => {
|
|
176
|
+
return checkboxSelection?.disabledRowIds?.has(rowId) ?? false;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const isRowSelected = (rowId: string): boolean => {
|
|
180
|
+
return selectedIds.has(rowId);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Stable string ID for every row in the currently visible (filtered) view.
|
|
185
|
+
*
|
|
186
|
+
* Each entry is resolved by iterating `filteredTableData` and looking up the
|
|
187
|
+
* row's pre-sort position via `sortedTableData.indexOf(row)`. Using the
|
|
188
|
+
* pre-filter index as the default numeric ID keeps `String(originalIndex)`
|
|
189
|
+
* stable even when the visible set shrinks or expands as the search term
|
|
190
|
+
* changes — so a row that was "row 2" in the full set keeps ID `"2"` even
|
|
191
|
+
* when it becomes the only visible result.
|
|
192
|
+
*/
|
|
193
|
+
let filteredRowIds = $derived.by((): string[] => {
|
|
194
|
+
return filteredTableData.map((row, fallbackIndex) => {
|
|
195
|
+
const originalIndex = sortedTableData.indexOf(row);
|
|
196
|
+
const stableIndex = originalIndex === -1 ? fallbackIndex : originalIndex;
|
|
197
|
+
if (checkboxSelection && checkboxSelection.enabled !== false) {
|
|
198
|
+
return resolveRowId(checkboxSelection, row, stableIndex);
|
|
199
|
+
}
|
|
200
|
+
return String(stableIndex);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* IDs of all selectable (non-disabled) rows in the currently filtered view.
|
|
206
|
+
*
|
|
207
|
+
* Iterates `filteredTableData` (the post-filter, post-sort visible rows) and
|
|
208
|
+
* resolves each row's stable ID using `sortedTableData.indexOf(row)` for the
|
|
209
|
+
* pre-filter positional index. When `getRowId` is absent this keeps the
|
|
210
|
+
* default numeric ID (`String(originalIndex)`) unchanged regardless of which
|
|
211
|
+
* rows the current search term hides, preventing ID collisions as the visible
|
|
212
|
+
* set changes.
|
|
213
|
+
*/
|
|
214
|
+
let selectableRowIds = $derived.by((): string[] => {
|
|
215
|
+
if (!checkboxSelection || checkboxSelection.enabled === false) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
return filteredRowIds.filter((rowId) => !isRowDisabled(rowId));
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Header checkbox tri-state.
|
|
223
|
+
* - `'all'` → every selectable row is selected → show checked
|
|
224
|
+
* - `'some'` → some but not all → show indeterminate
|
|
225
|
+
* - `'none'` → nothing selected → show unchecked
|
|
226
|
+
*/
|
|
227
|
+
let headerCheckboxState = $derived.by((): 'all' | 'some' | 'none' => {
|
|
228
|
+
if (
|
|
229
|
+
!checkboxSelection ||
|
|
230
|
+
checkboxSelection.enabled === false ||
|
|
231
|
+
selectableRowIds.length === 0
|
|
232
|
+
) {
|
|
233
|
+
return 'none';
|
|
234
|
+
}
|
|
235
|
+
const selectedCount = selectableRowIds.filter((rowId) => selectedIds.has(rowId)).length;
|
|
236
|
+
if (selectedCount === 0) {
|
|
237
|
+
return 'none';
|
|
238
|
+
}
|
|
239
|
+
if (selectedCount === selectableRowIds.length) {
|
|
240
|
+
return 'all';
|
|
241
|
+
}
|
|
242
|
+
return 'some';
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const toggleRowSelection = (rowId: string): void => {
|
|
246
|
+
if (!checkboxSelection || checkboxSelection.enabled === false || isRowDisabled(rowId)) {
|
|
81
247
|
return;
|
|
82
248
|
}
|
|
83
249
|
|
|
84
|
-
if (
|
|
85
|
-
|
|
250
|
+
if (checkboxSelection.selectionMode === 'single') {
|
|
251
|
+
const wasSelected = selectedIds.has(rowId);
|
|
252
|
+
selectedIds.clear();
|
|
253
|
+
if (!wasSelected) {
|
|
254
|
+
selectedIds.add(rowId);
|
|
255
|
+
}
|
|
86
256
|
} else {
|
|
87
|
-
|
|
88
|
-
|
|
257
|
+
if (selectedIds.has(rowId)) {
|
|
258
|
+
selectedIds.delete(rowId);
|
|
259
|
+
} else {
|
|
260
|
+
selectedIds.add(rowId);
|
|
261
|
+
}
|
|
89
262
|
}
|
|
90
|
-
|
|
91
|
-
}
|
|
263
|
+
checkboxSelection.onSelectionChange?.(new Set(selectedIds));
|
|
264
|
+
};
|
|
92
265
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
266
|
+
const toggleAllSelection = (): void => {
|
|
267
|
+
if (!checkboxSelection || checkboxSelection.enabled === false) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// selectableRowIds already excludes disabled rows
|
|
271
|
+
if (headerCheckboxState === 'all') {
|
|
272
|
+
// deselect all selectable rows in the current view
|
|
273
|
+
for (const rowId of selectableRowIds) {
|
|
274
|
+
selectedIds.delete(rowId);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
// select all selectable rows in the current view
|
|
278
|
+
for (const rowId of selectableRowIds) {
|
|
279
|
+
selectedIds.add(rowId);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
checkboxSelection.onSelectionChange?.(new Set(selectedIds));
|
|
283
|
+
};
|
|
96
284
|
|
|
97
|
-
|
|
98
|
-
if (event.key === '
|
|
285
|
+
const handleCheckboxKeydown = (event: KeyboardEvent, action: () => void): void => {
|
|
286
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
99
287
|
event.preventDefault();
|
|
100
|
-
|
|
288
|
+
action();
|
|
101
289
|
}
|
|
102
|
-
}
|
|
290
|
+
};
|
|
103
291
|
|
|
104
|
-
let
|
|
105
|
-
let
|
|
292
|
+
let isCheckboxMode = $derived(!!checkboxSelection && checkboxSelection.enabled !== false);
|
|
293
|
+
let isSingleSelect = $derived(checkboxSelection?.selectionMode === 'single');
|
|
106
294
|
</script>
|
|
107
295
|
|
|
108
296
|
{#if typeof tableTitle === 'string' && tableTitle.length > 0}
|
|
@@ -110,6 +298,37 @@
|
|
|
110
298
|
{tableTitle}
|
|
111
299
|
</div>
|
|
112
300
|
{/if}
|
|
301
|
+
|
|
302
|
+
{#if hasSearchConfig}
|
|
303
|
+
<div class="table-search">
|
|
304
|
+
<span class="table-search-icon">
|
|
305
|
+
<!-- eslint-disable svelte/no-at-html-tags -->
|
|
306
|
+
{@html searchSvg}
|
|
307
|
+
</span>
|
|
308
|
+
<input
|
|
309
|
+
bind:this={searchInputRef}
|
|
310
|
+
class="table-search-input"
|
|
311
|
+
type="search"
|
|
312
|
+
placeholder={searchConfig?.placeholder ?? 'Search…'}
|
|
313
|
+
value={searchTerm}
|
|
314
|
+
oninput={handleSearchInput}
|
|
315
|
+
data-pw={searchConfig?.testId ?? null}
|
|
316
|
+
aria-label={searchConfig?.placeholder ?? 'Search'}
|
|
317
|
+
/>
|
|
318
|
+
{#if searchTerm.length > 0}
|
|
319
|
+
<button
|
|
320
|
+
class="table-search-clear"
|
|
321
|
+
type="button"
|
|
322
|
+
onclick={clearSearch}
|
|
323
|
+
aria-label="Clear search"
|
|
324
|
+
>
|
|
325
|
+
<!-- eslint-disable svelte/no-at-html-tags -->
|
|
326
|
+
{@html closeSvg}
|
|
327
|
+
</button>
|
|
328
|
+
{/if}
|
|
329
|
+
</div>
|
|
330
|
+
{/if}
|
|
331
|
+
|
|
113
332
|
{#if tableHeaders.length !== 0 || tableData.length !== 0}
|
|
114
333
|
<div
|
|
115
334
|
class="table-container {isTableScrollable ? 'scrollable-table' : ''} {classes ?? ''}"
|
|
@@ -121,6 +340,37 @@
|
|
|
121
340
|
{/if}
|
|
122
341
|
<thead>
|
|
123
342
|
<tr>
|
|
343
|
+
{#if isCheckboxMode && !isSingleSelect}
|
|
344
|
+
<th class="table-header table-checkbox-col" class:table-header-sticky={isStickyHeader}>
|
|
345
|
+
<!-- Header tri-state checkbox -->
|
|
346
|
+
<span
|
|
347
|
+
class="table-checkbox-box"
|
|
348
|
+
class:checked={headerCheckboxState === 'all'}
|
|
349
|
+
class:indeterminate={headerCheckboxState === 'some'}
|
|
350
|
+
role="checkbox"
|
|
351
|
+
tabindex={0}
|
|
352
|
+
aria-checked={headerCheckboxState === 'some'
|
|
353
|
+
? 'mixed'
|
|
354
|
+
: headerCheckboxState === 'all'}
|
|
355
|
+
aria-label="Select all rows"
|
|
356
|
+
onclick={toggleAllSelection}
|
|
357
|
+
onkeydown={(keyboardEvent) =>
|
|
358
|
+
handleCheckboxKeydown(keyboardEvent, toggleAllSelection)}
|
|
359
|
+
>
|
|
360
|
+
{#if headerCheckboxState === 'all'}
|
|
361
|
+
<!-- eslint-disable svelte/no-at-html-tags -->
|
|
362
|
+
<span class="table-checkbox-icon">{@html checkmarkSvg}</span>
|
|
363
|
+
{:else if headerCheckboxState === 'some'}
|
|
364
|
+
<!-- eslint-disable svelte/no-at-html-tags -->
|
|
365
|
+
<span class="table-checkbox-icon">{@html minusSvg}</span>
|
|
366
|
+
{/if}
|
|
367
|
+
</span>
|
|
368
|
+
</th>
|
|
369
|
+
{:else if isCheckboxMode && isSingleSelect}
|
|
370
|
+
<!-- In single-select mode the header cell is an empty spacer -->
|
|
371
|
+
<th class="table-header table-checkbox-col" class:table-header-sticky={isStickyHeader}>
|
|
372
|
+
</th>
|
|
373
|
+
{/if}
|
|
124
374
|
{#each tableHeaders as header, colIndex (colIndex)}
|
|
125
375
|
<th class="table-header" class:table-header-sticky={isStickyHeader}>
|
|
126
376
|
<span class="table-header-content">
|
|
@@ -163,17 +413,24 @@
|
|
|
163
413
|
</tr>
|
|
164
414
|
</thead>
|
|
165
415
|
<tbody>
|
|
166
|
-
{#if
|
|
416
|
+
{#if filteredTableData.length === 0 && typeof empty === 'function'}
|
|
167
417
|
<tr>
|
|
168
|
-
<td
|
|
418
|
+
<td
|
|
419
|
+
class="table-empty"
|
|
420
|
+
colspan={isCheckboxMode ? tableHeaders.length + 1 : tableHeaders.length}
|
|
421
|
+
>
|
|
169
422
|
{@render empty()}
|
|
170
423
|
</td>
|
|
171
424
|
</tr>
|
|
172
425
|
{:else}
|
|
173
|
-
{#each
|
|
426
|
+
{#each filteredTableData as row, rowIndex (filteredRowIds[rowIndex])}
|
|
427
|
+
{@const rowId = filteredRowIds[rowIndex]}
|
|
428
|
+
{@const rowDisabled = isCheckboxMode && isRowDisabled(rowId)}
|
|
429
|
+
{@const rowSelected = isCheckboxMode && isRowSelected(rowId)}
|
|
174
430
|
<tr
|
|
175
431
|
class="table-row"
|
|
176
432
|
class:table-row-clickable={isRowClickable}
|
|
433
|
+
class:table-row-selected={rowSelected}
|
|
177
434
|
data-pw={typeof getRowTestId === 'function' ? getRowTestId(row, rowIndex) : null}
|
|
178
435
|
onclick={isRowClickable ? () => handleRowClick(rowIndex, row) : null}
|
|
179
436
|
onkeydown={isRowClickable
|
|
@@ -181,6 +438,32 @@
|
|
|
181
438
|
: null}
|
|
182
439
|
tabindex={isRowClickable ? 0 : null}
|
|
183
440
|
>
|
|
441
|
+
{#if isCheckboxMode}
|
|
442
|
+
<td class="table-content table-checkbox-col">
|
|
443
|
+
<span
|
|
444
|
+
class="table-checkbox-box"
|
|
445
|
+
class:checked={rowSelected}
|
|
446
|
+
class:disabled={rowDisabled}
|
|
447
|
+
role="checkbox"
|
|
448
|
+
tabindex={rowDisabled ? -1 : 0}
|
|
449
|
+
aria-checked={rowSelected}
|
|
450
|
+
aria-disabled={rowDisabled}
|
|
451
|
+
onclick={(mouseEvent) => {
|
|
452
|
+
mouseEvent.stopPropagation();
|
|
453
|
+
toggleRowSelection(rowId);
|
|
454
|
+
}}
|
|
455
|
+
onkeydown={(keyboardEvent) => {
|
|
456
|
+
keyboardEvent.stopPropagation();
|
|
457
|
+
handleCheckboxKeydown(keyboardEvent, () => toggleRowSelection(rowId));
|
|
458
|
+
}}
|
|
459
|
+
>
|
|
460
|
+
{#if rowSelected}
|
|
461
|
+
<!-- eslint-disable svelte/no-at-html-tags -->
|
|
462
|
+
<span class="table-checkbox-icon">{@html checkmarkSvg}</span>
|
|
463
|
+
{/if}
|
|
464
|
+
</span>
|
|
465
|
+
</td>
|
|
466
|
+
{/if}
|
|
184
467
|
{#each row as cellValue, colIndex (colIndex)}
|
|
185
468
|
<td
|
|
186
469
|
class="table-content"
|
|
@@ -211,6 +494,7 @@
|
|
|
211
494
|
{/if}
|
|
212
495
|
|
|
213
496
|
<style>
|
|
497
|
+
/* ── Title ─────────────────────────────────────────────────────────────── */
|
|
214
498
|
.table-title {
|
|
215
499
|
margin: var(--table-title-margin, 0px 0px 12px 0px);
|
|
216
500
|
font-size: var(--table-title-font-size, var(--table-tile-font-size, 18px));
|
|
@@ -220,6 +504,78 @@
|
|
|
220
504
|
padding: var(--table-title-padding);
|
|
221
505
|
}
|
|
222
506
|
|
|
507
|
+
/* ── C2-3 Search bar ────────────────────────────────────────────────────── */
|
|
508
|
+
.table-search {
|
|
509
|
+
display: flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
gap: var(--table-search-gap, 8px);
|
|
512
|
+
padding: var(--table-search-padding, 8px 12px);
|
|
513
|
+
border: var(--table-search-border, 1px solid #e5e7eb);
|
|
514
|
+
border-radius: var(--table-search-border-radius, 8px);
|
|
515
|
+
background-color: var(--table-search-background, #ffffff);
|
|
516
|
+
margin-bottom: var(--table-search-margin-bottom, 8px);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.table-search-icon {
|
|
520
|
+
display: inline-flex;
|
|
521
|
+
align-items: center;
|
|
522
|
+
color: var(--table-search-icon-color, #9ca3af);
|
|
523
|
+
flex-shrink: 0;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.table-search-icon :global(svg) {
|
|
527
|
+
width: var(--table-search-icon-size, 16px);
|
|
528
|
+
height: var(--table-search-icon-size, 16px);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.table-search-input {
|
|
532
|
+
flex: 1;
|
|
533
|
+
border: none;
|
|
534
|
+
outline: none;
|
|
535
|
+
background: transparent;
|
|
536
|
+
font-size: var(--table-search-font-size, 14px);
|
|
537
|
+
color: var(--table-search-color, #111827);
|
|
538
|
+
min-width: 0;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.table-search-input:focus-visible {
|
|
542
|
+
outline: 2px solid var(--table-focus-outline-color, #3b82f6);
|
|
543
|
+
outline-offset: 2px;
|
|
544
|
+
border-radius: var(--table-search-focus-border-radius, 2px);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.table-search-input::placeholder {
|
|
548
|
+
color: var(--table-search-placeholder-color, #9ca3af);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* Hide browser-default clear button on search inputs */
|
|
552
|
+
.table-search-input::-webkit-search-cancel-button {
|
|
553
|
+
display: none;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.table-search-clear {
|
|
557
|
+
display: inline-flex;
|
|
558
|
+
align-items: center;
|
|
559
|
+
padding: 2px;
|
|
560
|
+
border: none;
|
|
561
|
+
background: transparent;
|
|
562
|
+
color: var(--table-search-clear-color, #6b7280);
|
|
563
|
+
cursor: pointer;
|
|
564
|
+
border-radius: 4px;
|
|
565
|
+
flex-shrink: 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.table-search-clear:hover {
|
|
569
|
+
color: var(--table-search-clear-hover-color, #111827);
|
|
570
|
+
background-color: var(--table-search-clear-hover-background, rgba(0, 0, 0, 0.05));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.table-search-clear :global(svg) {
|
|
574
|
+
width: var(--table-search-clear-icon-size, 14px);
|
|
575
|
+
height: var(--table-search-clear-icon-size, 14px);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/* ── Container ──────────────────────────────────────────────────────────── */
|
|
223
579
|
.table-container {
|
|
224
580
|
border: var(--table-border, 1px solid #e5e7eb);
|
|
225
581
|
border-radius: var(--table-border-radius, 8px);
|
|
@@ -313,6 +669,77 @@
|
|
|
313
669
|
outline-offset: -2px;
|
|
314
670
|
}
|
|
315
671
|
|
|
672
|
+
/* C2-1: Selected row highlight */
|
|
673
|
+
.table-row-selected {
|
|
674
|
+
background-color: var(--table-row-selected-background, #eff6ff);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.table-row-selected > .table-content {
|
|
678
|
+
background-color: var(--table-row-selected-background, #eff6ff);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/* ── C2-1 Checkbox column ───────────────────────────────────────────────── */
|
|
682
|
+
.table-checkbox-col {
|
|
683
|
+
width: var(--table-checkbox-col-width, 44px);
|
|
684
|
+
padding: var(--table-checkbox-col-padding, 12px 12px);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.table-checkbox-box {
|
|
688
|
+
display: inline-flex;
|
|
689
|
+
align-items: center;
|
|
690
|
+
justify-content: center;
|
|
691
|
+
width: var(--table-checkbox-size, 18px);
|
|
692
|
+
height: var(--table-checkbox-size, 18px);
|
|
693
|
+
border: var(--table-checkbox-border, 2px solid #9ca3af);
|
|
694
|
+
border-radius: var(--table-checkbox-border-radius, 3px);
|
|
695
|
+
background-color: var(--table-checkbox-background, transparent);
|
|
696
|
+
cursor: pointer;
|
|
697
|
+
flex-shrink: 0;
|
|
698
|
+
transition:
|
|
699
|
+
background-color 0.15s,
|
|
700
|
+
border-color 0.15s;
|
|
701
|
+
user-select: none;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.table-checkbox-box:focus-visible {
|
|
705
|
+
outline: none;
|
|
706
|
+
box-shadow: var(--table-checkbox-focus-ring, 0 0 0 3px rgba(59, 130, 246, 0.3));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.table-checkbox-box:not(.disabled):hover {
|
|
710
|
+
border-color: var(--table-checkbox-hover-border-color, #6b7280);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.table-checkbox-box.checked {
|
|
714
|
+
background-color: var(--table-checkbox-checked-background, #2563eb);
|
|
715
|
+
border-color: var(--table-checkbox-checked-border-color, #2563eb);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.table-checkbox-box.indeterminate {
|
|
719
|
+
background-color: var(--table-checkbox-indeterminate-background, #2563eb);
|
|
720
|
+
border-color: var(--table-checkbox-indeterminate-border-color, #2563eb);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.table-checkbox-box.disabled {
|
|
724
|
+
opacity: var(--table-checkbox-disabled-opacity, 0.4);
|
|
725
|
+
cursor: not-allowed;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.table-checkbox-icon {
|
|
729
|
+
display: inline-flex;
|
|
730
|
+
align-items: center;
|
|
731
|
+
justify-content: center;
|
|
732
|
+
width: var(--table-checkbox-icon-size, 12px);
|
|
733
|
+
height: var(--table-checkbox-icon-size, 12px);
|
|
734
|
+
color: var(--table-checkbox-icon-color, #ffffff);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.table-checkbox-icon :global(svg) {
|
|
738
|
+
width: 100%;
|
|
739
|
+
height: 100%;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/* ── Sort button ────────────────────────────────────────────────────────── */
|
|
316
743
|
.sort-button {
|
|
317
744
|
--button-color: transparent;
|
|
318
745
|
--button-border: none;
|
|
@@ -351,18 +778,21 @@
|
|
|
351
778
|
opacity: var(--table-sort-idle-hover-opacity, 0.85);
|
|
352
779
|
}
|
|
353
780
|
|
|
781
|
+
/* ── Empty ──────────────────────────────────────────────────────────────── */
|
|
354
782
|
.table-empty {
|
|
355
783
|
padding: var(--table-empty-padding, 32px 24px);
|
|
356
784
|
text-align: center;
|
|
357
785
|
color: var(--table-empty-color, #9ca3af);
|
|
358
786
|
}
|
|
359
787
|
|
|
788
|
+
/* ── Footer ─────────────────────────────────────────────────────────────── */
|
|
360
789
|
.table-footer {
|
|
361
790
|
border-top: var(--table-footer-border, 1px solid #e5e7eb);
|
|
362
791
|
padding: var(--table-footer-padding, 8px 16px);
|
|
363
792
|
background-color: var(--table-footer-background, transparent);
|
|
364
793
|
}
|
|
365
794
|
|
|
795
|
+
/* ── Accessibility ──────────────────────────────────────────────────────── */
|
|
366
796
|
.sr-only {
|
|
367
797
|
position: absolute;
|
|
368
798
|
width: 1px;
|
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
import type { JSONValue } from 'type-decoder';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
export type SortDirection = 'asc' | 'desc';
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for row checkbox selection (C2-1).
|
|
6
|
+
*
|
|
7
|
+
* - `enabled` — activates the checkbox column.
|
|
8
|
+
* - `selectionMode` — `'single'` allows at most one row selected at a time;
|
|
9
|
+
* `'multiple'` (default) allows arbitrary many.
|
|
10
|
+
* - `onSelectionChange` — called whenever the selection set changes, receives
|
|
11
|
+
* the new set of selected row IDs.
|
|
12
|
+
* - `getRowId` — derives a stable string ID from a row + its index. Defaults
|
|
13
|
+
* to `String(rowIndex)` when omitted.
|
|
14
|
+
* - `disabledRowIds` — rows whose IDs appear in this set render a disabled,
|
|
15
|
+
* unchecked checkbox and cannot be selected.
|
|
16
|
+
*/
|
|
17
|
+
export type TableCheckboxSelectionConfig = {
|
|
18
|
+
/**
|
|
19
|
+
* Whether to activate the checkbox column. Defaults to `true` when the
|
|
20
|
+
* config object is provided — the presence of `checkboxSelection` already
|
|
21
|
+
* signals the intent to enable selection, so `enabled: false` is redundant.
|
|
22
|
+
* Kept for explicit opt-out without removing the config object.
|
|
23
|
+
*/
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
selectionMode?: 'single' | 'multiple';
|
|
26
|
+
onSelectionChange?: (selectedIds: Set<string>) => void;
|
|
27
|
+
getRowId?: (row: JSONValue[], rowIndex: number) => string;
|
|
28
|
+
disabledRowIds?: Set<string>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Configuration for the built-in search bar (C2-3).
|
|
32
|
+
*
|
|
33
|
+
* - `placeholder` — input placeholder text (default "Search…").
|
|
34
|
+
* - `searchableColumnIndices` — restrict filtering to these column indices.
|
|
35
|
+
* When omitted all columns are searched.
|
|
36
|
+
* - `testId` — `data-pw` attribute on the search input element.
|
|
37
|
+
*/
|
|
38
|
+
export type TableSearchConfig = {
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
searchableColumnIndices?: number[];
|
|
41
|
+
testId?: string;
|
|
42
|
+
};
|
|
4
43
|
export type TableProperties = OptionalTableProperties & TableEventProperties;
|
|
5
44
|
export type OptionalTableProperties = {
|
|
6
45
|
tableTitle?: string | null;
|
|
@@ -25,8 +64,57 @@ export type OptionalTableProperties = {
|
|
|
25
64
|
getRowTestId?: (row: JSONValue[], rowIndex: number) => string;
|
|
26
65
|
/** Return a data-pw value for the given cell. */
|
|
27
66
|
getCellTestId?: (row: JSONValue[], column: JSONValue, rowIndex: number) => string;
|
|
67
|
+
/**
|
|
68
|
+
* C2-1: Opt-in checkbox row-selection.
|
|
69
|
+
* Pass `{ enabled: true }` to show a leading checkbox column.
|
|
70
|
+
*/
|
|
71
|
+
checkboxSelection?: TableCheckboxSelectionConfig;
|
|
72
|
+
/**
|
|
73
|
+
* C2-3: Opt-in built-in search bar rendered above the table.
|
|
74
|
+
* Client-side filtering is performed by default; pass `onSearchChange` to
|
|
75
|
+
* delegate to the server instead.
|
|
76
|
+
*/
|
|
77
|
+
searchConfig?: TableSearchConfig;
|
|
28
78
|
};
|
|
29
79
|
export type TableEventProperties = {
|
|
30
80
|
onRowClick?: (rowIndex: number, rowData: JSONValue[]) => void;
|
|
31
81
|
onSort?: (columnIndex: number, direction: SortDirection) => void;
|
|
82
|
+
/**
|
|
83
|
+
* C2-2: Callback invoked when a cell value changes via an editable element
|
|
84
|
+
* inside the consumer's `cell` snippet (e.g. an `<Input>` or `<Select>`).
|
|
85
|
+
*
|
|
86
|
+
* **How to wire it**: Svelte 5 snippets execute in the consumer's scope, so
|
|
87
|
+
* `onCellChange` is NOT forwarded by Table — you must close over your own
|
|
88
|
+
* handler directly inside the snippet:
|
|
89
|
+
*
|
|
90
|
+
* ```svelte
|
|
91
|
+
* <script>
|
|
92
|
+
* let rows = $state(myData);
|
|
93
|
+
* const handleCellChange = (rowIndex, colIndex, newValue) => {
|
|
94
|
+
* rows[rowIndex][colIndex] = newValue;
|
|
95
|
+
* };
|
|
96
|
+
* </script>
|
|
97
|
+
*
|
|
98
|
+
* <Table tableData={rows}>
|
|
99
|
+
* {#snippet cell(value, rowIndex, colIndex)}
|
|
100
|
+
* <Input
|
|
101
|
+
* value={String(value ?? '')}
|
|
102
|
+
* onInput={(newValue) => handleCellChange(rowIndex, colIndex, newValue)}
|
|
103
|
+
* />
|
|
104
|
+
* {/snippet}
|
|
105
|
+
* </Table>
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* Table never mutates `tableData`; the consumer owns the source-of-truth
|
|
109
|
+
* array. Pass `onCellChange` as a top-level prop if you want Table to expose
|
|
110
|
+
* this handler to parent components via the standard props channel — it does
|
|
111
|
+
* not change the wiring inside the snippet.
|
|
112
|
+
*/
|
|
113
|
+
onCellChange?: (rowIndex: number, colIndex: number, newValue: JSONValue) => void;
|
|
114
|
+
/**
|
|
115
|
+
* C2-3: When provided, the built-in client-side filtering is disabled and
|
|
116
|
+
* this callback is called on every search input change instead, letting the
|
|
117
|
+
* server decide what rows to show.
|
|
118
|
+
*/
|
|
119
|
+
onSearchChange?: (searchTerm: string) => void;
|
|
32
120
|
};
|
package/dist/Toast/Toast.svelte
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import { fly } from 'svelte/transition';
|
|
3
4
|
import type { ToastDirection, ToastProperties } from './properties';
|
|
4
5
|
import type { FlyAnimationConfig } from '../types';
|
|
5
|
-
import { onMount } from 'svelte';
|
|
6
6
|
import Img from '../Img/Img.svelte';
|
|
7
7
|
|
|
8
8
|
let {
|
|
@@ -29,8 +29,11 @@
|
|
|
29
29
|
|
|
30
30
|
const animationConfig: FlyAnimationConfig = $derived(getAnimationConfig(overlapPage, direction));
|
|
31
31
|
|
|
32
|
-
let showToast = $state(
|
|
33
|
-
|
|
32
|
+
let showToast = $state(true);
|
|
33
|
+
|
|
34
|
+
const rootClass = $derived(
|
|
35
|
+
['toast', type ?? '', classes ?? ''].filter((cls) => cls.length > 0).join(' ')
|
|
36
|
+
);
|
|
34
37
|
|
|
35
38
|
function hideToast() {
|
|
36
39
|
showToast = false;
|
|
@@ -83,21 +86,30 @@
|
|
|
83
86
|
};
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
// Track `message` and `duration` as reactive dependencies so the timer
|
|
90
|
+
// restarts whenever either prop changes. Writing `showToast = true` via
|
|
91
|
+
// `untrack` prevents the assignment from enrolling `showToast` as a
|
|
92
|
+
// dependency of this effect, which would create an unwanted reactive cycle.
|
|
93
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
94
|
+
$effect(() => {
|
|
95
|
+
void message;
|
|
96
|
+
void duration;
|
|
97
|
+
|
|
98
|
+
untrack(() => {
|
|
99
|
+
showToast = true;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const id = setTimeout(hideToast, duration);
|
|
89
103
|
|
|
90
104
|
return () => {
|
|
91
|
-
|
|
92
|
-
clearTimeout(timeoutId);
|
|
93
|
-
}
|
|
105
|
+
clearTimeout(id);
|
|
94
106
|
};
|
|
95
107
|
});
|
|
96
108
|
</script>
|
|
97
109
|
|
|
98
110
|
{#if showToast}
|
|
99
111
|
<div
|
|
100
|
-
class=
|
|
112
|
+
class={rootClass}
|
|
101
113
|
class:no-page-overlap={!overlapPage}
|
|
102
114
|
role="alert"
|
|
103
115
|
aria-live="assertive"
|
|
@@ -129,9 +141,9 @@
|
|
|
129
141
|
tabindex="0"
|
|
130
142
|
role="button"
|
|
131
143
|
onclick={hideToast}
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
144
|
+
onkeydown={(event) => {
|
|
145
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
146
|
+
event.preventDefault();
|
|
135
147
|
hideToast();
|
|
136
148
|
}
|
|
137
149
|
}}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
export type ToastType = 'success' | 'error' | 'info' | 'warn';
|
|
3
3
|
export type ToastDirection = 'left-to-right' | 'right-to-left' | 'top-to-bottom' | 'bottom-to-top';
|
|
4
|
-
export type
|
|
4
|
+
export type MandatoryToastProperties = {
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
export type OptionalToastProperties = {
|
|
5
8
|
duration?: number;
|
|
6
9
|
leftIcon?: string | null;
|
|
7
|
-
message: string;
|
|
8
10
|
subtext?: string | null;
|
|
9
11
|
rightIcon?: string | null;
|
|
10
12
|
type?: ToastType | null;
|
|
@@ -24,3 +26,4 @@ export type ToastProperties = ToastEventProperties & {
|
|
|
24
26
|
export type ToastEventProperties = {
|
|
25
27
|
onToastHide?: () => void;
|
|
26
28
|
};
|
|
29
|
+
export type ToastProperties = MandatoryToastProperties & OptionalToastProperties & ToastEventProperties;
|