@juspay/svelte-ui-components 2.41.0 → 2.43.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,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { DateRangePickerProperties, DateRangePreset } from './properties';
3
- import { untrack } from 'svelte';
3
+ import { tick, untrack } from 'svelte';
4
4
  import { SvelteDate } from 'svelte/reactivity';
5
5
  import Calendar from '../Calendar/Calendar.svelte';
6
6
  import Button from '../Button/Button.svelte';
@@ -30,6 +30,8 @@
30
30
  triggerIcon,
31
31
  clearable = false,
32
32
  initialPresetLabel,
33
+ compareTrigger,
34
+ openCompare = $bindable(false),
33
35
  onapply,
34
36
  onapplysingle,
35
37
  onapplycompare,
@@ -87,6 +89,10 @@
87
89
  let isOpen: boolean = $state(false);
88
90
  let panelRef: HTMLDivElement | null = $state(null);
89
91
  let triggerRef: HTMLDivElement | null = $state(null);
92
+ let comparePanelRef: HTMLDivElement | null = $state(null);
93
+ let compareTriggerRef: HTMLDivElement | null = $state(null);
94
+ // Stores the element that opened the compare panel so focus can be restored on close.
95
+ let compareFocusReturnEl: HTMLElement | null = null;
90
96
 
91
97
  // Tracks the active preset label for the trigger display (seeded from initialPresetLabel on mount)
92
98
  const resolvedInitialPresetLabel: string | null = untrack(() => {
@@ -104,15 +110,15 @@
104
110
 
105
111
  let activePresetLabel: string | null = $state(resolvedInitialPresetLabel);
106
112
 
113
+ const now = new SvelteDate();
114
+
107
115
  // Dual-month navigation: track the year+month of the left calendar
108
- let leftYear: number = $state(new SvelteDate().getFullYear());
109
- let leftMonth: number = $state(new SvelteDate().getMonth());
116
+ let leftYear: number = $state(now.getFullYear());
117
+ let leftMonth: number = $state(now.getMonth());
110
118
 
111
119
  // A key counter — incrementing forces Calendar to re-mount with new initialMonth
112
120
  let calendarKey: number = $state(0);
113
121
 
114
- const now = new SvelteDate();
115
-
116
122
  const leftInitialMonth: Date = $derived(new SvelteDate(leftYear, leftMonth, 1));
117
123
  const rightInitialMonth: Date = $derived(new SvelteDate(leftYear, leftMonth + 1, 1));
118
124
 
@@ -145,6 +151,13 @@
145
151
  return placeholder;
146
152
  });
147
153
 
154
+ const compareTriggerLabel: string = $derived.by(() => {
155
+ if (compareStart !== null && compareEnd !== null) {
156
+ return `${formatDate(compareStart)} – ${formatDate(compareEnd)}`;
157
+ }
158
+ return placeholder;
159
+ });
160
+
148
161
  function openPicker(): void {
149
162
  // Seed draft from current committed values
150
163
  draftStart = rangeStart !== null ? rangeStart : null;
@@ -171,6 +184,46 @@
171
184
  onopentoggle?.({ open: false });
172
185
  }
173
186
 
187
+ function openComparePicker(): void {
188
+ compareFocusReturnEl =
189
+ document.activeElement instanceof HTMLElement ? document.activeElement : null;
190
+ draftCompareStart = compareStart ?? null;
191
+ draftCompareEnd = compareEnd ?? null;
192
+ openCompare = true;
193
+ tick().then(() => {
194
+ const firstFocusable = comparePanelRef?.querySelector<HTMLElement>(
195
+ 'button:not([disabled]), [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
196
+ );
197
+ firstFocusable?.focus();
198
+ });
199
+ }
200
+
201
+ function closeComparePicker(): void {
202
+ openCompare = false;
203
+ const returnEl = compareFocusReturnEl;
204
+ compareFocusReturnEl = null;
205
+ tick().then(() => {
206
+ returnEl?.focus();
207
+ });
208
+ }
209
+
210
+ function handleApplyCompare(): void {
211
+ if (draftCompareStart !== null && draftCompareEnd !== null) {
212
+ compareStart = draftCompareStart;
213
+ compareEnd = draftCompareEnd;
214
+ onapplycompare?.({
215
+ compareStart: draftCompareStart,
216
+ compareEnd: draftCompareEnd,
217
+ presetLabel: selectedPresetLabel
218
+ });
219
+ }
220
+ closeComparePicker();
221
+ }
222
+
223
+ function handleCancelCompare(): void {
224
+ closeComparePicker();
225
+ }
226
+
174
227
  function navigateDualMonths(delta: number): void {
175
228
  const next = new SvelteDate(leftYear, leftMonth + delta, 1);
176
229
  leftYear = next.getFullYear();
@@ -253,7 +306,7 @@
253
306
 
254
307
  // Close on outside-click
255
308
  function handleDocumentClick(event: MouseEvent): void {
256
- if (!isOpen) {
309
+ if (!isOpen && !openCompare) {
257
310
  return;
258
311
  }
259
312
  const target = event.target;
@@ -262,14 +315,35 @@
262
315
  }
263
316
  const clickedInsidePanel = panelRef !== null && panelRef.contains(target);
264
317
  const clickedTrigger = triggerRef !== null && triggerRef.contains(target);
265
- if (!clickedInsidePanel && !clickedTrigger) {
318
+ const clickedInsideComparePanel = comparePanelRef !== null && comparePanelRef.contains(target);
319
+ const clickedCompareTrigger = compareTriggerRef !== null && compareTriggerRef.contains(target);
320
+ if (
321
+ isOpen &&
322
+ !clickedInsidePanel &&
323
+ !clickedTrigger &&
324
+ !clickedInsideComparePanel &&
325
+ !clickedCompareTrigger
326
+ ) {
266
327
  closePicker();
267
328
  }
329
+ if (
330
+ openCompare &&
331
+ !clickedInsideComparePanel &&
332
+ !clickedCompareTrigger &&
333
+ !clickedInsidePanel &&
334
+ !clickedTrigger
335
+ ) {
336
+ closeComparePicker();
337
+ }
268
338
  }
269
339
 
270
340
  function handleDocumentKeyDown(event: KeyboardEvent): void {
271
- if (event.key === 'Escape' && isOpen) {
272
- handleCancel();
341
+ if (event.key === 'Escape') {
342
+ if (openCompare) {
343
+ handleCancelCompare();
344
+ } else if (isOpen) {
345
+ handleCancel();
346
+ }
273
347
  }
274
348
  }
275
349
 
@@ -340,6 +414,75 @@
340
414
  </Button>
341
415
  </div>
342
416
 
417
+ <!-- Compare trigger wrapper (standalone): position:relative so the compare panel
418
+ anchors below this trigger rather than the drp-root corner. -->
419
+ {#if typeof compareTrigger === 'function'}
420
+ <div bind:this={compareTriggerRef} class="drp-compare-trigger-wrapper">
421
+ <Button
422
+ onclick={openComparePicker}
423
+ ariaLabel="Open compare period picker"
424
+ classes="drp-compare-trigger {openCompare ? 'drp-compare-trigger-open' : ''}"
425
+ >
426
+ {@render compareTrigger(compareTriggerLabel)}
427
+ </Button>
428
+
429
+ <!-- Compare period picker panel (standalone): nested here so position:absolute
430
+ resolves against .drp-compare-trigger-wrapper, not .drp-root. -->
431
+ {#if openCompare}
432
+ <div
433
+ bind:this={comparePanelRef}
434
+ class="drp-compare-panel"
435
+ role="dialog"
436
+ aria-label="Compare period picker"
437
+ aria-modal="true"
438
+ tabindex="-1"
439
+ onkeydown={(event) => {
440
+ if (event.key === 'Tab') {
441
+ const focusable = comparePanelRef?.querySelectorAll<HTMLElement>(
442
+ 'button:not([disabled]), [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
443
+ );
444
+ if (!focusable || focusable.length === 0) {
445
+ return;
446
+ }
447
+ const first = focusable[0];
448
+ const last = focusable[focusable.length - 1];
449
+ if (event.shiftKey) {
450
+ if (document.activeElement === first) {
451
+ event.preventDefault();
452
+ last.focus();
453
+ }
454
+ } else {
455
+ if (document.activeElement === last) {
456
+ event.preventDefault();
457
+ first.focus();
458
+ }
459
+ }
460
+ }
461
+ }}
462
+ >
463
+ {#if typeof compareCalendar === 'function'}
464
+ <div class="drp-compare-panel-body">
465
+ {@render compareCalendar()}
466
+ </div>
467
+ {/if}
468
+ <div class="drp-footer">
469
+ <Button
470
+ onclick={handleCancelCompare}
471
+ ariaLabel="Cancel compare selection"
472
+ classes="drp-btn-cancel">Cancel</Button
473
+ >
474
+ <Button
475
+ onclick={handleApplyCompare}
476
+ ariaLabel="Apply compare selection"
477
+ classes="drp-btn-apply"
478
+ disabled={draftCompareStart === null || draftCompareEnd === null}>Apply</Button
479
+ >
480
+ </div>
481
+ </div>
482
+ {/if}
483
+ </div>
484
+ {/if}
485
+
343
486
  <!-- Dropdown panel -->
344
487
  {#if isOpen}
345
488
  <div
@@ -464,8 +607,11 @@
464
607
  </div>
465
608
  {/if}
466
609
 
467
- <!-- Compare calendar slot: consumer controls all compare UI -->
468
- {#if typeof compareCalendar === 'function'}
610
+ <!-- Compare calendar slot: only rendered here when there is no standalone
611
+ compareTrigger. When compareTrigger is provided the consumer uses the
612
+ separate compare panel; rendering the same Snippet in two places at once
613
+ causes a Svelte 5 runtime error. -->
614
+ {#if typeof compareCalendar === 'function' && typeof compareTrigger !== 'function'}
469
615
  <div class="drp-compare-section">
470
616
  {@render compareCalendar()}
471
617
  </div>
@@ -516,20 +662,22 @@
516
662
  --button-padding: var(--drp-trigger-padding, 8px 12px);
517
663
  --button-min-width: var(--drp-trigger-min-width, 200px);
518
664
  --button-gap: var(--drp-trigger-gap, 8px);
519
- --button-hover-border-color: var(--drp-trigger-hover-border-color, currentColor);
665
+ --button-hover-border: var(
666
+ --drp-trigger-hover-border,
667
+ var(--drp-trigger-border, 1px solid currentColor)
668
+ );
520
669
  }
521
670
 
522
- :global(.drp-trigger) {
523
- display: inline-flex !important;
671
+ :global(.drp-trigger-wrapper .drp-trigger) {
672
+ display: inline-flex;
524
673
  align-items: center;
525
674
  gap: var(--drp-trigger-gap, 8px);
526
675
  white-space: nowrap;
527
676
  min-width: var(--drp-trigger-min-width, 200px);
528
- font-family: inherit;
529
677
  }
530
678
 
531
- :global(.drp-trigger-open) {
532
- border-color: var(--drp-trigger-open-border-color, #000000) !important;
679
+ :global(.drp-trigger-wrapper .drp-trigger-open) {
680
+ border-color: var(--drp-trigger-open-border-color, #000000);
533
681
  box-shadow: var(--drp-trigger-open-shadow, 0 0 0 2px rgba(0, 0, 0, 0.1));
534
682
  }
535
683
 
@@ -591,7 +739,6 @@
591
739
  cursor: pointer;
592
740
  white-space: nowrap;
593
741
  transition: background 0.12s ease;
594
- font-family: inherit;
595
742
  }
596
743
 
597
744
  .drp-preset-item:hover {
@@ -778,7 +925,44 @@
778
925
  --button-border: none;
779
926
  --button-text-color: var(--drp-apply-color, #ffffff);
780
927
  --button-hover-color: var(--drp-apply-hover-background, #333333);
781
- --button-disabled-color: var(--drp-apply-disabled-background, #cccccc);
782
- --button-disabled-text-color: var(--drp-apply-disabled-color, #888888);
928
+ --disabled-background-color: var(--drp-apply-disabled-background, #cccccc);
929
+ --disabled-text-color: var(--drp-apply-disabled-color, #888888);
930
+ }
931
+
932
+ /* ── Compare standalone trigger ── */
933
+ .drp-compare-trigger-wrapper {
934
+ display: inline-block;
935
+ position: relative;
936
+ --button-color: var(--drp-compare-trigger-background, inherit);
937
+ --button-border: var(--drp-compare-trigger-border, 1px solid currentColor);
938
+ --button-border-radius: var(--drp-compare-trigger-border-radius, 6px);
939
+ --button-text-color: var(--drp-compare-trigger-color, inherit);
940
+ --button-padding: var(--drp-compare-trigger-padding, 8px 12px);
941
+ --button-min-width: var(--drp-compare-trigger-min-width, 160px);
942
+ }
943
+
944
+ :global(.drp-compare-trigger-open) {
945
+ border-color: var(--drp-trigger-open-border-color, #000000);
946
+ box-shadow: var(--drp-trigger-open-shadow, 0 0 0 2px rgba(0, 0, 0, 0.1));
947
+ }
948
+
949
+ /* ── Compare standalone panel ── */
950
+ .drp-compare-panel {
951
+ position: absolute;
952
+ top: calc(100% + var(--drp-panel-offset, 6px));
953
+ left: var(--drp-compare-panel-left, 0);
954
+ z-index: var(--drp-panel-z-index, 1000);
955
+ background: var(--drp-panel-background, inherit);
956
+ border: var(--drp-panel-border, 1px solid #e0e0e0);
957
+ border-radius: var(--drp-panel-border-radius, 10px);
958
+ box-shadow: var(--drp-panel-shadow, 0 8px 24px rgba(0, 0, 0, 0.12));
959
+ display: flex;
960
+ flex-direction: column;
961
+ min-width: var(--drp-compare-panel-min-width, 280px);
962
+ overflow: hidden;
963
+ }
964
+
965
+ .drp-compare-panel-body {
966
+ padding: var(--drp-calendars-padding, 16px);
783
967
  }
784
968
  </style>
@@ -1,4 +1,4 @@
1
1
  import type { DateRangePickerProperties } from './properties';
2
- declare const DateRangePicker: import("svelte").Component<DateRangePickerProperties, {}, "value" | "rangeStart" | "rangeEnd" | "compareStart" | "compareEnd">;
2
+ declare const DateRangePicker: import("svelte").Component<DateRangePickerProperties, {}, "value" | "rangeStart" | "rangeEnd" | "compareStart" | "compareEnd" | "openCompare">;
3
3
  type DateRangePicker = ReturnType<typeof DateRangePicker>;
4
4
  export default DateRangePicker;
@@ -70,6 +70,23 @@ export type OptionalDateRangePickerProperties = {
70
70
  * Only takes effect once on mount; subsequent prop changes are not tracked.
71
71
  */
72
72
  initialPresetLabel?: string;
73
+ /**
74
+ * Snippet rendered as the standalone compare-period trigger. When provided,
75
+ * the component renders this snippet in its own wrapper div
76
+ * (.drp-compare-trigger-wrapper) adjacent to the main trigger. Clicking the
77
+ * wrapper toggles openCompare. Receives the current compare trigger label
78
+ * string as its argument (formatted "start – end" or placeholder).
79
+ */
80
+ compareTrigger?: Snippet<[string]>;
81
+ /**
82
+ * Bindable. Controls whether the compare-period picker panel is open.
83
+ * The component writes back on open/close, so `bind:openCompare` lets a
84
+ * parent open/close it programmatically or observe the state. When
85
+ * compareTrigger is not provided this prop is still functional — the parent
86
+ * can drive it without a built-in trigger element.
87
+ * Default: false.
88
+ */
89
+ openCompare?: boolean;
73
90
  };
74
91
  export type DateRangePickerEventProperties = {
75
92
  /** Fired when the user clicks Apply in range mode. presetLabel is the sidebar preset name if one was active, or null for a custom calendar selection. */
@@ -17,9 +17,12 @@
17
17
  testId,
18
18
  itemTestId,
19
19
  onchange,
20
+ onopen,
21
+ onclose,
20
22
  classes,
21
23
  open = $bindable(false),
22
- dropdownAlign = 'left'
24
+ dropdownAlign = 'left',
25
+ hierarchy = 'default'
23
26
  }: SelectProperties = $props();
24
27
 
25
28
  function normalizeItems(source: SelectItem[] | string[]): SelectItem[] {
@@ -65,6 +68,7 @@
65
68
  return;
66
69
  }
67
70
  open = true;
71
+ onopen?.();
68
72
  highlightedIndex = -1;
69
73
  query = '';
70
74
  if (searchable) {
@@ -77,6 +81,7 @@
77
81
 
78
82
  function close(): void {
79
83
  open = false;
84
+ onclose?.();
80
85
  query = '';
81
86
  highlightedIndex = -1;
82
87
  }
@@ -208,7 +213,7 @@
208
213
  }
209
214
  query = event.target.value;
210
215
  if (!open) {
211
- open = true;
216
+ openDropdown();
212
217
  }
213
218
  highlightedIndex = -1;
214
219
  }
@@ -233,6 +238,9 @@
233
238
  document.addEventListener('click', handleClickOutside);
234
239
  return () => {
235
240
  document.removeEventListener('click', handleClickOutside);
241
+ if (open) {
242
+ onclose?.();
243
+ }
236
244
  };
237
245
  });
238
246
  </script>
@@ -241,6 +249,7 @@
241
249
  class="select {classes ?? ''}"
242
250
  class:open
243
251
  class:disabled
252
+ class:ghost={hierarchy === 'ghost'}
244
253
  bind:this={containerEl}
245
254
  {...typeof testId === 'string' ? { 'data-pw': testId } : {}}
246
255
  >
@@ -552,4 +561,22 @@
552
561
  --pill-padding: var(--select-pill-padding, 2px 8px);
553
562
  --pill-font-size: var(--select-pill-font-size, 13px);
554
563
  }
564
+
565
+ /* Ghost hierarchy: transparent, borderless trigger */
566
+ .select.ghost .select-trigger {
567
+ background: var(--select-ghost-trigger-background, transparent);
568
+ border-color: var(--select-ghost-trigger-border-color, transparent);
569
+ box-shadow: none;
570
+ }
571
+
572
+ .select.ghost .select-trigger:hover:not(.disabled .select-trigger) {
573
+ border-color: var(--select-ghost-trigger-hover-border-color, transparent);
574
+ background: var(--select-ghost-trigger-hover-background, rgba(0, 0, 0, 0.04));
575
+ }
576
+
577
+ .select.ghost.open .select-trigger {
578
+ border-color: var(--select-ghost-trigger-open-border-color, transparent);
579
+ background: var(--select-ghost-trigger-open-background, rgba(0, 0, 0, 0.06));
580
+ box-shadow: none;
581
+ }
555
582
  </style>
@@ -9,6 +9,7 @@ export type SelectItem = {
9
9
  export type MandatorySelectProperties = {
10
10
  items: SelectItem[] | string[];
11
11
  };
12
+ export type SelectHierarchy = 'default' | 'ghost';
12
13
  export type OptionalSelectProperties = {
13
14
  value?: string[];
14
15
  multiple?: boolean;
@@ -32,7 +33,11 @@ export type OptionalSelectProperties = {
32
33
  value: string[];
33
34
  items: SelectItem[];
34
35
  }]>;
36
+ /** Visual hierarchy of the trigger. `'ghost'` renders a transparent, borderless trigger — useful when the Select is embedded in a toolbar or header where a full bordered input would be visually heavy. Defaults to `'default'`. */
37
+ hierarchy?: SelectHierarchy;
35
38
  };
36
39
  export type SelectEventProperties = {
37
40
  onchange?: (value: string[]) => void;
41
+ onopen?: () => void;
42
+ onclose?: () => void;
38
43
  };
@@ -1,20 +1,33 @@
1
1
  <script lang="ts">
2
2
  import type { ToggleProperties } from './properties';
3
3
 
4
- let { checked = false, text = '', classes, onclick }: ToggleProperties = $props();
4
+ let {
5
+ checked = false,
6
+ text = '',
7
+ disabled = false,
8
+ testId,
9
+ classes,
10
+ onclick
11
+ }: ToggleProperties = $props();
5
12
 
6
- function handleCheckboxClick(e: MouseEvent): void {
13
+ const handleCheckboxClick = (e: MouseEvent): void => {
7
14
  if (e.target instanceof HTMLInputElement && typeof e.target.checked === 'boolean') {
8
15
  checked = e.target.checked;
9
16
  }
10
17
  onclick?.(checked);
11
- }
18
+ };
12
19
  </script>
13
20
 
14
- <div class="container {classes ?? ''}">
21
+ <div class="container {classes ?? ''}" class:disabled aria-disabled={disabled} data-pw={testId}>
15
22
  <div class="text" hidden={text.length === 0}>{text}</div>
16
23
  <label class="switch">
17
- <input class="input-checkbox" type="checkbox" {checked} onclick={handleCheckboxClick} />
24
+ <input
25
+ class="input-checkbox"
26
+ type="checkbox"
27
+ {checked}
28
+ {disabled}
29
+ onclick={handleCheckboxClick}
30
+ />
18
31
  <span class="slider round"></span>
19
32
  </label>
20
33
  </div>
@@ -26,6 +39,12 @@
26
39
  gap: var(--toggle-container-gap, 8px);
27
40
  }
28
41
 
42
+ .container.disabled {
43
+ opacity: var(--toggle-disabled-opacity, 0.4);
44
+ cursor: not-allowed;
45
+ pointer-events: none;
46
+ }
47
+
29
48
  .text {
30
49
  font-size: var(--toggle-text-font-size, 14px);
31
50
  font-weight: var(--toggle-text-font-weight, 400);
@@ -1,6 +1,9 @@
1
- export type ToggleProperties = ToggleEventProperties & {
1
+ export type ToggleProperties = OptionalToggleProperties & ToggleEventProperties;
2
+ export type OptionalToggleProperties = {
3
+ text?: string;
2
4
  checked?: boolean;
3
- text: string;
5
+ disabled?: boolean;
6
+ testId?: string;
4
7
  classes?: string;
5
8
  };
6
9
  export type ToggleEventProperties = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.41.0",
3
+ "version": "2.43.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",