@juspay/svelte-ui-components 2.111.1 → 2.111.2

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,9 @@
1
1
  <script lang="ts">
2
- import type { DateRangePickerProperties, DateRangePreset } from './properties';
2
+ import type {
3
+ DateRangePickerProperties,
4
+ DateRangePreset,
5
+ TimeDisplayBoundary
6
+ } from './properties';
3
7
  import { tick, untrack } from 'svelte';
4
8
  import { SvelteDate } from 'svelte/reactivity';
5
9
  import Calendar from '../Calendar/Calendar.svelte';
@@ -11,6 +15,7 @@
11
15
  TIME_DISPLAY_PATTERN,
12
16
  applyTimeDisplay,
13
17
  formatTimeDisplay,
18
+ parseDateDisplay,
14
19
  toMinutesOfDay
15
20
  } from './timeUtils';
16
21
 
@@ -80,6 +85,23 @@
80
85
  let endTimeDisplay: string = $state('11:59 PM');
81
86
  let showTimeRow: boolean = $state(false);
82
87
 
88
+ // Typed text for the built-in date inputs (showDateInputs, range mode). The displayed
89
+ // text is *derived* from draftStart/draftEnd — the single source of truth — so it can
90
+ // never go stale relative to a draft change made elsewhere (preset pick, calendar
91
+ // click, the initialPresetLabel seed effect, ...). While the user is actively editing
92
+ // a field, its edit buffer holds the in-progress text instead (a half-typed string
93
+ // must never reach the calendar grid or Apply's canApply check); the buffer resets to
94
+ // null — falling back to the canonical formatted date — on every successful or
95
+ // rejected commit, see commitTypedDate.
96
+ let startDateEditBuffer: string | null = $state(null);
97
+ let endDateEditBuffer: string | null = $state(null);
98
+ const startDateInputText: string = $derived(
99
+ startDateEditBuffer ?? (draftStart !== null ? formatDate(draftStart) : '')
100
+ );
101
+ const endDateInputText: string = $derived(
102
+ endDateEditBuffer ?? (draftEnd !== null ? formatDate(draftEnd) : '')
103
+ );
104
+
83
105
  // Inline time layout (timeSelectionLayout="inline"): the start/end time inputs
84
106
  // render beside their date inputs on the same row, always visible — no clock
85
107
  // toggle and no collapsible row. All seeding/validation/fold logic is shared
@@ -99,11 +121,14 @@
99
121
 
100
122
  // When maxRangeDays is set, disable any date whose span from the in-progress
101
123
  // start would exceed the limit (inclusive of both ends), until the range is
102
- // completed. Falls back to the raw disabledDates otherwise.
124
+ // completed. Falls back to the raw disabledDates otherwise. Always returns a
125
+ // callable predicate (rather than sometimes the raw Date[]/predicate union) so
126
+ // it can be called directly — by both the Calendar grid and isDateSelectable's
127
+ // typed-date validation — without a union-type call-signature error.
103
128
  const rangeConstrainedDisabledDates = $derived.by(() => {
104
129
  const limit = maxRangeDays;
105
130
  if (limit === null) {
106
- return disabledDates;
131
+ return (date: Date): boolean => isBaseDisabledDate(date);
107
132
  }
108
133
  const anchor = draftStart;
109
134
  const selecting = anchor !== null && draftEnd === null;
@@ -200,10 +225,6 @@
200
225
  return d.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });
201
226
  }
202
227
 
203
- // Read-only date-input box contents (opt-in via showDateInputs), reflecting the draft.
204
- const draftStartDateLabel: string = $derived(draftStart !== null ? formatDate(draftStart) : '');
205
- const draftEndDateLabel: string = $derived(draftEnd !== null ? formatDate(draftEnd) : '');
206
-
207
228
  const isStartTimeValid: boolean = $derived(TIME_DISPLAY_PATTERN.test(startTimeDisplay.trim()));
208
229
  const isEndTimeValid: boolean = $derived(TIME_DISPLAY_PATTERN.test(endTimeDisplay.trim()));
209
230
  // Apply is blocked while a time is malformed, or while both ends fall on the same
@@ -259,6 +280,13 @@
259
280
  // by label. A direct calendar click later clears this, reverting to date matching.
260
281
  selectedPresetLabel = committedPresetLabel;
261
282
 
283
+ // Discard any in-progress typed text from a previous session — the date-input
284
+ // display text is derived from draftStart/draftEnd above, so it already reflects
285
+ // whatever draft this open seeds (including a later re-seed from initialPresetLabel,
286
+ // which runs in an $effect.pre after this synchronous assignment).
287
+ startDateEditBuffer = null;
288
+ endDateEditBuffer = null;
289
+
262
290
  // Seed the time inputs from the committed range's time-of-day, and collapse the
263
291
  // time row so each open starts from the date view.
264
292
  if (showTimeSelection) {
@@ -407,6 +435,8 @@
407
435
  draftEnd = rangeEnd;
408
436
  draftValue = value;
409
437
  selectedPresetLabel = committedPresetLabel;
438
+ startDateEditBuffer = null;
439
+ endDateEditBuffer = null;
410
440
  const anchor = rangeStart !== null ? rangeStart : value !== null ? value : now;
411
441
  leftYear = anchor.getFullYear();
412
442
  leftMonth = anchor.getMonth();
@@ -420,6 +450,10 @@
420
450
  if (mode === 'range') {
421
451
  draftStart = start;
422
452
  draftEnd = end;
453
+ // Discard any in-progress typed text — the date-input display text is derived
454
+ // from draftStart/draftEnd, so it already reflects the preset's own start/end.
455
+ startDateEditBuffer = null;
456
+ endDateEditBuffer = null;
423
457
  // Reseed the time-of-day inputs from the preset's own start/end so a
424
458
  // time-bearing preset (e.g. "Last 30 minutes" / "Last 12 Hours") is not
425
459
  // silently overwritten by a stale display string in handleApply's
@@ -443,6 +477,9 @@
443
477
  draftEnd = event.rangeEnd;
444
478
  // A direct calendar click is not a preset selection.
445
479
  selectedPresetLabel = null;
480
+ // Discard any in-progress typed text in favor of the calendar's own selection.
481
+ startDateEditBuffer = null;
482
+ endDateEditBuffer = null;
446
483
  if (showTimeSelection) {
447
484
  startTimeDisplay = '12:00 AM';
448
485
  endTimeDisplay = isSameDay(event.rangeEnd, now) ? formatTimeDisplay(now) : '11:59 PM';
@@ -455,6 +492,177 @@
455
492
  selectedPresetLabel = null;
456
493
  }
457
494
 
495
+ // Whether a typed date passes the same minDate/maxDate/disabledDates/maxRangeDays
496
+ // constraints the calendar grid itself enforces — a typed date must never commit
497
+ // somewhere the grid would have refused to let the user click. Checks
498
+ // rangeConstrainedDisabledDates (not just the raw disabledDates) so a typed end date
499
+ // that would blow past maxRangeDays mid-selection is rejected exactly like a click on
500
+ // the same grid cell would be.
501
+ function isDateSelectable(date: Date): boolean {
502
+ if (minDate !== null) {
503
+ const normalizedMinDate = new SvelteDate(
504
+ minDate.getFullYear(),
505
+ minDate.getMonth(),
506
+ minDate.getDate()
507
+ );
508
+ if (date.getTime() < normalizedMinDate.getTime()) {
509
+ return false;
510
+ }
511
+ }
512
+ if (maxDate !== null) {
513
+ const normalizedMaxDate = new SvelteDate(
514
+ maxDate.getFullYear(),
515
+ maxDate.getMonth(),
516
+ maxDate.getDate()
517
+ );
518
+ if (date.getTime() > normalizedMaxDate.getTime()) {
519
+ return false;
520
+ }
521
+ }
522
+ return !rangeConstrainedDisabledDates(date);
523
+ }
524
+
525
+ // The single acceptance rule for a typed date, shared by the live invalid-border
526
+ // feedback and by commitTypedDate. Keeping one predicate is the point: while these
527
+ // were two separate expressions, the border could stay clean for a date that commit
528
+ // then silently rejected, leaving no cue as to why the field reverted.
529
+ //
530
+ // Three ways a parsed date is refused:
531
+ // 1. it isn't selectable at all (minDate/maxDate/disabled, and the maxRangeDays
532
+ // span while a range is mid-selection),
533
+ // 2. it crosses the opposite boundary (a start after the end, or vice versa),
534
+ // 3. it would stretch an *already-complete* range past maxRangeDays.
535
+ // Case 3 is not covered by isDateSelectable: rangeConstrainedDisabledDates only
536
+ // applies its span check while `selecting` is true (draftStart set, draftEnd still
537
+ // null). Once both boundaries exist that guard goes quiet, so retyping either one
538
+ // could otherwise commit a range longer than the calendar grid would ever allow.
539
+ function isTypedDateAcceptable(boundary: TimeDisplayBoundary, candidate: Date): boolean {
540
+ if (!isDateSelectable(candidate)) {
541
+ return false;
542
+ }
543
+ const otherBoundaryDate = boundary === 'start' ? draftEnd : draftStart;
544
+ if (otherBoundaryDate === null) {
545
+ return true;
546
+ }
547
+ const crossesOtherBoundary =
548
+ boundary === 'start'
549
+ ? candidate.getTime() > otherBoundaryDate.getTime()
550
+ : candidate.getTime() < otherBoundaryDate.getTime();
551
+ if (crossesOtherBoundary) {
552
+ return false;
553
+ }
554
+ if (maxRangeDays !== null) {
555
+ const spanDays =
556
+ Math.abs(Math.round((candidate.getTime() - otherBoundaryDate.getTime()) / MS_PER_DAY)) + 1;
557
+ if (spanDays > maxRangeDays) {
558
+ return false;
559
+ }
560
+ }
561
+ return true;
562
+ }
563
+
564
+ // Live validity feedback for the typed date-input text, mirroring isStartTimeValid /
565
+ // isEndTimeValid — the field is flagged invalid as soon as the typed text can't parse
566
+ // into a date this picker would accept, matching the time input's parse-as-you-type
567
+ // styling. Empty text is not flagged invalid; nothing has been typed yet.
568
+ const isStartDateValid: boolean = $derived.by(() => {
569
+ const trimmedStartDateText = startDateInputText.trim();
570
+ if (trimmedStartDateText === '') {
571
+ return true;
572
+ }
573
+ const parsedStartDate = parseDateDisplay(startDateInputText);
574
+ return parsedStartDate !== null && isTypedDateAcceptable('start', parsedStartDate);
575
+ });
576
+ const isEndDateValid: boolean = $derived.by(() => {
577
+ const trimmedEndDateText = endDateInputText.trim();
578
+ if (trimmedEndDateText === '') {
579
+ return true;
580
+ }
581
+ const parsedEndDate = parseDateDisplay(endDateInputText);
582
+ return parsedEndDate !== null && isTypedDateAcceptable('end', parsedEndDate);
583
+ });
584
+
585
+ // True when date's month is already one of the two visible calendar months (or the
586
+ // single visible month outside dual-month mode) — used to avoid an unnecessary
587
+ // re-navigation/remount when a typed date is already on screen.
588
+ function isMonthVisible(date: Date): boolean {
589
+ const isLeftMonthVisible = date.getFullYear() === leftYear && date.getMonth() === leftMonth;
590
+ if (!isDualMonth) {
591
+ return isLeftMonthVisible;
592
+ }
593
+ const rightMonth = new SvelteDate(leftYear, leftMonth + 1, 1);
594
+ const isRightMonthVisible =
595
+ date.getFullYear() === rightMonth.getFullYear() && date.getMonth() === rightMonth.getMonth();
596
+ return isLeftMonthVisible || isRightMonthVisible;
597
+ }
598
+
599
+ function navigateCalendarTo(date: Date): void {
600
+ if (isMonthVisible(date)) {
601
+ return;
602
+ }
603
+ leftYear = date.getFullYear();
604
+ leftMonth = date.getMonth();
605
+ calendarKey++;
606
+ }
607
+
608
+ // Discard one boundary's in-progress edit buffer, falling back to the derived display
609
+ // of its current draft value. Used both to normalize a successful commit and to revert
610
+ // a rejected/unparseable one.
611
+ function clearDateEditBuffer(boundary: TimeDisplayBoundary): void {
612
+ if (boundary === 'start') {
613
+ startDateEditBuffer = null;
614
+ } else {
615
+ endDateEditBuffer = null;
616
+ }
617
+ }
618
+
619
+ // Record the in-progress typed text for one date-input boundary as the user types.
620
+ function handleDateInputChange(event: Event, boundary: TimeDisplayBoundary): void {
621
+ if (!(event.currentTarget instanceof HTMLInputElement)) {
622
+ return;
623
+ }
624
+ if (boundary === 'start') {
625
+ startDateEditBuffer = event.currentTarget.value;
626
+ } else {
627
+ endDateEditBuffer = event.currentTarget.value;
628
+ }
629
+ }
630
+
631
+ // Parse and commit the typed text for one date-input boundary (called on blur and on
632
+ // Enter). Invalid text (unparseable, outside minDate/maxDate, disabled, exceeding
633
+ // maxRangeDays, or crossing the other boundary) is rejected — the field reverts to the
634
+ // last valid value instead of committing garbage into draftStart/draftEnd. A no-op
635
+ // when the field was never edited (buffer already null — nothing to commit).
636
+ function commitTypedDate(boundary: TimeDisplayBoundary): void {
637
+ const editBuffer = boundary === 'start' ? startDateEditBuffer : endDateEditBuffer;
638
+ if (editBuffer === null) {
639
+ return;
640
+ }
641
+ const parsedDate = parseDateDisplay(editBuffer);
642
+
643
+ if (parsedDate === null || !isTypedDateAcceptable(boundary, parsedDate)) {
644
+ clearDateEditBuffer(boundary);
645
+ return;
646
+ }
647
+
648
+ if (boundary === 'start') {
649
+ draftStart = parsedDate;
650
+ } else {
651
+ draftEnd = parsedDate;
652
+ }
653
+ // A typed date is a custom selection, same as a direct calendar click.
654
+ selectedPresetLabel = null;
655
+ navigateCalendarTo(parsedDate);
656
+ clearDateEditBuffer(boundary);
657
+ }
658
+
659
+ function handleDateInputKeyDown(event: KeyboardEvent, boundary: TimeDisplayBoundary): void {
660
+ if (event.key === 'Enter') {
661
+ event.preventDefault();
662
+ commitTypedDate(boundary);
663
+ }
664
+ }
665
+
458
666
  // Close on outside-click
459
667
  function handleDocumentClick(event: MouseEvent): void {
460
668
  if (!isOpen && !openCompare) {
@@ -705,13 +913,21 @@
705
913
  <div class="drp-date-input-row">
706
914
  {#if isInlineTime}
707
915
  <div class="drp-date-time-group">
708
- <div
709
- class="drp-date-input"
710
- data-pw={testId ? `${testId}-start-date` : null}
711
- testID={testId ? `${testId}-start-date` : null}
712
- >
713
- <span class="drp-date-input-value">{draftStartDateLabel || 'Start date'}</span
714
- >
916
+ <div class="drp-date-input" class:drp-date-input-invalid={!isStartDateValid}>
917
+ <input
918
+ type="text"
919
+ class="drp-date-input-value"
920
+ value={startDateInputText}
921
+ oninput={(event) => handleDateInputChange(event, 'start')}
922
+ maxlength="32"
923
+ placeholder="Start date"
924
+ aria-label="Start date"
925
+ aria-invalid={!isStartDateValid}
926
+ onblur={() => commitTypedDate('start')}
927
+ onkeydown={(event) => handleDateInputKeyDown(event, 'start')}
928
+ data-pw={testId ? `${testId}-start-date` : null}
929
+ testID={testId ? `${testId}-start-date` : null}
930
+ />
715
931
  </div>
716
932
  <div
717
933
  class="drp-time-input drp-time-input-inline"
@@ -735,12 +951,21 @@
735
951
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
736
952
  <span class="drp-datetime-arrow" aria-hidden="true">{@html chevronRightSvg}</span>
737
953
  <div class="drp-date-time-group">
738
- <div
739
- class="drp-date-input"
740
- data-pw={testId ? `${testId}-end-date` : null}
741
- testID={testId ? `${testId}-end-date` : null}
742
- >
743
- <span class="drp-date-input-value">{draftEndDateLabel || 'End date'}</span>
954
+ <div class="drp-date-input" class:drp-date-input-invalid={!isEndDateValid}>
955
+ <input
956
+ type="text"
957
+ class="drp-date-input-value"
958
+ value={endDateInputText}
959
+ oninput={(event) => handleDateInputChange(event, 'end')}
960
+ maxlength="32"
961
+ placeholder="End date"
962
+ aria-label="End date"
963
+ aria-invalid={!isEndDateValid}
964
+ onblur={() => commitTypedDate('end')}
965
+ onkeydown={(event) => handleDateInputKeyDown(event, 'end')}
966
+ data-pw={testId ? `${testId}-end-date` : null}
967
+ testID={testId ? `${testId}-end-date` : null}
968
+ />
744
969
  </div>
745
970
  <div
746
971
  class="drp-time-input drp-time-input-inline"
@@ -762,21 +987,39 @@
762
987
  </div>
763
988
  </div>
764
989
  {:else}
765
- <div
766
- class="drp-date-input"
767
- data-pw={testId ? `${testId}-start-date` : null}
768
- testID={testId ? `${testId}-start-date` : null}
769
- >
770
- <span class="drp-date-input-value">{draftStartDateLabel || 'Start date'}</span>
990
+ <div class="drp-date-input" class:drp-date-input-invalid={!isStartDateValid}>
991
+ <input
992
+ type="text"
993
+ class="drp-date-input-value"
994
+ value={startDateInputText}
995
+ oninput={(event) => handleDateInputChange(event, 'start')}
996
+ maxlength="32"
997
+ placeholder="Start date"
998
+ aria-label="Start date"
999
+ aria-invalid={!isStartDateValid}
1000
+ onblur={() => commitTypedDate('start')}
1001
+ onkeydown={(event) => handleDateInputKeyDown(event, 'start')}
1002
+ data-pw={testId ? `${testId}-start-date` : null}
1003
+ testID={testId ? `${testId}-start-date` : null}
1004
+ />
771
1005
  </div>
772
1006
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
773
1007
  <span class="drp-datetime-arrow" aria-hidden="true">{@html chevronRightSvg}</span>
774
- <div
775
- class="drp-date-input"
776
- data-pw={testId ? `${testId}-end-date` : null}
777
- testID={testId ? `${testId}-end-date` : null}
778
- >
779
- <span class="drp-date-input-value">{draftEndDateLabel || 'End date'}</span>
1008
+ <div class="drp-date-input" class:drp-date-input-invalid={!isEndDateValid}>
1009
+ <input
1010
+ type="text"
1011
+ class="drp-date-input-value"
1012
+ value={endDateInputText}
1013
+ oninput={(event) => handleDateInputChange(event, 'end')}
1014
+ maxlength="32"
1015
+ placeholder="End date"
1016
+ aria-label="End date"
1017
+ aria-invalid={!isEndDateValid}
1018
+ onblur={() => commitTypedDate('end')}
1019
+ onkeydown={(event) => handleDateInputKeyDown(event, 'end')}
1020
+ data-pw={testId ? `${testId}-end-date` : null}
1021
+ testID={testId ? `${testId}-end-date` : null}
1022
+ />
780
1023
  </div>
781
1024
  {#if showTimeSelection}
782
1025
  <button
@@ -1218,15 +1461,30 @@
1218
1461
  background: var(--drp-date-input-background, #ffffff);
1219
1462
  }
1220
1463
 
1464
+ .drp-date-input-invalid {
1465
+ border-color: var(--drp-date-input-invalid-border, #e5484d);
1466
+ }
1467
+
1221
1468
  .drp-date-input-value {
1222
1469
  display: block;
1470
+ width: 100%;
1471
+ min-width: 0;
1472
+ padding: 0;
1473
+ border: none;
1474
+ outline: none;
1475
+ background: transparent;
1223
1476
  color: var(--drp-date-input-color, #333333);
1224
1477
  font-size: var(--drp-date-input-font-size, 13px);
1478
+ font-family: inherit;
1225
1479
  white-space: nowrap;
1226
1480
  overflow: hidden;
1227
1481
  text-overflow: ellipsis;
1228
1482
  }
1229
1483
 
1484
+ .drp-date-input-value::placeholder {
1485
+ color: var(--drp-date-input-placeholder-color, #aaaaaa);
1486
+ }
1487
+
1230
1488
  .drp-datetime-arrow {
1231
1489
  flex-shrink: 0;
1232
1490
  display: inline-flex;
@@ -43,8 +43,13 @@ export type OptionalDateRangePickerProperties = {
43
43
  */
44
44
  presetCheckmark?: boolean;
45
45
  /**
46
- * Show read-only start/end date boxes at the top of the calendar area (range mode),
47
- * reflecting the current draft selection. Opt-in. Default: false.
46
+ * Show typeable start/end date boxes at the top of the calendar area (range mode).
47
+ * Each box seeds from the current draft selection and can be edited directly by
48
+ * typing a date; the typed text is parsed and committed on blur or Enter. Accepts
49
+ * the component's own display format ("Jul 10, 2026"), numeric "M/D/YYYY", or ISO
50
+ * "YYYY-MM-DD". Unparseable, out-of-range (minDate/maxDate), disabled, or
51
+ * boundary-crossing (start after end, or vice versa) input is rejected — the field
52
+ * reverts to its last valid value instead of committing garbage. Opt-in. Default: false.
48
53
  */
49
54
  showDateInputs?: boolean;
50
55
  /**
@@ -12,3 +12,9 @@ export declare const formatTimeDisplay: (date: Date) => string;
12
12
  export declare const applyTimeDisplay: (date: Date, display: string, boundary: TimeDisplayBoundary) => Date | null;
13
13
  /** Minutes-since-midnight for ordering comparisons, or null when the display is invalid. */
14
14
  export declare const toMinutesOfDay: (display: string) => number | null;
15
+ /**
16
+ * Parse a typed calendar-date string into a local-midnight Date, or null when the text is
17
+ * unparseable or names a calendar date that does not exist (e.g. "Feb 30, 2026"). Accepts the
18
+ * component's own display format ("Jul 10, 2026"), numeric "M/D/YYYY", and ISO "YYYY-MM-DD".
19
+ */
20
+ export declare const parseDateDisplay: (display: string) => Date | null;
@@ -45,3 +45,74 @@ export const toMinutesOfDay = (display) => {
45
45
  const parsed = parseTimeDisplay(display);
46
46
  return parsed === null ? null : parsed.hours * 60 + parsed.minutes;
47
47
  };
48
+ const SHORT_MONTH_NAMES = [
49
+ 'jan',
50
+ 'feb',
51
+ 'mar',
52
+ 'apr',
53
+ 'may',
54
+ 'jun',
55
+ 'jul',
56
+ 'aug',
57
+ 'sep',
58
+ 'oct',
59
+ 'nov',
60
+ 'dec'
61
+ ];
62
+ /** Matches the DateRangePicker's own display format, e.g. "Jul 10, 2026" or "Jul 10 2026". */
63
+ const MONTH_NAME_DATE_PATTERN = /^([A-Za-z]{3,})\.?\s+(\d{1,2}),?\s+(\d{4})$/;
64
+ /** Matches numeric "M/D/YYYY" or "M-D-YYYY" input. */
65
+ const NUMERIC_SLASH_DATE_PATTERN = /^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/;
66
+ /** Matches ISO "YYYY-MM-DD" input. */
67
+ const ISO_DATE_PATTERN = /^(\d{4})-(\d{1,2})-(\d{1,2})$/;
68
+ /**
69
+ * Build a local-midnight Date for the given calendar parts.
70
+ *
71
+ * `new Date(year, ...)` maps years 0-99 onto 1900-1999, so a literal the parser accepts
72
+ * as four digits — "0050-01-01" — would silently land in 1950. setFullYear has no such
73
+ * remapping, so the date is constructed neutrally and then stamped.
74
+ */
75
+ const localMidnightFromParts = (year, monthIndex, day) => {
76
+ const date = new Date();
77
+ date.setFullYear(year, monthIndex, day);
78
+ date.setHours(0, 0, 0, 0);
79
+ return date;
80
+ };
81
+ const daysInMonth = (year, monthIndex) =>
82
+ // Day 0 of the next month is the last day of this one.
83
+ localMidnightFromParts(year, monthIndex + 1, 0).getDate();
84
+ /** Build a local-midnight Date from year/monthIndex/day, or null when the calendar date does not exist. */
85
+ const buildDateFromParts = (year, monthIndex, day) => {
86
+ if (monthIndex < 0 || monthIndex > 11 || day < 1 || day > daysInMonth(year, monthIndex)) {
87
+ return null;
88
+ }
89
+ return localMidnightFromParts(year, monthIndex, day);
90
+ };
91
+ /**
92
+ * Parse a typed calendar-date string into a local-midnight Date, or null when the text is
93
+ * unparseable or names a calendar date that does not exist (e.g. "Feb 30, 2026"). Accepts the
94
+ * component's own display format ("Jul 10, 2026"), numeric "M/D/YYYY", and ISO "YYYY-MM-DD".
95
+ */
96
+ export const parseDateDisplay = (display) => {
97
+ const trimmedDisplay = display.trim();
98
+ if (trimmedDisplay === '') {
99
+ return null;
100
+ }
101
+ const isoMatch = ISO_DATE_PATTERN.exec(trimmedDisplay);
102
+ if (isoMatch !== null) {
103
+ return buildDateFromParts(Number(isoMatch[1]), Number(isoMatch[2]) - 1, Number(isoMatch[3]));
104
+ }
105
+ const monthNameMatch = MONTH_NAME_DATE_PATTERN.exec(trimmedDisplay);
106
+ if (monthNameMatch !== null) {
107
+ const monthIndex = SHORT_MONTH_NAMES.indexOf(monthNameMatch[1].slice(0, 3).toLowerCase());
108
+ if (monthIndex === -1) {
109
+ return null;
110
+ }
111
+ return buildDateFromParts(Number(monthNameMatch[3]), monthIndex, Number(monthNameMatch[2]));
112
+ }
113
+ const slashMatch = NUMERIC_SLASH_DATE_PATTERN.exec(trimmedDisplay);
114
+ if (slashMatch !== null) {
115
+ return buildDateFromParts(Number(slashMatch[3]), Number(slashMatch[1]) - 1, Number(slashMatch[2]));
116
+ }
117
+ return null;
118
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.111.1",
3
+ "version": "2.111.2",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",