@juspay/svelte-ui-components 2.37.0 → 2.39.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.
@@ -1,21 +1,60 @@
1
1
  <script lang="ts">
2
2
  import type { AccordionProperties } from './properties';
3
3
 
4
- let { expand = false, children, classes }: AccordionProperties = $props();
4
+ let {
5
+ expand = $bindable(false),
6
+ children,
7
+ trigger,
8
+ ontoggle,
9
+ triggerClasses,
10
+ classes,
11
+ testId
12
+ }: AccordionProperties = $props();
13
+
14
+ function handleTriggerClick(): void {
15
+ expand = !expand;
16
+ ontoggle?.(expand);
17
+ }
5
18
  </script>
6
19
 
7
- <div class="accordion {classes ?? ''}" class:expanded={expand}>
20
+ {#if trigger}
21
+ <div
22
+ class="accordion-trigger {triggerClasses ?? ''}"
23
+ role="button"
24
+ tabindex="0"
25
+ aria-expanded={expand}
26
+ onclick={handleTriggerClick}
27
+ onkeydown={(event) => {
28
+ if (event.key === 'Enter' || event.key === ' ') {
29
+ event.preventDefault();
30
+ handleTriggerClick();
31
+ }
32
+ }}
33
+ >
34
+ {@render trigger({ expanded: expand })}
35
+ </div>
36
+ {/if}
37
+
38
+ <div
39
+ class="accordion {classes ?? ''}"
40
+ class:expanded={expand}
41
+ data-pw={typeof testId === 'string' ? testId : null}
42
+ >
8
43
  <div class="accordion-content">
9
44
  {@render children?.()}
10
45
  </div>
11
46
  </div>
12
47
 
13
48
  <style>
49
+ .accordion-trigger {
50
+ cursor: var(--accordion-trigger-cursor, pointer);
51
+ }
52
+
14
53
  .accordion {
15
54
  display: grid;
16
55
  grid-template-rows: 0fr;
17
56
  overflow: hidden;
18
- transition: grid-template-rows 0.2s ease-out;
57
+ transition: grid-template-rows var(--accordion-transition, 0.2s ease-out);
19
58
  }
20
59
 
21
60
  .accordion.expanded {
@@ -1,4 +1,4 @@
1
1
  import type { AccordionProperties } from './properties';
2
- declare const Accordion: import("svelte").Component<AccordionProperties, {}, "">;
2
+ declare const Accordion: import("svelte").Component<AccordionProperties, {}, "expand">;
3
3
  type Accordion = ReturnType<typeof Accordion>;
4
4
  export default Accordion;
@@ -1,6 +1,15 @@
1
1
  import type { Snippet } from 'svelte';
2
- export type AccordionProperties = {
2
+ export type AccordionProperties = OptionalAccordionProperties & AccordionEventProperties;
3
+ export type OptionalAccordionProperties = {
3
4
  expand?: boolean;
4
5
  children?: Snippet;
6
+ trigger?: Snippet<[{
7
+ expanded: boolean;
8
+ }]>;
9
+ triggerClasses?: string;
5
10
  classes?: string;
11
+ testId?: string;
12
+ };
13
+ export type AccordionEventProperties = {
14
+ ontoggle?: (expanded: boolean) => void;
6
15
  };
@@ -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
- function isColumnSortable(colIndex: number): boolean {
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
- function handleSort(colIndex: number) {
80
- if (!isColumnSortable(colIndex)) {
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 (sortColumn === colIndex) {
85
- sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
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
- sortColumn = colIndex;
88
- sortDirection = 'asc';
257
+ if (selectedIds.has(rowId)) {
258
+ selectedIds.delete(rowId);
259
+ } else {
260
+ selectedIds.add(rowId);
261
+ }
89
262
  }
90
- onSort?.(colIndex, sortDirection);
91
- }
263
+ checkboxSelection.onSelectionChange?.(new Set(selectedIds));
264
+ };
92
265
 
93
- function handleRowClick(rowIndex: number, rowData: JSONValue[]) {
94
- onRowClick?.(rowIndex, rowData);
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
- function handleRowKeydown(event: KeyboardEvent, rowIndex: number, rowData: JSONValue[]) {
98
- if (event.key === 'Enter' || event.key === ' ') {
285
+ const handleCheckboxKeydown = (event: KeyboardEvent, action: () => void): void => {
286
+ if (event.key === ' ' || event.key === 'Enter') {
99
287
  event.preventDefault();
100
- onRowClick?.(rowIndex, rowData);
288
+ action();
101
289
  }
102
- }
290
+ };
103
291
 
104
- let isRowClickable = $derived(typeof onRowClick === 'function');
105
- let isStickyHeader = $derived(stickyHeader || isTableScrollable);
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 sortedTableData.length === 0 && typeof empty === 'function'}
416
+ {#if filteredTableData.length === 0 && typeof empty === 'function'}
167
417
  <tr>
168
- <td class="table-empty" colspan={tableHeaders.length}>
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 sortedTableData as row, rowIndex (rowIndex)}
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/index.d.ts CHANGED
@@ -78,6 +78,7 @@ export type * from './Toolbar/properties';
78
78
  export type * from './Carousel/properties';
79
79
  export type * from './Badge/properties';
80
80
  export type * from './Banner/properties';
81
+ export type * from './Accordion/properties';
81
82
  export type * from './Table/properties';
82
83
  export type * from './Stepper/properties';
83
84
  export type * from './Toast/properties';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.37.0",
3
+ "version": "2.39.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",