@juspay/svelte-ui-components 2.28.2 → 2.28.3

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 (34) hide show
  1. package/dist/Banner/Banner.svelte +34 -7
  2. package/dist/Banner/properties.d.ts +2 -0
  3. package/dist/Breadcrumb/Breadcrumb.svelte +58 -0
  4. package/dist/Breadcrumb/Breadcrumb.svelte.d.ts +4 -0
  5. package/dist/Breadcrumb/properties.d.ts +19 -0
  6. package/dist/Breadcrumb/properties.js +1 -0
  7. package/dist/Calendar/Calendar.svelte +10 -4
  8. package/dist/Calendar/properties.d.ts +2 -0
  9. package/dist/DateRangePicker/DateRangePicker.svelte +627 -0
  10. package/dist/DateRangePicker/DateRangePicker.svelte.d.ts +4 -0
  11. package/dist/DateRangePicker/properties.d.ts +74 -0
  12. package/dist/DateRangePicker/properties.js +1 -0
  13. package/dist/EmptyState/EmptyState.svelte +3 -1
  14. package/dist/EmptyState/properties.d.ts +1 -1
  15. package/dist/FileInput/FileInput.svelte +176 -0
  16. package/dist/FileInput/FileInput.svelte.d.ts +4 -0
  17. package/dist/FileInput/properties.d.ts +18 -0
  18. package/dist/FileInput/properties.js +1 -0
  19. package/dist/Modal/Modal.svelte +5 -4
  20. package/dist/Pagination/Pagination.svelte +56 -11
  21. package/dist/Pagination/properties.d.ts +15 -0
  22. package/dist/Pill/Pill.svelte +10 -0
  23. package/dist/Pill/properties.d.ts +7 -0
  24. package/dist/Select/Select.svelte +69 -29
  25. package/dist/Select/Select.svelte.d.ts +1 -1
  26. package/dist/Select/properties.d.ts +8 -1
  27. package/dist/Sheet/Sheet.svelte +16 -0
  28. package/dist/Sheet/properties.d.ts +4 -0
  29. package/dist/Table/Table.svelte +28 -6
  30. package/dist/Table/properties.d.ts +6 -0
  31. package/dist/Tabs/Tabs.svelte +52 -8
  32. package/dist/index.d.ts +6 -0
  33. package/dist/index.js +3 -0
  34. package/package.json +1 -1
@@ -5,18 +5,25 @@
5
5
  import chevronDownSvg from '../assets/chevron-down.svg?raw';
6
6
 
7
7
  let {
8
- items,
8
+ items: rawItems,
9
9
  value = $bindable([]),
10
10
  multiple = false,
11
11
  searchable = false,
12
12
  placeholder = '',
13
13
  disabled = false,
14
+ bottomContent,
15
+ optionIndicator,
14
16
  testId,
15
17
  onchange,
16
- classes
18
+ classes,
19
+ open = $bindable(false)
17
20
  }: SelectProperties = $props();
18
21
 
19
- let isOpen = $state(false);
22
+ function normalizeItems(source: SelectItem[] | string[]): SelectItem[] {
23
+ return source.map((entry) => (typeof entry === 'string' ? { id: entry, label: entry } : entry));
24
+ }
25
+
26
+ let items: SelectItem[] = $derived(normalizeItems(rawItems));
20
27
  let query = $state('');
21
28
  let highlightedIndex = $state(-1);
22
29
  let containerEl: HTMLDivElement | null = $state(null);
@@ -48,13 +55,13 @@
48
55
  highlightedIndex >= 0 ? `${listboxId}-option-${highlightedIndex}` : null
49
56
  );
50
57
 
51
- let searchPlaceholder = $derived(isOpen && displayText.length > 0 ? displayText : placeholder);
58
+ let searchPlaceholder = $derived(open && displayText.length > 0 ? displayText : placeholder);
52
59
 
53
- async function open(): Promise<void> {
54
- if (disabled || isOpen) {
60
+ async function openDropdown(): Promise<void> {
61
+ if (disabled || open) {
55
62
  return;
56
63
  }
57
- isOpen = true;
64
+ open = true;
58
65
  highlightedIndex = -1;
59
66
  query = '';
60
67
  if (searchable) {
@@ -66,7 +73,7 @@
66
73
  }
67
74
 
68
75
  function close(): void {
69
- isOpen = false;
76
+ open = false;
70
77
  query = '';
71
78
  highlightedIndex = -1;
72
79
  }
@@ -119,17 +126,17 @@
119
126
 
120
127
  function handleTriggerClick(event: MouseEvent): void {
121
128
  if (event.target instanceof HTMLInputElement) {
122
- if (!isOpen) {
123
- open();
129
+ if (!open) {
130
+ openDropdown();
124
131
  }
125
132
  return;
126
133
  }
127
134
  if (multiple && searchable) {
128
- open();
129
- } else if (isOpen) {
135
+ openDropdown();
136
+ } else if (open) {
130
137
  close();
131
138
  } else {
132
- open();
139
+ openDropdown();
133
140
  }
134
141
  }
135
142
 
@@ -140,28 +147,28 @@
140
147
  switch (event.key) {
141
148
  case 'Enter':
142
149
  event.preventDefault();
143
- if (isOpen) {
150
+ if (open) {
144
151
  selectHighlighted();
145
152
  } else {
146
- open();
153
+ openDropdown();
147
154
  }
148
155
  break;
149
156
  case ' ':
150
157
  if (!(event.target instanceof HTMLInputElement)) {
151
158
  event.preventDefault();
152
- if (isOpen) {
159
+ if (open) {
153
160
  selectHighlighted();
154
161
  } else {
155
- open();
162
+ openDropdown();
156
163
  }
157
164
  }
158
165
  break;
159
166
  case 'ArrowDown':
160
167
  event.preventDefault();
161
- if (isOpen) {
168
+ if (open) {
162
169
  moveHighlight(1);
163
170
  } else {
164
- open();
171
+ openDropdown();
165
172
  }
166
173
  break;
167
174
  case 'ArrowUp':
@@ -169,7 +176,7 @@
169
176
  moveHighlight(-1);
170
177
  break;
171
178
  case 'Escape':
172
- if (isOpen) {
179
+ if (open) {
173
180
  close();
174
181
  if (!searchable && triggerEl !== null) {
175
182
  triggerEl.focus();
@@ -185,7 +192,7 @@
185
192
  }
186
193
  break;
187
194
  case 'Tab':
188
- if (isOpen) {
195
+ if (open) {
189
196
  close();
190
197
  }
191
198
  break;
@@ -197,15 +204,15 @@
197
204
  return;
198
205
  }
199
206
  query = event.target.value;
200
- if (!isOpen) {
201
- isOpen = true;
207
+ if (!open) {
208
+ open = true;
202
209
  }
203
210
  highlightedIndex = -1;
204
211
  }
205
212
 
206
213
  function handleSearchFocus(): void {
207
- if (!isOpen) {
208
- open();
214
+ if (!open) {
215
+ openDropdown();
209
216
  }
210
217
  }
211
218
 
@@ -229,7 +236,7 @@
229
236
 
230
237
  <div
231
238
  class="select {classes ?? ''}"
232
- class:open={isOpen}
239
+ class:open
233
240
  class:disabled
234
241
  bind:this={containerEl}
235
242
  {...typeof testId === 'string' ? { 'data-pw': testId } : {}}
@@ -240,7 +247,7 @@
240
247
  onclick={handleTriggerClick}
241
248
  onkeydown={handleKeydown}
242
249
  role="combobox"
243
- aria-expanded={isOpen}
250
+ aria-expanded={open}
244
251
  aria-haspopup="listbox"
245
252
  aria-controls={listboxId}
246
253
  {...highlightedOptionId !== null ? { 'aria-activedescendant': highlightedOptionId } : {}}
@@ -276,7 +283,7 @@
276
283
  <input
277
284
  class="select-search"
278
285
  type="text"
279
- value={isOpen ? query : displayText}
286
+ value={open ? query : displayText}
280
287
  oninput={handleSearchInput}
281
288
  onfocus={handleSearchFocus}
282
289
  bind:this={searchInputEl}
@@ -294,7 +301,7 @@
294
301
  <span class="select-arrow">{@html chevronDownSvg}</span>
295
302
  </div>
296
303
 
297
- {#if isOpen && !disabled}
304
+ {#if open && !disabled}
298
305
  <div class="select-dropdown" role="listbox" id={listboxId} aria-multiselectable={multiple}>
299
306
  {#if filteredItems.length === 0}
300
307
  <div class="select-empty">No results</div>
@@ -303,6 +310,7 @@
303
310
  <!-- svelte-ignore a11y_click_events_have_key_events -->
304
311
  <div
305
312
  class="select-option"
313
+ class:multi={multiple}
306
314
  class:selected={value.includes(item.id)}
307
315
  class:highlighted={index === highlightedIndex}
308
316
  role="option"
@@ -312,10 +320,24 @@
312
320
  onclick={() => selectItem(item.id)}
313
321
  onmouseenter={() => (highlightedIndex = index)}
314
322
  >
323
+ {#if multiple}
324
+ {#if typeof optionIndicator === 'function'}
325
+ {@render optionIndicator({ checked: value.includes(item.id) })}
326
+ {:else}
327
+ <span class="select-option-indicator" aria-hidden="true"
328
+ >{value.includes(item.id) ? '☑' : '☐'}</span
329
+ >
330
+ {/if}
331
+ {/if}
315
332
  {item.label}
316
333
  </div>
317
334
  {/each}
318
335
  {/if}
336
+ {#if typeof bottomContent === 'function'}
337
+ <div class="select-bottom-content">
338
+ {@render bottomContent()}
339
+ </div>
340
+ {/if}
319
341
  </div>
320
342
  {/if}
321
343
  </div>
@@ -436,6 +458,12 @@
436
458
  transition: background 0.1s;
437
459
  }
438
460
 
461
+ .select-option.multi {
462
+ display: flex;
463
+ align-items: center;
464
+ gap: var(--select-option-gap, 0);
465
+ }
466
+
439
467
  .select-option:hover,
440
468
  .select-option.highlighted {
441
469
  background: var(--select-option-hover-background, #f0f0f0);
@@ -454,6 +482,13 @@
454
482
  );
455
483
  }
456
484
 
485
+ .select-option-indicator {
486
+ display: inline-flex;
487
+ align-items: center;
488
+ color: var(--select-option-indicator-color, currentColor);
489
+ flex-shrink: 0;
490
+ }
491
+
457
492
  .select-empty {
458
493
  padding: var(--select-empty-padding, 8px 12px);
459
494
  color: var(--select-empty-color, #999999);
@@ -461,6 +496,11 @@
461
496
  font-size: var(--select-empty-font-size, inherit);
462
497
  }
463
498
 
499
+ .select-bottom-content {
500
+ border-top: var(--select-bottom-content-border, none);
501
+ padding: var(--select-bottom-content-padding, 8px 12px);
502
+ }
503
+
464
504
  .select-trigger :global(.pill) {
465
505
  --pill-background: var(--select-pill-background, #e0e0e0);
466
506
  --pill-color: var(--select-pill-color, #333333);
@@ -1,4 +1,4 @@
1
1
  import type { SelectProperties } from './properties';
2
- declare const Select: import("svelte").Component<SelectProperties, {}, "value">;
2
+ declare const Select: import("svelte").Component<SelectProperties, {}, "value" | "open">;
3
3
  type Select = ReturnType<typeof Select>;
4
4
  export default Select;
@@ -1,10 +1,11 @@
1
+ import type { Snippet } from 'svelte';
1
2
  export type SelectProperties = MandatorySelectProperties & OptionalSelectProperties & SelectEventProperties;
2
3
  export type SelectItem = {
3
4
  id: string;
4
5
  label: string;
5
6
  };
6
7
  export type MandatorySelectProperties = {
7
- items: SelectItem[];
8
+ items: SelectItem[] | string[];
8
9
  };
9
10
  export type OptionalSelectProperties = {
10
11
  value?: string[];
@@ -12,8 +13,14 @@ export type OptionalSelectProperties = {
12
13
  searchable?: boolean;
13
14
  placeholder?: string;
14
15
  disabled?: boolean;
16
+ bottomContent?: Snippet;
17
+ optionIndicator?: Snippet<[{
18
+ checked: boolean;
19
+ }]>;
15
20
  testId?: string;
16
21
  classes?: string;
22
+ /** Bindable. Controls whether the dropdown is open; the component writes back on open/close so parents can `bind:open` to observe or drive it. Unbound, the component manages its own state. */
23
+ open?: boolean;
17
24
  };
18
25
  export type SelectEventProperties = {
19
26
  onchange?: (value: string[]) => void;
@@ -14,6 +14,8 @@
14
14
  content,
15
15
  footer,
16
16
  onclose,
17
+ onafteropen,
18
+ onafterclose,
17
19
  classes
18
20
  }: SheetProperties = $props();
19
21
 
@@ -90,6 +92,18 @@
90
92
  }
91
93
  };
92
94
  }
95
+
96
+ function handleIntroEnd() {
97
+ if (typeof onafteropen === 'function') {
98
+ onafteropen();
99
+ }
100
+ }
101
+
102
+ function handleOutroEnd() {
103
+ if (typeof onafterclose === 'function') {
104
+ onafterclose();
105
+ }
106
+ }
93
107
  </script>
94
108
 
95
109
  {#if open}
@@ -112,6 +126,8 @@
112
126
  aria-label={title ?? 'Sheet'}
113
127
  tabindex="-1"
114
128
  transition:fly|global={flyParams}
129
+ onintroend={handleIntroEnd}
130
+ onoutroend={handleOutroEnd}
115
131
  >
116
132
  {#if typeof title === 'string' || showCloseButton}
117
133
  <div class="sheet-header">
@@ -16,4 +16,8 @@ export type OptionalSheetProperties = {
16
16
  };
17
17
  export type SheetEventProperties = {
18
18
  onclose?: () => void;
19
+ /** Called once after the open transition has fully completed. */
20
+ onafteropen?: () => void;
21
+ /** Called once after the close transition has fully completed. */
22
+ onafterclose?: () => void;
19
23
  };
@@ -24,7 +24,10 @@
24
24
  empty,
25
25
  onRowClick,
26
26
  onSort,
27
- classes
27
+ classes,
28
+ paginatorSlot,
29
+ getRowTestId,
30
+ getCellTestId
28
31
  }: TableProperties = $props();
29
32
 
30
33
  let sortColumn = $state<number | null>(null);
@@ -48,9 +51,9 @@
48
51
  const colIndex = sortColumn;
49
52
  const direction = sortDirection;
50
53
 
51
- return [...tableData].sort((a, b) => {
52
- const valueA = a[colIndex];
53
- const valueB = b[colIndex];
54
+ return [...tableData].sort((rowA, rowB) => {
55
+ const valueA = rowA[colIndex];
56
+ const valueB = rowB[colIndex];
54
57
 
55
58
  if (typeof valueA === 'number' && typeof valueB === 'number') {
56
59
  return direction === 'asc' ? valueA - valueB : valueB - valueA;
@@ -171,12 +174,20 @@
171
174
  <tr
172
175
  class="table-row"
173
176
  class:table-row-clickable={isRowClickable}
177
+ data-pw={typeof getRowTestId === 'function' ? getRowTestId(row, rowIndex) : null}
174
178
  onclick={isRowClickable ? () => handleRowClick(rowIndex, row) : null}
175
- onkeydown={isRowClickable ? (e) => handleRowKeydown(e, rowIndex, row) : null}
179
+ onkeydown={isRowClickable
180
+ ? (keyboardEvent) => handleRowKeydown(keyboardEvent, rowIndex, row)
181
+ : null}
176
182
  tabindex={isRowClickable ? 0 : null}
177
183
  >
178
184
  {#each row as cellValue, colIndex (colIndex)}
179
- <td class="table-content">
185
+ <td
186
+ class="table-content"
187
+ data-pw={typeof getCellTestId === 'function'
188
+ ? getCellTestId(row, cellValue, rowIndex)
189
+ : null}
190
+ >
180
191
  <div class={isContentScrollable ? 'scrollable-content' : ''}>
181
192
  {#if typeof cell === 'function'}
182
193
  {@render cell(cellValue, rowIndex, colIndex)}
@@ -191,6 +202,11 @@
191
202
  {/if}
192
203
  </tbody>
193
204
  </table>
205
+ {#if typeof paginatorSlot === 'function'}
206
+ <div class="table-footer">
207
+ {@render paginatorSlot()}
208
+ </div>
209
+ {/if}
194
210
  </div>
195
211
  {/if}
196
212
 
@@ -341,6 +357,12 @@
341
357
  color: var(--table-empty-color, #9ca3af);
342
358
  }
343
359
 
360
+ .table-footer {
361
+ border-top: var(--table-footer-border, 1px solid #e5e7eb);
362
+ padding: var(--table-footer-padding, 8px 16px);
363
+ background-color: var(--table-footer-background, transparent);
364
+ }
365
+
344
366
  .sr-only {
345
367
  position: absolute;
346
368
  width: 1px;
@@ -19,6 +19,12 @@ export type OptionalTableProperties = {
19
19
  cell?: Snippet<[JSONValue, number, number]>;
20
20
  empty?: Snippet;
21
21
  classes?: string;
22
+ /** Snippet rendered in a footer region below the table (e.g. a paginator). */
23
+ paginatorSlot?: Snippet;
24
+ /** Return a data-pw value for the given row. */
25
+ getRowTestId?: (row: JSONValue[], rowIndex: number) => string;
26
+ /** Return a data-pw value for the given cell. */
27
+ getCellTestId?: (row: JSONValue[], column: JSONValue, rowIndex: number) => string;
22
28
  };
23
29
  export type TableEventProperties = {
24
30
  onRowClick?: (rowIndex: number, rowData: JSONValue[]) => void;
@@ -44,6 +44,11 @@
44
44
  let canScrollLeft = $state(false);
45
45
  let canScrollRight = $state(false);
46
46
 
47
+ // Sliding indicator state
48
+ let indicatorLeft = $state(0);
49
+ let indicatorWidth = $state(0);
50
+ let indicatorReady = $state(false);
51
+
47
52
  function updateOverflow(): void {
48
53
  if (scrollContainer === null) {
49
54
  return;
@@ -53,6 +58,22 @@
53
58
  canScrollRight = scrollLeft + clientWidth < scrollWidth - 1;
54
59
  }
55
60
 
61
+ function updateIndicator(): void {
62
+ if (scrollContainer === null) {
63
+ return;
64
+ }
65
+ const activeEl = scrollContainer.querySelector<HTMLElement>('.tabs-item.active');
66
+ if (activeEl === null) {
67
+ indicatorReady = false;
68
+ indicatorLeft = 0;
69
+ indicatorWidth = 0;
70
+ return;
71
+ }
72
+ indicatorLeft = activeEl.offsetLeft;
73
+ indicatorWidth = activeEl.offsetWidth;
74
+ indicatorReady = true;
75
+ }
76
+
56
77
  function scroll(direction: 'left' | 'right'): void {
57
78
  if (scrollContainer === null) {
58
79
  return;
@@ -73,7 +94,10 @@
73
94
  if (typeof rawItem !== 'object') {
74
95
  return;
75
96
  }
76
- const tabItem = toTabItem(rawItem)!;
97
+ const tabItem = toTabItem(rawItem);
98
+ if (tabItem === null) {
99
+ return;
100
+ }
77
101
  if (tabItem.key === activeKey) {
78
102
  return;
79
103
  }
@@ -101,8 +125,17 @@
101
125
  function initOverflow(node: HTMLDivElement): { destroy: () => void } {
102
126
  scrollContainer = node;
103
127
  updateOverflow();
104
- const observer = new MutationObserver(updateOverflow);
105
- observer.observe(node, { childList: true, subtree: true });
128
+ updateIndicator();
129
+ const observer = new MutationObserver(() => {
130
+ updateOverflow();
131
+ updateIndicator();
132
+ });
133
+ observer.observe(node, {
134
+ childList: true,
135
+ subtree: true,
136
+ attributes: true,
137
+ attributeFilter: ['class']
138
+ });
106
139
  return {
107
140
  destroy() {
108
141
  observer.disconnect();
@@ -153,11 +186,15 @@
153
186
  {:else}
154
187
  {label}
155
188
  {/if}
156
- {#if isActiveItem(index)}
157
- <span class="tabs-indicator"></span>
158
- {/if}
159
189
  </div>
160
190
  {/each}
191
+ {#if indicatorReady}
192
+ <span
193
+ class="tabs-indicator"
194
+ aria-hidden="true"
195
+ style="left: {indicatorLeft}px; width: {indicatorWidth}px;"
196
+ ></span>
197
+ {/if}
161
198
  </div>
162
199
  {#if canScrollRight}
163
200
  <button
@@ -199,6 +236,7 @@
199
236
  gap: var(--tabs-bar-gap, 0px);
200
237
  overflow-x: auto;
201
238
  scrollbar-width: none;
239
+ position: relative;
202
240
  }
203
241
 
204
242
  .tabs-bar::-webkit-scrollbar {
@@ -294,10 +332,16 @@
294
332
  .tabs-indicator {
295
333
  position: absolute;
296
334
  bottom: 0;
297
- left: 0;
298
- right: 0;
299
335
  height: var(--tabs-indicator-height, 2px);
300
336
  background-color: var(--tabs-indicator-color, #1a73e8);
301
337
  border-radius: var(--tabs-indicator-border-radius, 2px 2px 0 0);
338
+ transition: var(--tabs-indicator-transition, left 0.3s ease, width 0.3s ease);
339
+ pointer-events: none;
340
+ }
341
+
342
+ @media (prefers-reduced-motion: reduce) {
343
+ .tabs-indicator {
344
+ transition: none;
345
+ }
302
346
  }
303
347
  </style>
package/dist/index.d.ts CHANGED
@@ -56,6 +56,9 @@ export { default as EmptyState } from './EmptyState/EmptyState.svelte';
56
56
  export { default as Combobox } from './Combobox/Combobox.svelte';
57
57
  export { default as ColorPicker } from './ColorPicker/ColorPicker.svelte';
58
58
  export { default as SplitInput } from './SplitInput/SplitInput.svelte';
59
+ export { default as FileInput } from './FileInput/FileInput.svelte';
60
+ export { default as DateRangePicker } from './DateRangePicker/DateRangePicker.svelte';
61
+ export { default as Breadcrumb } from './Breadcrumb/Breadcrumb.svelte';
59
62
  export type * from './Button/properties';
60
63
  export type * from './Modal/properties';
61
64
  export type * from './Input/properties';
@@ -108,4 +111,7 @@ export type * from './EmptyState/properties';
108
111
  export type * from './Combobox/properties';
109
112
  export type * from './ColorPicker/properties';
110
113
  export type * from './SplitInput/properties';
114
+ export type * from './FileInput/properties';
115
+ export type * from './DateRangePicker/properties';
116
+ export type * from './Breadcrumb/properties';
111
117
  export { validateInput } from './utils';
package/dist/index.js CHANGED
@@ -56,4 +56,7 @@ export { default as EmptyState } from './EmptyState/EmptyState.svelte';
56
56
  export { default as Combobox } from './Combobox/Combobox.svelte';
57
57
  export { default as ColorPicker } from './ColorPicker/ColorPicker.svelte';
58
58
  export { default as SplitInput } from './SplitInput/SplitInput.svelte';
59
+ export { default as FileInput } from './FileInput/FileInput.svelte';
60
+ export { default as DateRangePicker } from './DateRangePicker/DateRangePicker.svelte';
61
+ export { default as Breadcrumb } from './Breadcrumb/Breadcrumb.svelte';
59
62
  export { validateInput } from './utils';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.28.2",
3
+ "version": "2.28.3",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",