@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
@@ -11,7 +11,9 @@
11
11
  </div>
12
12
  {/if}
13
13
  <div class="empty-state-title">{title}</div>
14
- <div class="empty-state-description">{description}</div>
14
+ {#if typeof description === 'string' && description.length > 0}
15
+ <div class="empty-state-description">{description}</div>
16
+ {/if}
15
17
  {#if typeof children === 'function'}
16
18
  <div class="empty-state-actions">
17
19
  {@render children()}
@@ -2,9 +2,9 @@ import type { Snippet } from 'svelte';
2
2
  export type EmptyStateProperties = MandatoryEmptyStateProperties & OptionalEmptyStateProperties;
3
3
  export type MandatoryEmptyStateProperties = {
4
4
  title: string;
5
- description: string;
6
5
  };
7
6
  export type OptionalEmptyStateProperties = {
7
+ description?: string;
8
8
  icon?: Snippet;
9
9
  children?: Snippet;
10
10
  classes?: string;
@@ -0,0 +1,176 @@
1
+ <script lang="ts">
2
+ import type { FileInputProperties } from './properties';
3
+
4
+ let {
5
+ trigger,
6
+ accept,
7
+ multiple = false,
8
+ maxSizeBytes,
9
+ disabled = false,
10
+ testId,
11
+ classes,
12
+ onfiles,
13
+ onerror
14
+ }: FileInputProperties = $props();
15
+
16
+ let dragOver = $state(false);
17
+ let inputEl: HTMLInputElement | null = $state(null);
18
+
19
+ function openFilePicker(): void {
20
+ if (!disabled) {
21
+ inputEl?.click();
22
+ }
23
+ }
24
+
25
+ function isAccepted(file: File): boolean {
26
+ if (typeof accept !== 'string' || accept.length === 0) {
27
+ return true;
28
+ }
29
+ const tokens = accept.split(',').map((token) => token.trim().toLowerCase());
30
+ const fileName = file.name.toLowerCase();
31
+ const fileMime = file.type.toLowerCase();
32
+
33
+ return tokens.some((token) => {
34
+ if (token.startsWith('.')) {
35
+ return fileName.endsWith(token);
36
+ }
37
+ if (token.endsWith('/*')) {
38
+ const baseType = token.slice(0, -2);
39
+ return fileMime.startsWith(baseType + '/');
40
+ }
41
+ return fileMime === token;
42
+ });
43
+ }
44
+
45
+ function processFiles(rawFiles: FileList | null): void {
46
+ if (rawFiles === null || rawFiles.length === 0) {
47
+ return;
48
+ }
49
+
50
+ const fileArray = Array.from(rawFiles);
51
+ const rejected: string[] = [];
52
+ const accepted: File[] = [];
53
+
54
+ for (const file of fileArray) {
55
+ if (!isAccepted(file)) {
56
+ rejected.push(`"${file.name}" has an unsupported type.`);
57
+ continue;
58
+ }
59
+ if (typeof maxSizeBytes === 'number' && file.size > maxSizeBytes) {
60
+ const limitMb = (maxSizeBytes / (1024 * 1024)).toFixed(1);
61
+ rejected.push(`"${file.name}" exceeds the ${limitMb} MB limit.`);
62
+ continue;
63
+ }
64
+ accepted.push(file);
65
+ }
66
+
67
+ if (rejected.length > 0) {
68
+ onerror?.(rejected.join(' '));
69
+ }
70
+ if (accepted.length > 0) {
71
+ onfiles?.(accepted);
72
+ }
73
+ }
74
+
75
+ function handleInputChange(event: Event): void {
76
+ const target = event.currentTarget;
77
+ if (!(target instanceof HTMLInputElement)) {
78
+ return;
79
+ }
80
+ processFiles(target.files);
81
+ target.value = '';
82
+ }
83
+
84
+ function handleDragOver(event: DragEvent): void {
85
+ event.preventDefault();
86
+ if (!disabled) {
87
+ dragOver = true;
88
+ }
89
+ }
90
+
91
+ function handleDragLeave(event: DragEvent): void {
92
+ event.preventDefault();
93
+ dragOver = false;
94
+ }
95
+
96
+ function handleDrop(event: DragEvent): void {
97
+ event.preventDefault();
98
+ dragOver = false;
99
+ if (!disabled) {
100
+ processFiles(event.dataTransfer?.files ?? null);
101
+ }
102
+ }
103
+
104
+ function handleKeyDown(event: KeyboardEvent): void {
105
+ if (event.key === 'Enter' || event.key === ' ') {
106
+ event.preventDefault();
107
+ openFilePicker();
108
+ }
109
+ }
110
+ </script>
111
+
112
+ <div
113
+ class="file-input {classes ?? ''}"
114
+ class:file-input-dragover={dragOver}
115
+ class:file-input-disabled={disabled}
116
+ data-pw={testId}
117
+ role="button"
118
+ tabindex={disabled ? -1 : 0}
119
+ aria-disabled={disabled}
120
+ ondragover={handleDragOver}
121
+ ondragleave={handleDragLeave}
122
+ ondrop={handleDrop}
123
+ onkeydown={handleKeyDown}
124
+ >
125
+ <input
126
+ bind:this={inputEl}
127
+ type="file"
128
+ class="file-input-hidden"
129
+ {accept}
130
+ {multiple}
131
+ {disabled}
132
+ data-pw={typeof testId === 'string' ? `${testId}-input` : null}
133
+ onchange={handleInputChange}
134
+ tabindex="-1"
135
+ aria-hidden="true"
136
+ />
137
+
138
+ {@render trigger({ openFilePicker, dragOver, disabled })}
139
+ </div>
140
+
141
+ <style>
142
+ .file-input {
143
+ display: var(--file-input-display, inline-flex);
144
+ flex-direction: var(--file-input-flex-direction, column);
145
+ align-items: var(--file-input-align-items, center);
146
+ justify-content: var(--file-input-justify-content, center);
147
+ padding: var(--file-input-padding);
148
+ border: var(--file-input-border);
149
+ border-radius: var(--file-input-radius);
150
+ background: var(--file-input-background);
151
+ gap: var(--file-input-gap);
152
+ text-align: var(--file-input-text-align, center);
153
+ transition: var(--file-input-transition);
154
+ cursor: pointer;
155
+ }
156
+
157
+ .file-input:focus-visible {
158
+ outline: var(--file-input-focus-outline);
159
+ outline-offset: var(--file-input-focus-outline-offset);
160
+ }
161
+
162
+ .file-input-dragover {
163
+ background: var(--file-input-dragover-background);
164
+ border-color: var(--file-input-dragover-border-color);
165
+ }
166
+
167
+ .file-input-disabled {
168
+ opacity: var(--file-input-disabled-opacity, 0.5);
169
+ cursor: var(--file-input-disabled-cursor, not-allowed);
170
+ pointer-events: none;
171
+ }
172
+
173
+ .file-input-hidden {
174
+ display: none;
175
+ }
176
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { FileInputProperties } from './properties';
2
+ declare const FileInput: import("svelte").Component<FileInputProperties, {}, "">;
3
+ type FileInput = ReturnType<typeof FileInput>;
4
+ export default FileInput;
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type FileInputSnippetProps = {
3
+ openFilePicker: () => void;
4
+ dragOver: boolean;
5
+ disabled: boolean;
6
+ };
7
+ export type FileInputProperties = {
8
+ /** Content slot — receives { openFilePicker, dragOver, disabled } so consumers build any visual they need. */
9
+ trigger: Snippet<[FileInputSnippetProps]>;
10
+ accept?: string;
11
+ multiple?: boolean;
12
+ maxSizeBytes?: number;
13
+ disabled?: boolean;
14
+ testId?: string;
15
+ classes?: string;
16
+ onfiles?: (files: File[]) => void;
17
+ onerror?: (message: string) => void;
18
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -57,7 +57,7 @@
57
57
  }
58
58
 
59
59
  function handleOverlayClick(event: MouseEvent) {
60
- if (event.target && event.target === overlayDiv) {
60
+ if (event.target === overlayDiv) {
61
61
  debounce(() => {
62
62
  onoverlayClick?.();
63
63
  });
@@ -110,7 +110,7 @@
110
110
  <div class="modal-content {size}">
111
111
  {#if (typeof header?.leftImage === 'string' && header.leftImage.length > 0) || (typeof header?.text === 'string' && header.text.length > 0) || (typeof header?.rightImage === 'string' && header.rightImage.length > 0)}
112
112
  <div class="header">
113
- {#if header.leftImage}
113
+ {#if typeof header.leftImage === 'string' && header.leftImage.length > 0}
114
114
  <div
115
115
  onclick={handleLeftImageClick}
116
116
  {onkeydown}
@@ -121,12 +121,12 @@
121
121
  <img class="header-left-img" src={header.leftImage} alt="" />
122
122
  </div>
123
123
  {/if}
124
- {#if header.text}
124
+ {#if typeof header.text === 'string' && header.text.length > 0}
125
125
  <div class="header-text" data-pw={header.testId}>
126
126
  {header.text}
127
127
  </div>
128
128
  {/if}
129
- {#if header.rightImage}
129
+ {#if typeof header.rightImage === 'string' && header.rightImage.length > 0}
130
130
  <div
131
131
  role="button"
132
132
  tabindex="0"
@@ -255,6 +255,7 @@
255
255
  padding: var(--modal-header-padding, 18px 20px);
256
256
  border-radius: var(--modal-header-border-radius, 0px);
257
257
  border-bottom: var(--modal-header-border-bottom, none);
258
+ align-items: var(--modal-header-align-items, center);
258
259
  }
259
260
 
260
261
  .footer-content {
@@ -8,7 +8,11 @@
8
8
  disabled = false,
9
9
  testId,
10
10
  onchange,
11
- classes
11
+ classes,
12
+ hasMore = false,
13
+ prevButtonTestId,
14
+ nextButtonTestId,
15
+ onLoadMore
12
16
  }: PaginationProperties = $props();
13
17
 
14
18
  function generatePages(total: number, current: number, siblings: number): (number | '...')[] {
@@ -40,24 +44,39 @@
40
44
  let pages = $derived(generatePages(totalPages, currentPage, siblingCount));
41
45
 
42
46
  function goToPage(page: number): void {
43
- if (disabled || page < 1 || page > totalPages || page === currentPage) {
47
+ const isForwardAllowed = hasMore || page <= totalPages;
48
+ if (disabled || page < 1 || !isForwardAllowed || page === currentPage) {
44
49
  return;
45
50
  }
46
51
  currentPage = page;
47
52
  onchange?.(page);
48
53
  }
54
+
55
+ let isNextDisabled = $derived(disabled || (!hasMore && currentPage >= totalPages));
56
+ // Cursor mode: on the last known page with more to load, swap the next-button
57
+ // for a load-more CTA.
58
+ let isLoadMore = $derived(hasMore && currentPage >= totalPages);
59
+
60
+ function loadMore(): void {
61
+ if (disabled) {
62
+ return;
63
+ }
64
+ currentPage = currentPage + 1;
65
+ onLoadMore?.();
66
+ }
49
67
  </script>
50
68
 
51
69
  <nav
52
70
  class="pagination {classes ?? ''}"
53
71
  class:disabled
54
- data-pw={typeof testId === 'string' ? testId : null}
72
+ data-pw={typeof testId === 'string' && testId.length > 0 ? testId : null}
55
73
  >
56
74
  <button
57
75
  class="page-button prev-button"
58
76
  disabled={disabled || currentPage <= 1}
59
77
  onclick={() => goToPage(currentPage - 1)}
60
78
  aria-label="Previous page"
79
+ data-pw={typeof prevButtonTestId === 'string' ? prevButtonTestId : null}
61
80
  >
62
81
  &#8249;
63
82
  </button>
@@ -79,14 +98,27 @@
79
98
  {/if}
80
99
  {/each}
81
100
 
82
- <button
83
- class="page-button next-button"
84
- disabled={disabled || currentPage >= totalPages}
85
- onclick={() => goToPage(currentPage + 1)}
86
- aria-label="Next page"
87
- >
88
- &#8250;
89
- </button>
101
+ {#if isLoadMore}
102
+ <button
103
+ class="page-button load-more-button"
104
+ {disabled}
105
+ onclick={loadMore}
106
+ aria-label="Load more"
107
+ data-pw={typeof nextButtonTestId === 'string' ? nextButtonTestId : null}
108
+ >
109
+ &#8250;
110
+ </button>
111
+ {:else}
112
+ <button
113
+ class="page-button next-button"
114
+ disabled={isNextDisabled}
115
+ onclick={() => goToPage(currentPage + 1)}
116
+ aria-label="Next page"
117
+ data-pw={typeof nextButtonTestId === 'string' ? nextButtonTestId : null}
118
+ >
119
+ &#8250;
120
+ </button>
121
+ {/if}
90
122
  </nav>
91
123
 
92
124
  <style>
@@ -131,6 +163,19 @@
131
163
  font-weight: var(--pagination-active-font-weight, 600);
132
164
  }
133
165
 
166
+ .load-more-button {
167
+ width: var(--pagination-load-more-width, auto);
168
+ padding: var(--pagination-load-more-padding, 6px 14px);
169
+ color: var(--pagination-load-more-color, #3a4550);
170
+ background: var(--pagination-load-more-background, transparent);
171
+ border-color: var(--pagination-load-more-border-color, #d1d5db);
172
+ }
173
+
174
+ .load-more-button:hover:not(:disabled) {
175
+ color: var(--pagination-load-more-hover-color, #111827);
176
+ background: var(--pagination-load-more-hover-background, #f3f4f6);
177
+ }
178
+
134
179
  .page-button:disabled {
135
180
  opacity: var(--pagination-disabled-opacity, 0.5);
136
181
  cursor: var(--pagination-disabled-cursor, not-allowed);
@@ -8,7 +8,22 @@ export type OptionalPaginationProperties = {
8
8
  disabled?: boolean;
9
9
  testId?: string;
10
10
  classes?: string;
11
+ /**
12
+ * Cursor-pagination hint. When `true`, the next button stays enabled even
13
+ * when `currentPage >= totalPages` — use this when the total page count is
14
+ * not known ahead of time (e.g. cursor-based APIs). `totalPages` takes
15
+ * precedence when both are provided: the next button is enabled only if
16
+ * `hasMore` is `true` OR `currentPage < totalPages`. In cursor mode the
17
+ * next-button becomes a load-more CTA on the last known page (see onLoadMore).
18
+ */
19
+ hasMore?: boolean;
20
+ /** `data-pw` test-id for the previous-page button. */
21
+ prevButtonTestId?: string;
22
+ /** `data-pw` test-id for the next-page button. */
23
+ nextButtonTestId?: string;
11
24
  };
12
25
  export type PaginationEventProperties = {
13
26
  onchange?: (page: number) => void;
27
+ /** Fired when the load-more CTA is clicked (cursor mode). */
28
+ onLoadMore?: () => void;
14
29
  };
@@ -9,6 +9,7 @@
9
9
  disabled = false,
10
10
  testId,
11
11
  dismissIcon,
12
+ leadingIcon,
12
13
  onclick,
13
14
  ondismiss,
14
15
  classes
@@ -52,6 +53,9 @@
52
53
  aria-disabled={interactive && disabled ? true : null}
53
54
  data-pw={typeof testId === 'string' ? testId : null}
54
55
  >
56
+ {#if typeof leadingIcon === 'function'}
57
+ <span class="pill-leading-icon">{@render leadingIcon()}</span>
58
+ {/if}
55
59
  <span class="pill-text">{text}</span>
56
60
  {#if dismissible}
57
61
  <div class="pill-dismiss">
@@ -106,6 +110,12 @@
106
110
  white-space: nowrap;
107
111
  }
108
112
 
113
+ .pill-leading-icon {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ flex-shrink: 0;
117
+ }
118
+
109
119
  .pill-dismiss {
110
120
  --button-color: transparent;
111
121
  --button-border: none;
@@ -8,6 +8,13 @@ export type OptionalPillProperties = {
8
8
  disabled?: boolean;
9
9
  testId?: string;
10
10
  dismissIcon?: Snippet;
11
+ /**
12
+ * A Svelte snippet rendered immediately before the text label inside a
13
+ * `<span class="pill-leading-icon">` wrapper. Use for icons, logos, or any
14
+ * inline decoration. The wrapper does not receive `aria-hidden` — leave
15
+ * accessibility attributes on the icon itself.
16
+ */
17
+ leadingIcon?: Snippet;
11
18
  classes?: string;
12
19
  };
13
20
  export type PillEventProperties = {