@juspay/svelte-ui-components 2.80.7 → 2.80.9

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.
@@ -248,6 +248,12 @@
248
248
  .button-text {
249
249
  order: var(--button-text-order, 3);
250
250
  display: var(--button-text-display);
251
+
252
+ /* With nowrap as the default, a label longer than the button's box would
253
+ otherwise bleed past it — truncate with an ellipsis instead. Full labels
254
+ stay available via the button's accessible name / title at the call site. */
255
+ overflow: hidden;
256
+ text-overflow: ellipsis;
251
257
  }
252
258
 
253
259
  .button-el:hover:not(.disabled) {
@@ -154,6 +154,12 @@
154
154
  return n;
155
155
  }
156
156
 
157
+ function endOfDay(d: Date): Date {
158
+ const newDate = new SvelteDate(d);
159
+ newDate.setHours(23, 59, 59, 999);
160
+ return newDate;
161
+ }
162
+
157
163
  function isSameDay(a: Date, b: Date): boolean {
158
164
  return (
159
165
  a.getFullYear() === b.getFullYear() &&
@@ -206,6 +212,8 @@
206
212
  if (date.getTime() < normalizeDate(start).getTime()) {
207
213
  rangeEnd = start;
208
214
  rangeStart = date;
215
+ } else if (isSameDay(date, start)) {
216
+ rangeEnd = endOfDay(date);
209
217
  } else {
210
218
  rangeEnd = date;
211
219
  }
@@ -347,10 +347,10 @@
347
347
  if (draftStart !== null && draftEnd !== null) {
348
348
  // Fold the time-of-day inputs onto the committed dates when time selection is on.
349
349
  const appliedStart = showTimeSelection
350
- ? (applyTimeDisplay(draftStart, startTimeDisplay) ?? draftStart)
350
+ ? (applyTimeDisplay(draftStart, startTimeDisplay, 'start') ?? draftStart)
351
351
  : draftStart;
352
352
  const appliedEnd = showTimeSelection
353
- ? (applyTimeDisplay(draftEnd, endTimeDisplay) ?? draftEnd)
353
+ ? (applyTimeDisplay(draftEnd, endTimeDisplay, 'end') ?? draftEnd)
354
354
  : draftEnd;
355
355
  rangeStart = appliedStart;
356
356
  rangeEnd = appliedEnd;
@@ -16,6 +16,7 @@ export type DateRangePreset = {
16
16
  export type DateRangePickerMode = 'range' | 'single';
17
17
  export type DateRangePickerAlign = 'left' | 'right';
18
18
  export type DateRangePickerTimeLayout = 'toggle' | 'inline';
19
+ export type TimeDisplayBoundary = 'start' | 'end';
19
20
  export type OptionalDateRangePickerProperties = {
20
21
  /** Currently selected start of range (bindable). */
21
22
  rangeStart?: Date | null;
@@ -1,3 +1,4 @@
1
+ import type { TimeDisplayBoundary } from './properties';
1
2
  /** Matches a 12-hour clock display such as "2:30 PM" or "02:05 am". */
2
3
  export declare const TIME_DISPLAY_PATTERN: RegExp;
3
4
  /** Parse a 12-hour display string into 24-hour hours/minutes, or null when invalid. */
@@ -8,6 +9,6 @@ export declare const parseTimeDisplay: (display: string) => {
8
9
  /** Format a Date's time-of-day as a 12-hour display string, e.g. "02:30 PM". */
9
10
  export declare const formatTimeDisplay: (date: Date) => string;
10
11
  /** Return a new Date with the time-of-day from a 12-hour display string applied, or null when invalid. */
11
- export declare const applyTimeDisplay: (date: Date, display: string) => Date | null;
12
+ export declare const applyTimeDisplay: (date: Date, display: string, boundary: TimeDisplayBoundary) => Date | null;
12
13
  /** Minutes-since-midnight for ordering comparisons, or null when the display is invalid. */
13
14
  export declare const toMinutesOfDay: (display: string) => number | null;
@@ -1,4 +1,3 @@
1
- // Time-of-day helpers for the DateRangePicker's built-in date + time inputs.
2
1
  /** Matches a 12-hour clock display such as "2:30 PM" or "02:05 am". */
3
2
  export const TIME_DISPLAY_PATTERN = /^(1[0-2]|0?[1-9]):([0-5][0-9])\s?(AM|PM)$/i;
4
3
  const pad = (n) => n.toString().padStart(2, '0');
@@ -27,13 +26,18 @@ export const formatTimeDisplay = (date) => {
27
26
  return `${pad(hours12)}:${pad(date.getMinutes())} ${meridiem}`;
28
27
  };
29
28
  /** Return a new Date with the time-of-day from a 12-hour display string applied, or null when invalid. */
30
- export const applyTimeDisplay = (date, display) => {
29
+ export const applyTimeDisplay = (date, display, boundary) => {
31
30
  const parsed = parseTimeDisplay(display);
32
31
  if (parsed === null) {
33
32
  return null;
34
33
  }
35
34
  const next = new Date(date.getTime());
36
- next.setHours(parsed.hours, parsed.minutes, 0, 0);
35
+ if (boundary === 'end') {
36
+ next.setHours(parsed.hours, parsed.minutes, 59, 999);
37
+ }
38
+ else {
39
+ next.setHours(parsed.hours, parsed.minutes, 0, 0);
40
+ }
37
41
  return next;
38
42
  };
39
43
  /** Minutes-since-midnight for ordering comparisons, or null when the display is invalid. */
@@ -18,6 +18,77 @@
18
18
  let visible = $state(false);
19
19
  let delayTimeout = $state<ReturnType<typeof setTimeout> | null>(null);
20
20
 
21
+ /**
22
+ * Cross-axis shift (px) applied to the inline bubble so it stays inside the
23
+ * viewport: horizontal for top/bottom tooltips, vertical for left/right ones.
24
+ * The arrow compensates by the same amount, so it keeps pointing at the trigger.
25
+ */
26
+ let bubbleShift = $state(0);
27
+
28
+ /** Minimum gap kept between the bubble and the viewport edge. */
29
+ const VIEWPORT_MARGIN = 8;
30
+
31
+ /** Opposite side per position, used to flip a bubble whose main axis overflows. */
32
+ const OPPOSITE_POSITION: Record<TooltipPosition, TooltipPosition> = {
33
+ top: 'bottom',
34
+ bottom: 'top',
35
+ left: 'right',
36
+ right: 'left'
37
+ };
38
+
39
+ /**
40
+ * Set when the bubble's own side overflows the viewport (e.g. a `right`
41
+ * tooltip on a trigger near the right screen edge): the bubble renders on
42
+ * the opposite side instead, where cross-axis shifting alone cannot help.
43
+ */
44
+ let flipped = $state(false);
45
+
46
+ const displayPosition = $derived(flipped ? OPPOSITE_POSITION[position] : position);
47
+
48
+ const mainAxisOverflows = (bubbleRect: DOMRect, pos: TooltipPosition): boolean => {
49
+ if (pos === 'right') {
50
+ return bubbleRect.right > window.innerWidth - VIEWPORT_MARGIN;
51
+ }
52
+ if (pos === 'left') {
53
+ return bubbleRect.left < VIEWPORT_MARGIN;
54
+ }
55
+ if (pos === 'top') {
56
+ return bubbleRect.top < VIEWPORT_MARGIN;
57
+ }
58
+ return bubbleRect.bottom > window.innerHeight - VIEWPORT_MARGIN;
59
+ };
60
+
61
+ /**
62
+ * Action for the inline (non-portal) bubble: on mount it measures the bubble's
63
+ * natural position (shift is 0 and no flip is applied at that point) and keeps
64
+ * it inside the viewport — flipping to the opposite side when its own side
65
+ * overflows, and shifting along the cross axis when the perpendicular edges
66
+ * clip. Flipping mirrors only the main axis, so the cross-axis measurement
67
+ * stays valid after a flip. On unmount both reset so the next show starts
68
+ * from the natural position.
69
+ */
70
+ const clampInlineBubble = (node: HTMLElement): { destroy: () => void } => {
71
+ if (typeof window !== 'undefined') {
72
+ const bubbleRect = node.getBoundingClientRect();
73
+ flipped = mainAxisOverflows(bubbleRect, position);
74
+ if (position === 'top' || position === 'bottom') {
75
+ const overflowRight = bubbleRect.right - (window.innerWidth - VIEWPORT_MARGIN);
76
+ const overflowLeft = VIEWPORT_MARGIN - bubbleRect.left;
77
+ bubbleShift = overflowRight > 0 ? -overflowRight : overflowLeft > 0 ? overflowLeft : 0;
78
+ } else {
79
+ const overflowBottom = bubbleRect.bottom - (window.innerHeight - VIEWPORT_MARGIN);
80
+ const overflowTop = VIEWPORT_MARGIN - bubbleRect.top;
81
+ bubbleShift = overflowBottom > 0 ? -overflowBottom : overflowTop > 0 ? overflowTop : 0;
82
+ }
83
+ }
84
+ return {
85
+ destroy: () => {
86
+ bubbleShift = 0;
87
+ flipped = false;
88
+ }
89
+ };
90
+ };
91
+
21
92
  /** Reference to the trigger wrapper element, used for `getBoundingClientRect` in portal mode. */
22
93
  let containerEl: HTMLDivElement | null = $state(null);
23
94
 
@@ -86,6 +157,76 @@
86
157
  return `${base}top:50%;right:100%;transform:translateY(-50%);border-width:${arrowSize}px ${arrowSize}px ${arrowSize}px 0;border-color:${t} ${bg} ${t} ${t};`;
87
158
  };
88
159
 
160
+ /**
161
+ * Flips an appended portal bubble to the opposite side of the trigger when
162
+ * its own side overflows the viewport, repositioning the bubble and its
163
+ * arrow. Returns the side the bubble ends up on.
164
+ */
165
+ const flipPortalBubbleIfNeeded = (
166
+ bubble: HTMLDivElement,
167
+ rect: DOMRect,
168
+ pos: TooltipPosition
169
+ ): TooltipPosition => {
170
+ if (typeof window === 'undefined') {
171
+ return pos;
172
+ }
173
+ const bubbleRect = bubble.getBoundingClientRect();
174
+ if (!mainAxisOverflows(bubbleRect, pos)) {
175
+ return pos;
176
+ }
177
+ const flippedPos = OPPOSITE_POSITION[pos];
178
+ const coords = computePortalCoords(rect, flippedPos);
179
+ bubble.style.top = `${coords.top}px`;
180
+ bubble.style.left = `${coords.left}px`;
181
+ bubble.style.transform = coords.transform;
182
+ const arrowEl = bubble.firstElementChild;
183
+ if (arrowEl instanceof HTMLElement) {
184
+ arrowEl.style.cssText = computeArrowStyle(flippedPos);
185
+ }
186
+ return flippedPos;
187
+ };
188
+
189
+ /**
190
+ * Shifts an appended portal bubble back inside the viewport (cross-axis only)
191
+ * and moves its arrow the opposite way so it still points at the trigger.
192
+ * Margins are used so the shift composes with the centring transform.
193
+ */
194
+ const clampPortalBubble = (bubble: HTMLDivElement, pos: TooltipPosition): void => {
195
+ if (typeof window === 'undefined') {
196
+ return;
197
+ }
198
+ const bubbleRect = bubble.getBoundingClientRect();
199
+ let shiftX = 0;
200
+ let shiftY = 0;
201
+ if (pos === 'top' || pos === 'bottom') {
202
+ if (bubbleRect.right > window.innerWidth - VIEWPORT_MARGIN) {
203
+ shiftX = window.innerWidth - VIEWPORT_MARGIN - bubbleRect.right;
204
+ } else if (bubbleRect.left < VIEWPORT_MARGIN) {
205
+ shiftX = VIEWPORT_MARGIN - bubbleRect.left;
206
+ }
207
+ } else {
208
+ if (bubbleRect.bottom > window.innerHeight - VIEWPORT_MARGIN) {
209
+ shiftY = window.innerHeight - VIEWPORT_MARGIN - bubbleRect.bottom;
210
+ } else if (bubbleRect.top < VIEWPORT_MARGIN) {
211
+ shiftY = VIEWPORT_MARGIN - bubbleRect.top;
212
+ }
213
+ }
214
+ if (shiftX === 0 && shiftY === 0) {
215
+ return;
216
+ }
217
+ bubble.style.marginLeft = `${shiftX}px`;
218
+ bubble.style.marginTop = `${shiftY}px`;
219
+ const arrowEl = bubble.firstElementChild;
220
+ if (arrowEl instanceof HTMLElement) {
221
+ if (shiftX !== 0) {
222
+ arrowEl.style.left = `calc(50% - ${shiftX}px)`;
223
+ }
224
+ if (shiftY !== 0) {
225
+ arrowEl.style.top = `calc(50% - ${shiftY}px)`;
226
+ }
227
+ }
228
+ };
229
+
89
230
  const createPortalBubble = (rect: DOMRect, pos: TooltipPosition): HTMLDivElement | null => {
90
231
  if (typeof document === 'undefined') {
91
232
  return null;
@@ -159,6 +300,8 @@
159
300
  portalBubbleEl = createPortalBubble(rect, pos);
160
301
  if (portalBubbleEl !== null) {
161
302
  document.body.appendChild(portalBubbleEl);
303
+ const effectivePos = flipPortalBubbleIfNeeded(portalBubbleEl, rect, pos);
304
+ clampPortalBubble(portalBubbleEl, effectivePos);
162
305
  }
163
306
  }
164
307
  };
@@ -217,7 +360,12 @@
217
360
  {/if}
218
361
  {@render children()}
219
362
  {#if visible && !usePortal}
220
- <div class="tooltip-bubble {position}" role="tooltip">
363
+ <div
364
+ use:clampInlineBubble
365
+ class="tooltip-bubble {displayPosition}"
366
+ role="tooltip"
367
+ style:--tooltip-shift="{bubbleShift}px"
368
+ >
221
369
  <div class="tooltip-arrow"></div>
222
370
  {#if typeof content === 'function'}
223
371
  {@render content()}
@@ -266,16 +414,18 @@
266
414
  border-style: solid;
267
415
  }
268
416
 
269
- /* Top position */
417
+ /* Top position. --tooltip-shift is the viewport-edge clamp: the bubble slides
418
+ along its cross axis to stay on screen while the arrow compensates the other
419
+ way so it keeps pointing at the trigger. */
270
420
  .top {
271
421
  bottom: calc(100% + var(--tooltip-offset, 8px));
272
422
  left: 50%;
273
- transform: translateX(-50%);
423
+ transform: translateX(calc(-50% + var(--tooltip-shift, 0px)));
274
424
  }
275
425
 
276
426
  .top .tooltip-arrow {
277
427
  top: 100%;
278
- left: 50%;
428
+ left: calc(50% - var(--tooltip-shift, 0px));
279
429
  transform: translateX(-50%);
280
430
  border-width: var(--tooltip-arrow-size, 5px) var(--tooltip-arrow-size, 5px) 0
281
431
  var(--tooltip-arrow-size, 5px);
@@ -287,12 +437,12 @@
287
437
  .bottom {
288
438
  top: calc(100% + var(--tooltip-offset, 8px));
289
439
  left: 50%;
290
- transform: translateX(-50%);
440
+ transform: translateX(calc(-50% + var(--tooltip-shift, 0px)));
291
441
  }
292
442
 
293
443
  .bottom .tooltip-arrow {
294
444
  bottom: 100%;
295
- left: 50%;
445
+ left: calc(50% - var(--tooltip-shift, 0px));
296
446
  transform: translateX(-50%);
297
447
  border-width: 0 var(--tooltip-arrow-size, 5px) var(--tooltip-arrow-size, 5px)
298
448
  var(--tooltip-arrow-size, 5px);
@@ -304,12 +454,12 @@
304
454
  .left {
305
455
  right: calc(100% + var(--tooltip-offset, 8px));
306
456
  top: 50%;
307
- transform: translateY(-50%);
457
+ transform: translateY(calc(-50% + var(--tooltip-shift, 0px)));
308
458
  }
309
459
 
310
460
  .left .tooltip-arrow {
311
461
  left: 100%;
312
- top: 50%;
462
+ top: calc(50% - var(--tooltip-shift, 0px));
313
463
  transform: translateY(-50%);
314
464
  border-width: var(--tooltip-arrow-size, 5px) 0 var(--tooltip-arrow-size, 5px)
315
465
  var(--tooltip-arrow-size, 5px);
@@ -321,12 +471,12 @@
321
471
  .right {
322
472
  left: calc(100% + var(--tooltip-offset, 8px));
323
473
  top: 50%;
324
- transform: translateY(-50%);
474
+ transform: translateY(calc(-50% + var(--tooltip-shift, 0px)));
325
475
  }
326
476
 
327
477
  .right .tooltip-arrow {
328
478
  right: 100%;
329
- top: 50%;
479
+ top: calc(50% - var(--tooltip-shift, 0px));
330
480
  transform: translateY(-50%);
331
481
  border-width: var(--tooltip-arrow-size, 5px) var(--tooltip-arrow-size, 5px)
332
482
  var(--tooltip-arrow-size, 5px) 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.80.7",
3
+ "version": "2.80.9",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",