@juspay/svelte-ui-components 2.32.1 → 2.34.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,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { DateRangePickerProperties, DateRangePreset } from './properties';
3
+ import { untrack } from 'svelte';
3
4
  import { SvelteDate } from 'svelte/reactivity';
4
5
  import Calendar from '../Calendar/Calendar.svelte';
5
6
  import Button from '../Button/Button.svelte';
@@ -27,11 +28,14 @@
27
28
  classes,
28
29
  triggerSnippet,
29
30
  triggerIcon,
31
+ clearable = false,
32
+ initialPresetLabel,
30
33
  onapply,
31
34
  onapplysingle,
32
35
  onapplycompare,
33
36
  oncancel,
34
- onopentoggle
37
+ onopentoggle,
38
+ onclear
35
39
  }: DateRangePickerProperties = $props();
36
40
 
37
41
  const isDualMonth: boolean = $derived(
@@ -84,6 +88,22 @@
84
88
  let panelRef: HTMLDivElement | null = $state(null);
85
89
  let triggerRef: HTMLDivElement | null = $state(null);
86
90
 
91
+ // Tracks the active preset label for the trigger display (seeded from initialPresetLabel on mount)
92
+ const resolvedInitialPresetLabel: string | null = untrack(() => {
93
+ if (
94
+ typeof initialPresetLabel === 'string' &&
95
+ initialPresetLabel !== '' &&
96
+ presets !== null &&
97
+ presets.length > 0
98
+ ) {
99
+ const matched = presets.find((preset) => preset.label === initialPresetLabel);
100
+ return matched ? matched.label : null;
101
+ }
102
+ return null;
103
+ });
104
+
105
+ let activePresetLabel: string | null = $state(resolvedInitialPresetLabel);
106
+
87
107
  // Dual-month navigation: track the year+month of the left calendar
88
108
  let leftYear: number = $state(new SvelteDate().getFullYear());
89
109
  let leftMonth: number = $state(new SvelteDate().getMonth());
@@ -109,6 +129,10 @@
109
129
  }
110
130
 
111
131
  const triggerLabel: string = $derived.by(() => {
132
+ // If an initial preset label is active (seeded on mount, not yet overridden), show it
133
+ if (activePresetLabel !== null) {
134
+ return activePresetLabel;
135
+ }
112
136
  if (mode === 'single') {
113
137
  return value !== null ? formatDate(value) : placeholder;
114
138
  }
@@ -155,6 +179,8 @@
155
179
  }
156
180
 
157
181
  function handleApply(): void {
182
+ // Once the user explicitly applies, clear the initial-preset display label
183
+ activePresetLabel = null;
158
184
  if (mode === 'range') {
159
185
  if (draftStart !== null && draftEnd !== null) {
160
186
  rangeStart = draftStart;
@@ -183,12 +209,21 @@
183
209
  closePicker();
184
210
  }
185
211
 
212
+ function handleClear(): void {
213
+ value = null;
214
+ draftValue = null;
215
+ onclear?.();
216
+ closePicker();
217
+ }
218
+
186
219
  function handleCancel(): void {
187
220
  oncancel?.();
188
221
  closePicker();
189
222
  }
190
223
 
191
224
  function handlePreset(preset: DateRangePreset): void {
225
+ // User explicitly picked a preset — clear the initial-preset display label
226
+ activePresetLabel = null;
192
227
  const { start, end } = preset.getValue();
193
228
  selectedPresetLabel = preset.label;
194
229
  if (mode === 'range') {
@@ -246,6 +281,16 @@
246
281
  });
247
282
 
248
283
  function isPresetActive(preset: DateRangePreset): boolean {
284
+ // When an initial preset label is active and the user hasn't picked anything yet,
285
+ // highlight the matching preset in the sidebar by label match
286
+ if (
287
+ activePresetLabel !== null &&
288
+ draftStart === null &&
289
+ draftEnd === null &&
290
+ draftValue === null
291
+ ) {
292
+ return preset.label === activePresetLabel;
293
+ }
249
294
  if (mode === 'single') {
250
295
  if (draftValue === null) {
251
296
  return false;
@@ -308,7 +353,19 @@
308
353
  <!-- Preset sidebar -->
309
354
  {#if presets !== null && presets.length > 0}
310
355
  <div class="drp-sidebar" role="listbox" aria-label="Date presets">
311
- {#each presets as preset (preset.label)}
356
+ {#each presets as preset, presetIndex (preset.label)}
357
+ {@const previousPreset = presetIndex > 0 ? presets[presetIndex - 1] : null}
358
+ {@const groupChanged =
359
+ presetIndex > 0 &&
360
+ 'group' in preset &&
361
+ (previousPreset === null || previousPreset.group !== preset.group)}
362
+ {#if groupChanged}
363
+ <div class="drp-preset-divider" role="separator" aria-hidden="true">
364
+ {#if preset.group !== ''}
365
+ <span class="drp-preset-group-label">{preset.group}</span>
366
+ {/if}
367
+ </div>
368
+ {/if}
312
369
  <button
313
370
  type="button"
314
371
  class="drp-preset-item"
@@ -418,6 +475,11 @@
418
475
 
419
476
  <!-- Footer -->
420
477
  <div class="drp-footer">
478
+ {#if clearable && mode === 'single' && value !== null}
479
+ <Button onclick={handleClear} ariaLabel="Clear date selection" classes="drp-btn-clear">
480
+ Clear
481
+ </Button>
482
+ {/if}
421
483
  <Button onclick={handleCancel} ariaLabel="Cancel date selection" classes="drp-btn-cancel">
422
484
  Cancel
423
485
  </Button>
@@ -655,6 +717,44 @@
655
717
  border-top: var(--drp-footer-border, 1px solid #e8e8e8);
656
718
  }
657
719
 
720
+ /* ── Preset group dividers ── */
721
+ .drp-preset-divider {
722
+ display: flex;
723
+ align-items: center;
724
+ gap: var(--drp-preset-divider-gap, 6px);
725
+ margin: var(--drp-preset-divider-margin, 4px 0);
726
+ padding: 0 var(--drp-preset-padding-left, 12px);
727
+ }
728
+
729
+ .drp-preset-divider::before {
730
+ content: '';
731
+ flex: 1;
732
+ height: 0;
733
+ border-top: var(--drp-preset-divider-border, 1px solid #e8e8e8);
734
+ }
735
+
736
+ .drp-preset-group-label {
737
+ color: var(--drp-preset-group-label-color, #999999);
738
+ white-space: nowrap;
739
+ flex-shrink: 0;
740
+ }
741
+
742
+ .drp-preset-divider:has(.drp-preset-group-label) {
743
+ padding-right: var(--drp-preset-padding-right, 12px);
744
+ }
745
+
746
+ .drp-preset-divider:has(.drp-preset-group-label)::before {
747
+ flex: none;
748
+ width: var(--drp-preset-divider-leader-width, 8px);
749
+ }
750
+
751
+ .drp-preset-divider:has(.drp-preset-group-label)::after {
752
+ content: '';
753
+ flex: 1;
754
+ height: 0;
755
+ border-top: var(--drp-preset-divider-border, 1px solid #e8e8e8);
756
+ }
757
+
658
758
  /* Cancel button variant via Button's CSS vars */
659
759
  :global(.drp-btn-cancel) {
660
760
  --button-color: transparent;
@@ -663,6 +763,15 @@
663
763
  --button-hover-color: var(--drp-cancel-hover-background, #f5f5f5);
664
764
  }
665
765
 
766
+ /* Clear button variant (single-mode reset) */
767
+ :global(.drp-btn-clear) {
768
+ --button-color: transparent;
769
+ --button-border: 1px solid var(--drp-clear-border-color, #d0d0d0);
770
+ --button-text-color: var(--drp-clear-color, inherit);
771
+ --button-hover-color: var(--drp-clear-hover-background, #f5f5f5);
772
+ margin-right: auto;
773
+ }
774
+
666
775
  /* Apply button variant */
667
776
  :global(.drp-btn-apply) {
668
777
  --button-color: var(--drp-apply-background, currentColor);
@@ -5,6 +5,13 @@ export type DateRangePreset = {
5
5
  start: Date;
6
6
  end: Date;
7
7
  };
8
+ /**
9
+ * Optional group key. Consecutive presets sharing the same group key are
10
+ * visually grouped together. A thin divider (and optional group label) is
11
+ * rendered whenever the group key changes between two consecutive presets.
12
+ * Presets without a group field render exactly as before.
13
+ */
14
+ group?: string;
8
15
  };
9
16
  export type DateRangePickerMode = 'range' | 'single';
10
17
  export type OptionalDateRangePickerProperties = {
@@ -50,6 +57,19 @@ export type OptionalDateRangePickerProperties = {
50
57
  triggerSnippet?: Snippet<[string]>;
51
58
  /** Custom icon snippet for the trigger button. */
52
59
  triggerIcon?: Snippet;
60
+ /**
61
+ * When true (and mode='single'), shows a Clear button in the footer whenever a
62
+ * date is committed. Clicking Clear resets value to null and fires onclear.
63
+ * Has no effect in range mode. Default: false.
64
+ */
65
+ clearable?: boolean;
66
+ /**
67
+ * Label of the preset that should appear active on mount, without firing onapply.
68
+ * The matching preset's date range is seeded into the draft and the trigger shows
69
+ * the preset label. If no preset matches the string exactly, the prop is ignored.
70
+ * Only takes effect once on mount; subsequent prop changes are not tracked.
71
+ */
72
+ initialPresetLabel?: string;
53
73
  };
54
74
  export type DateRangePickerEventProperties = {
55
75
  /** 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. */
@@ -75,5 +95,10 @@ export type DateRangePickerEventProperties = {
75
95
  onopentoggle?: (event: {
76
96
  open: boolean;
77
97
  }) => void;
98
+ /**
99
+ * Fired when the user clicks the Clear button in single mode (requires clearable=true).
100
+ * value is reset to null before this callback is invoked.
101
+ */
102
+ onclear?: () => void;
78
103
  };
79
104
  export type DateRangePickerProperties = OptionalDateRangePickerProperties & DateRangePickerEventProperties;
@@ -471,8 +471,11 @@
471
471
  .select-dropdown {
472
472
  position: absolute;
473
473
  top: 100%;
474
- left: 0;
475
- right: 0;
474
+ left: var(--select-dropdown-left, 0);
475
+ right: var(--select-dropdown-right, 0);
476
+ min-width: var(--select-dropdown-min-width, auto);
477
+ max-width: var(--select-dropdown-max-width, none);
478
+ width: var(--select-dropdown-width, auto);
476
479
  margin-top: var(--select-dropdown-gap, 4px);
477
480
  background: var(--select-dropdown-background, #ffffff);
478
481
  border: var(--select-dropdown-border, 1px solid #cccccc);
@@ -485,9 +488,10 @@
485
488
 
486
489
  .select-dropdown-right {
487
490
  left: auto;
488
- right: 0;
489
- min-width: 100%;
490
- width: max-content;
491
+ right: var(--select-dropdown-right, 0);
492
+ min-width: var(--select-dropdown-min-width, 100%);
493
+ max-width: var(--select-dropdown-max-width, none);
494
+ width: var(--select-dropdown-width, max-content);
491
495
  }
492
496
 
493
497
  .select-option {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.32.1",
3
+ "version": "2.34.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",