@juspay/svelte-ui-components 2.80.6 → 2.80.8
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.
- package/dist/Button/Button.svelte +5 -0
- package/dist/Calendar/Calendar.svelte +8 -0
- package/dist/DateRangePicker/DateRangePicker.svelte +2 -2
- package/dist/DateRangePicker/properties.d.ts +1 -0
- package/dist/DateRangePicker/timeUtils.d.ts +2 -1
- package/dist/DateRangePicker/timeUtils.js +7 -3
- package/dist/Menu/Menu.svelte +26 -5
- package/dist/Menu/properties.d.ts +5 -0
- package/dist/SankeyChart/SankeyChart.svelte +45 -7
- package/package.json +1 -1
|
@@ -213,6 +213,11 @@
|
|
|
213
213
|
gap: var(--button-content-gap, 16px);
|
|
214
214
|
visibility: var(--button-visibility, visible);
|
|
215
215
|
box-shadow: var(--button-box-shadow, none);
|
|
216
|
+
|
|
217
|
+
/* A button label should never soft-wrap onto a second line as the label text
|
|
218
|
+
changes (e.g. "Select" -> "Select (1)") — that jitters the layout around it.
|
|
219
|
+
Consumers that genuinely want multi-line buttons opt back in via the var. */
|
|
220
|
+
white-space: var(--button-white-space, nowrap);
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
.button-el.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
|
-
|
|
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. */
|
package/dist/Menu/Menu.svelte
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
onopen,
|
|
14
14
|
onclose,
|
|
15
15
|
classes,
|
|
16
|
+
selectedValue = null,
|
|
16
17
|
role: menuRole = 'menu',
|
|
17
18
|
ariaLabel: menuAriaLabel,
|
|
18
19
|
id: menuId
|
|
@@ -43,12 +44,20 @@
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function openMenu(startIndex: number =
|
|
47
|
+
function openMenu(startIndex: number | null = null) {
|
|
47
48
|
open = true;
|
|
48
|
-
|
|
49
|
+
// With a known selection, opening focuses the selected option (listbox
|
|
50
|
+
// convention) instead of always parking the focus highlight on item 0.
|
|
51
|
+
const selectedItem =
|
|
52
|
+
selectedValue != null
|
|
53
|
+
? (items.find((item) => item.value === selectedValue && item.disabled !== true) ?? null)
|
|
54
|
+
: null;
|
|
55
|
+
const initialIndex =
|
|
56
|
+
startIndex ?? (selectedItem !== null ? getSelectableIndex(selectedItem) : 0);
|
|
57
|
+
focusedIndex = initialIndex;
|
|
49
58
|
onopen?.();
|
|
50
59
|
tick().then(() => {
|
|
51
|
-
focusItem(
|
|
60
|
+
focusItem(initialIndex);
|
|
52
61
|
});
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -232,12 +241,17 @@
|
|
|
232
241
|
class="menu-item"
|
|
233
242
|
class:menu-item-danger={item.danger === true}
|
|
234
243
|
class:menu-item-disabled={item.disabled === true}
|
|
244
|
+
class:menu-item-selected={selectedValue != null && item.value === selectedValue}
|
|
235
245
|
role={itemRole}
|
|
236
246
|
id={item.id}
|
|
237
247
|
tabindex={item.disabled === true || menuRole === 'listbox' ? -1 : 0}
|
|
238
248
|
aria-disabled={item.disabled === true ? 'true' : null}
|
|
239
|
-
aria-selected={menuRole === 'listbox'
|
|
240
|
-
?
|
|
249
|
+
aria-selected={menuRole === 'listbox'
|
|
250
|
+
? selectedValue != null
|
|
251
|
+
? item.value === selectedValue
|
|
252
|
+
: focusedIndex === getSelectableIndex(item)
|
|
253
|
+
? true
|
|
254
|
+
: null
|
|
241
255
|
: null}
|
|
242
256
|
onclick={() => selectItem(item)}
|
|
243
257
|
onfocus={() => {
|
|
@@ -318,6 +332,13 @@
|
|
|
318
332
|
outline: var(--menu-item-focus-outline, none);
|
|
319
333
|
}
|
|
320
334
|
|
|
335
|
+
/* True selection (selectedValue match) — distinct from transient focus so the
|
|
336
|
+
highlight follows the chosen option, not wherever keyboard focus landed. */
|
|
337
|
+
.menu-item-selected {
|
|
338
|
+
background-color: var(--menu-item-selected-background-color, transparent);
|
|
339
|
+
color: var(--menu-item-selected-color, inherit);
|
|
340
|
+
}
|
|
341
|
+
|
|
321
342
|
.menu-item-danger {
|
|
322
343
|
color: var(--menu-item-danger-color, #dc3545);
|
|
323
344
|
}
|
|
@@ -17,6 +17,11 @@ export type OptionalMenuProperties = {
|
|
|
17
17
|
testId?: string;
|
|
18
18
|
trigger?: Snippet;
|
|
19
19
|
classes?: string;
|
|
20
|
+
/** Value of the currently selected item. When set, opening the menu focuses the
|
|
21
|
+
* selected option instead of the first item, the matching item gets the
|
|
22
|
+
* `menu-item-selected` class (stylable via --menu-item-selected-*), and
|
|
23
|
+
* listbox aria-selected reflects the real selection. */
|
|
24
|
+
selectedValue?: string | null;
|
|
20
25
|
role?: 'menu' | 'listbox';
|
|
21
26
|
ariaLabel?: string;
|
|
22
27
|
id?: string;
|
|
@@ -52,11 +52,39 @@
|
|
|
52
52
|
let format = $derived(valueFormat ?? formatNumber);
|
|
53
53
|
let isEmpty = $derived(nodes.length === 0);
|
|
54
54
|
const MARGIN = 40;
|
|
55
|
+
const LABEL_CHAR_PX = 7.2; // ≈ 0.6em at the 12px default label size
|
|
56
|
+
|
|
57
|
+
// Final-column labels render to the RIGHT of their node; the bare 40px margin is
|
|
58
|
+
// nowhere near enough for real funnel labels ("PARTIALLY_FAILED (1,234)"), so they
|
|
59
|
+
// used to run past the svg edge and clip. Reserve a capped gutter sized from the
|
|
60
|
+
// sink-node labels (sinks are what land in the final column) and lay the diagram
|
|
61
|
+
// out in the remaining width instead.
|
|
62
|
+
let lastColumnLabelGutter = $derived.by(() => {
|
|
63
|
+
if (!showLabels || nodes.length === 0 || chartWidth <= 0) {
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
const sourceIds = new Set(links.map((link) => link.source));
|
|
67
|
+
const sinkLabels = nodes
|
|
68
|
+
.filter((node) => !sourceIds.has(node.id))
|
|
69
|
+
.map((node) => node.label ?? node.id);
|
|
70
|
+
if (sinkLabels.length === 0) {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
const longestChars =
|
|
74
|
+
Math.max(...sinkLabels.map((label) => label.length)) + (showValues ? 9 : 0);
|
|
75
|
+
const wanted = longestChars * LABEL_CHAR_PX + 10 + dataLabelOffsetX;
|
|
76
|
+
// Cap the reservation so labels can never squeeze the diagram below 3/4 width,
|
|
77
|
+
// and floor at 0 — a negative dataLabelOffsetX must not inflate the plot
|
|
78
|
+
// past the right margin.
|
|
79
|
+
return Math.max(0, Math.min(chartWidth * 0.25, wanted));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let plotWidth = $derived(Math.max(0, chartWidth - MARGIN * 2 - lastColumnLabelGutter));
|
|
55
83
|
let layout = $derived(
|
|
56
84
|
computeSankeyLayout(
|
|
57
85
|
nodes,
|
|
58
86
|
links,
|
|
59
|
-
|
|
87
|
+
plotWidth,
|
|
60
88
|
Math.max(0, chartHeight - MARGIN * 2),
|
|
61
89
|
nodeWidth,
|
|
62
90
|
nodePadding,
|
|
@@ -87,21 +115,31 @@
|
|
|
87
115
|
layout.nodes.length > 0 ? Math.max(...layout.nodes.map((n) => n.column)) + 1 : 0
|
|
88
116
|
);
|
|
89
117
|
let colWidth = $derived(
|
|
90
|
-
columnCount <= 1
|
|
91
|
-
? Math.max(0, chartWidth - MARGIN * 2)
|
|
92
|
-
: (Math.max(0, chartWidth - MARGIN * 2) - nodeWidth) / (columnCount - 1)
|
|
118
|
+
columnCount <= 1 ? plotWidth : (plotWidth - nodeWidth) / (columnCount - 1)
|
|
93
119
|
);
|
|
94
120
|
|
|
95
121
|
// Node labels render alongside their bar; long labels used to overflow into the
|
|
96
122
|
// next column and collide. Clip each to the horizontal room its column actually
|
|
97
123
|
// has and append an ellipsis (the full text stays available via the node tooltip).
|
|
98
|
-
|
|
124
|
+
// The final column's room is the reserved right gutter (plus the margin), NOT
|
|
125
|
+
// colWidth — budgeting it at colWidth is what used to let edge labels clip.
|
|
126
|
+
// Column headers are centred over columns that narrow as the label gutter and
|
|
127
|
+
// column count grow; untruncated they collide into one unreadable run. Clip to
|
|
128
|
+
// the column pitch with an ellipsis — the full text stays on the <title>.
|
|
129
|
+
const truncateColumnLabel = (text: string): string => {
|
|
130
|
+
const maxChars = Math.floor(Math.max(0, colWidth - 6) / LABEL_CHAR_PX);
|
|
131
|
+
if (maxChars < 3) {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
return text.length > maxChars ? text.slice(0, maxChars - 1) + '…' : text;
|
|
135
|
+
};
|
|
136
|
+
|
|
99
137
|
const truncateLabel = (text: string, column: number): string => {
|
|
100
138
|
const available =
|
|
101
139
|
column === 0
|
|
102
140
|
? MARGIN + 16
|
|
103
141
|
: column === columnCount - 1
|
|
104
|
-
? Math.max(
|
|
142
|
+
? Math.max(0, lastColumnLabelGutter + MARGIN - 6 - dataLabelOffsetX)
|
|
105
143
|
: Math.max(0, colWidth - nodeWidth - 12);
|
|
106
144
|
const maxChars = Math.floor(available / LABEL_CHAR_PX);
|
|
107
145
|
// No usable room — hide the label rather than force text that would overflow;
|
|
@@ -308,7 +346,7 @@
|
|
|
308
346
|
x={ci * colWidth + nodeWidth / 2}
|
|
309
347
|
y={-8}
|
|
310
348
|
text-anchor="middle"
|
|
311
|
-
dominant-baseline="auto">{label}</text
|
|
349
|
+
dominant-baseline="auto">{truncateColumnLabel(label)}<title>{label}</title></text
|
|
312
350
|
>
|
|
313
351
|
{/each}
|
|
314
352
|
{/if}
|