@superleapai/flow-ui 2.5.15 → 2.5.17

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.
@@ -2,6 +2,7 @@
2
2
  * Date-time-picker Component (vanilla JS)
3
3
  * Combines calendar grid, optional time picker, and quick actions (Today, Tomorrow, No date).
4
4
  * Uses Popover, Button, Select, TimePicker; depends on date-time-picker-utils.js.
5
+ * Value API: Unix epoch milliseconds (number) or undefined — not Date instances.
5
6
  */
6
7
 
7
8
  (function (global) {
@@ -35,6 +36,13 @@
35
36
  return Array.prototype.filter.call(arguments, Boolean).join(" ");
36
37
  }
37
38
 
39
+ /** @param {unknown} v @returns {number|undefined} epoch ms, or undefined if missing/invalid */
40
+ function normalizeEpoch(v) {
41
+ if (v == null || v === "") return undefined;
42
+ if (typeof v === "number" && !Number.isNaN(v)) return v;
43
+ return undefined;
44
+ }
45
+
38
46
  // Mirror input wrapper variant + size behaviour for visual consistency
39
47
  var TRIGGER_WRAPPER_CLASS = {
40
48
  base:
@@ -280,8 +288,8 @@
280
288
  /**
281
289
  * Create date-time-picker component
282
290
  * @param {Object} config
283
- * @param {Date} [config.value] - Current value
284
- * @param {Function} [config.onChange] - (date: Date | undefined) => void
291
+ * @param {number} [config.value] - Current instant as Unix epoch milliseconds
292
+ * @param {Function} [config.onChange] - (epochMs: number | undefined) => void
285
293
  * @param {boolean} [config.disabled]
286
294
  * @param {12|24} [config.hourCycle=12]
287
295
  * @param {number} [config.yearRange=50]
@@ -305,7 +313,7 @@
305
313
  throw new Error("Popover and Button required");
306
314
  }
307
315
 
308
- var value = config.value;
316
+ var value = normalizeEpoch(config.value);
309
317
  var onChange = config.onChange;
310
318
  var disabled = !!config.disabled;
311
319
  var hourCycle = config.hourCycle === 24 ? 24 : 12;
@@ -318,7 +326,13 @@
318
326
  var toDate = config.toDate;
319
327
  var align = config.align || "start";
320
328
 
321
- var validDate = Utils.getValidDate(value, fromDate, toDate);
329
+ var validDate =
330
+ value != null ? Utils.getValidDate(new Date(value), fromDate, toDate) : undefined;
331
+ if (value != null && validDate) {
332
+ value = validDate.getTime();
333
+ } else if (value != null && !validDate) {
334
+ value = undefined;
335
+ }
322
336
  var displayMonth = validDate ? new Date(validDate.getTime()) : new Date();
323
337
 
324
338
  var triggerWrapper = document.createElement("div");
@@ -362,7 +376,12 @@
362
376
  container.appendChild(triggerWrapper);
363
377
 
364
378
  function updateTriggerText() {
365
- validDate = Utils.getValidDate(value, fromDate, toDate);
379
+ if (value == null) {
380
+ validDate = undefined;
381
+ } else {
382
+ validDate = Utils.getValidDate(new Date(value), fromDate, toDate);
383
+ if (validDate) value = validDate.getTime();
384
+ }
366
385
  formatted = Utils.formatDateTime(validDate, hourCycle, granularity);
367
386
  triggerText.textContent = formatted || placeholder;
368
387
  triggerText.classList.toggle("text-typography-quaternary-text", !formatted);
@@ -393,14 +412,15 @@
393
412
  out.setHours(validDate.getHours(), validDate.getMinutes(), validDate.getSeconds(), 0);
394
413
  }
395
414
  }
396
- value = Utils.getValidDate(out, fromDate, toDate);
397
- validDate = value;
415
+ var picked = Utils.getValidDate(out, fromDate, toDate);
416
+ validDate = picked;
417
+ value = picked != null ? picked.getTime() : undefined;
398
418
  updateTriggerText();
399
419
  if (onChange) onChange(value);
400
420
  setMonth(out);
401
421
  if (captionEl && captionEl.setMonth) captionEl.setMonth(displayMonth);
402
422
  if (timePickerEl && timePickerEl.updateValue) {
403
- timePickerEl.updateValue(Utils.getTimeString(value));
423
+ timePickerEl.updateValue(Utils.getTimeString(validDate));
404
424
  }
405
425
  refreshCalendar();
406
426
  }
@@ -494,10 +514,11 @@
494
514
  isTimeDropdownOpen = false;
495
515
  },
496
516
  onChange: function (timeStr) {
497
- var base = value ? new Date(value.getTime()) : new Date();
517
+ var base = value != null ? new Date(value) : new Date();
498
518
  Utils.setTimeFromString(base, timeStr);
499
- value = Utils.getValidDate(base, fromDate, toDate);
500
- validDate = value;
519
+ var next = Utils.getValidDate(base, fromDate, toDate);
520
+ validDate = next;
521
+ value = next != null ? next.getTime() : undefined;
501
522
  updateTriggerText();
502
523
  if (onChange) onChange(value);
503
524
  },
@@ -532,14 +553,15 @@
532
553
  return;
533
554
  }
534
555
  var t = granularity === "day" ? Utils.startOfDay(new Date()) : new Date();
535
- value = Utils.getValidDate(t, fromDate, toDate);
536
- validDate = value;
537
- setMonth(value || new Date());
556
+ var todayValid = Utils.getValidDate(t, fromDate, toDate);
557
+ validDate = todayValid;
558
+ value = todayValid != null ? todayValid.getTime() : undefined;
559
+ setMonth(value != null ? new Date(value) : new Date());
538
560
  updateTriggerText();
539
561
  if (onChange) onChange(value);
540
562
  refreshCalendar();
541
- if (timePickerEl && timePickerEl.updateValue && value) {
542
- timePickerEl.updateValue(Utils.getTimeString(value));
563
+ if (timePickerEl && timePickerEl.updateValue && validDate) {
564
+ timePickerEl.updateValue(Utils.getTimeString(validDate));
543
565
  }
544
566
  },
545
567
  });
@@ -558,14 +580,15 @@
558
580
  }
559
581
  var t = Utils.addDays(new Date(), 1);
560
582
  t = granularity === "day" ? Utils.startOfDay(t) : t;
561
- value = Utils.getValidDate(t, fromDate, toDate);
562
- validDate = value;
563
- setMonth(value || new Date());
583
+ var tomorrowValid = Utils.getValidDate(t, fromDate, toDate);
584
+ validDate = tomorrowValid;
585
+ value = tomorrowValid != null ? tomorrowValid.getTime() : undefined;
586
+ setMonth(value != null ? new Date(value) : new Date());
564
587
  updateTriggerText();
565
588
  if (onChange) onChange(value);
566
589
  refreshCalendar();
567
- if (timePickerEl && timePickerEl.updateValue && value) {
568
- timePickerEl.updateValue(Utils.getTimeString(value));
590
+ if (timePickerEl && timePickerEl.updateValue && validDate) {
591
+ timePickerEl.updateValue(Utils.getTimeString(validDate));
569
592
  }
570
593
  },
571
594
  });
@@ -644,8 +667,14 @@
644
667
  return value;
645
668
  };
646
669
  container.setValue = function (v) {
647
- value = v;
648
- validDate = Utils.getValidDate(value, fromDate, toDate);
670
+ value = normalizeEpoch(v);
671
+ validDate =
672
+ value != null ? Utils.getValidDate(new Date(value), fromDate, toDate) : undefined;
673
+ if (value != null && validDate) {
674
+ value = validDate.getTime();
675
+ } else if (value != null && !validDate) {
676
+ value = undefined;
677
+ }
649
678
  displayMonth = validDate ? new Date(validDate.getTime()) : new Date();
650
679
  updateTriggerText();
651
680
  };
@@ -258,7 +258,7 @@
258
258
  var content = document.createElement("div");
259
259
  content.setAttribute("role", "listbox");
260
260
  content.setAttribute("aria-multiselectable", "true");
261
- content.className = "w-full min-w-[200px] max-h-[45vh] overflow-hidden flex flex-col";
261
+ content.className = "w-full min-w-[200px] max-h-[30vh] overflow-hidden flex flex-col";
262
262
 
263
263
  // Search input (using InputComponent like enum-select)
264
264
  var searchContainer = document.createElement("div");
@@ -319,7 +319,7 @@
319
319
  // Options list
320
320
  var optionsList = document.createElement("div");
321
321
  optionsList.className =
322
- "overflow-y-auto max-h-[45vh] p-2 w-full rounded-4 bg-fill-quarternary-fill-white flex-1 min-h-0";
322
+ "overflow-y-auto max-h-[30vh] p-2 w-full rounded-4 bg-fill-quarternary-fill-white flex-1 min-h-0";
323
323
 
324
324
  content.appendChild(searchContainer);
325
325
  content.appendChild(optionsList);
@@ -231,7 +231,7 @@
231
231
  // Create dropdown content
232
232
  var content = document.createElement("div");
233
233
  content.setAttribute("role", "listbox");
234
- content.className = "w-full min-w-[200px] max-h-[45vh] overflow-hidden flex flex-col";
234
+ content.className = "w-full min-w-[200px] max-h-[30vh] overflow-hidden flex flex-col";
235
235
 
236
236
  // Search input (using InputComponent like phone-input)
237
237
  var searchContainer = document.createElement("div");
@@ -292,7 +292,7 @@
292
292
  // Options list
293
293
  var optionsList = document.createElement("div");
294
294
  optionsList.className =
295
- "overflow-y-auto max-h-[45vh] p-2 w-full rounded-4 bg-fill-quarternary-fill-white flex-1 min-h-0";
295
+ "overflow-y-auto max-h-[30vh] p-2 w-full rounded-4 bg-fill-quarternary-fill-white flex-1 min-h-0";
296
296
 
297
297
  content.appendChild(searchContainer);
298
298
  content.appendChild(optionsList);
@@ -154,11 +154,11 @@
154
154
  var content = document.createElement("div");
155
155
  content.setAttribute("role", "listbox");
156
156
  content.setAttribute("aria-multiselectable", "true");
157
- content.className = "custom-multiselect-content w-full max-h-[45vh] overflow-hidden flex flex-col";
157
+ content.className = "custom-multiselect-content w-full max-h-[30vh] overflow-hidden flex flex-col";
158
158
 
159
159
  var optionsList = document.createElement("div");
160
160
  optionsList.className =
161
- "overflow-y-auto max-h-[45vh] p-2 w-full rounded-4 bg-fill-quarternary-fill-white";
161
+ "overflow-y-auto max-h-[30vh] p-2 w-full rounded-4 bg-fill-quarternary-fill-white";
162
162
 
163
163
  function isSelected(optionValue) {
164
164
  return values.some(function (v) {
@@ -263,7 +263,7 @@
263
263
  align: "start",
264
264
  closeOnClickOutside: true,
265
265
  bodyClassName: "p-0 overflow-hidden",
266
- panelClassName: "min-w-[var(--trigger-width)] max-h-[45vh] overflow-hidden",
266
+ panelClassName: "min-w-[var(--trigger-width)] max-h-[30vh] overflow-hidden",
267
267
  onOpen: function () {
268
268
  if (disabled) {
269
269
  popover.hide();
@@ -143,6 +143,8 @@
143
143
  const triggerRect = triggerEl.getBoundingClientRect();
144
144
  const panelRect = panel.getBoundingClientRect();
145
145
  const gap = 8;
146
+ const topGap = 24;
147
+ const viewportPadding = 8;
146
148
  const viewportHeight =
147
149
  window.innerHeight || document.documentElement.clientHeight;
148
150
  const viewportWidth =
@@ -152,32 +154,52 @@
152
154
  const spaceRight = viewportWidth - triggerRect.right;
153
155
  const spaceLeft = triggerRect.left;
154
156
 
155
- // Flip placement when there is not enough space (prefer requested side, flip only when needed)
156
- let effectivePlacement = placement;
157
- if (
158
- placement === "bottom" &&
159
- spaceBelow < panelRect.height + gap &&
160
- spaceAbove >= panelRect.height + gap
161
- ) {
162
- effectivePlacement = "top";
163
- } else if (
164
- placement === "top" &&
165
- spaceAbove < panelRect.height + gap &&
166
- spaceBelow >= panelRect.height + gap
167
- ) {
168
- effectivePlacement = "bottom";
169
- } else if (
170
- placement === "right" &&
171
- spaceRight < panelRect.width + gap &&
172
- spaceLeft >= panelRect.width + gap
173
- ) {
174
- effectivePlacement = "left";
175
- } else if (
176
- placement === "left" &&
177
- spaceLeft < panelRect.width + gap &&
178
- spaceRight >= panelRect.width + gap
179
- ) {
180
- effectivePlacement = "right";
157
+ // Normalize placement and resolve to the best side when space is constrained.
158
+ const normalizedPlacement =
159
+ placement === "top" ||
160
+ placement === "bottom" ||
161
+ placement === "left" ||
162
+ placement === "right"
163
+ ? placement
164
+ : "bottom";
165
+ const oppositeBySide = {
166
+ bottom: "top",
167
+ top: "bottom",
168
+ right: "left",
169
+ left: "right",
170
+ };
171
+ const requiredSpaceBySide = {
172
+ bottom: panelRect.height + gap,
173
+ top: panelRect.height + topGap,
174
+ right: panelRect.width + gap,
175
+ left: panelRect.width + gap,
176
+ };
177
+ const availableSpaceBySide = {
178
+ bottom: spaceBelow,
179
+ top: spaceAbove,
180
+ right: spaceRight,
181
+ left: spaceLeft,
182
+ };
183
+ const oppositePlacement = oppositeBySide[normalizedPlacement];
184
+ const preferredFits =
185
+ availableSpaceBySide[normalizedPlacement] >=
186
+ requiredSpaceBySide[normalizedPlacement];
187
+ const oppositeFits =
188
+ availableSpaceBySide[oppositePlacement] >=
189
+ requiredSpaceBySide[oppositePlacement];
190
+
191
+ let effectivePlacement = normalizedPlacement;
192
+ if (!preferredFits) {
193
+ if (oppositeFits) {
194
+ effectivePlacement = oppositePlacement;
195
+ } else {
196
+ // Neither side fully fits: use the side with more available room.
197
+ effectivePlacement =
198
+ availableSpaceBySide[normalizedPlacement] >=
199
+ availableSpaceBySide[oppositePlacement]
200
+ ? normalizedPlacement
201
+ : oppositePlacement;
202
+ }
181
203
  }
182
204
 
183
205
  panel.setAttribute("data-side", effectivePlacement);
@@ -209,7 +231,6 @@
209
231
  break;
210
232
  case "top": {
211
233
  // Larger gap when above the trigger so the popover doesn't overlap it.
212
- const topGap = 24;
213
234
  top = triggerRect.top - panelRect.height - topGap;
214
235
  left = triggerRect.left + alignLeft;
215
236
  // Cap panel height so when content loads/tall it doesn't grow downward into the trigger.
@@ -232,6 +253,18 @@
232
253
  left = triggerRect.left + alignLeft;
233
254
  }
234
255
 
256
+ // Keep popover within viewport on the non-placement axis.
257
+ const minTop = viewportPadding;
258
+ const maxTop = viewportHeight - panelRect.height - viewportPadding;
259
+ if (maxTop >= minTop) {
260
+ top = Math.min(Math.max(top, minTop), maxTop);
261
+ }
262
+ const minLeft = viewportPadding;
263
+ const maxLeft = viewportWidth - panelRect.width - viewportPadding;
264
+ if (maxLeft >= minLeft) {
265
+ left = Math.min(Math.max(left, minLeft), maxLeft);
266
+ }
267
+
235
268
  wrapper.style.transform = "";
236
269
  wrapper.style.top = top + "px";
237
270
  wrapper.style.left = left + "px";
@@ -318,7 +318,7 @@
318
318
  var content = document.createElement("div");
319
319
  content.setAttribute("role", "listbox");
320
320
  content.setAttribute("aria-multiselectable", "true");
321
- content.className = "record-multiselect-content max-h-[45vh] overflow-hidden flex flex-col";
321
+ content.className = "record-multiselect-content max-h-[30vh] overflow-hidden flex flex-col";
322
322
 
323
323
  var searchWrap = document.createElement("div");
324
324
  searchWrap.className = "p-8 pb-4 border-b-1/2 border-border-primary ";
@@ -368,7 +368,7 @@
368
368
  content.appendChild(searchWrap);
369
369
 
370
370
  var optionsList = document.createElement("div");
371
- optionsList.className = "overflow-y-auto max-h-[45vh] p-2 w-full rounded-4 bg-fill-quarternary-fill-white record-multiselect-options";
371
+ optionsList.className = "overflow-y-auto max-h-[30vh] p-2 w-full rounded-4 bg-fill-quarternary-fill-white record-multiselect-options";
372
372
 
373
373
  var loadMoreSentinel = null;
374
374
  var loadMoreObserver =
@@ -315,7 +315,7 @@
315
315
  content.setAttribute("role", "listbox");
316
316
  content.setAttribute("data-field-id", fieldId);
317
317
  content.className =
318
- "record-select-content max-h-[45vh] overflow-hidden flex flex-col";
318
+ "record-select-content max-h-[30vh] overflow-hidden flex flex-col";
319
319
 
320
320
  var searchWrap = document.createElement("div");
321
321
  searchWrap.className = "py-8 border-b-1/2 border-border-primary";
@@ -366,7 +366,7 @@
366
366
 
367
367
  var optionsList = document.createElement("div");
368
368
  optionsList.className =
369
- "overflow-y-auto max-h-[45vh] p-2 w-full rounded-4 bg-fill-quarternary-fill-white record-select-options";
369
+ "overflow-y-auto max-h-[30vh] p-2 w-full rounded-4 bg-fill-quarternary-fill-white record-select-options";
370
370
 
371
371
  var loadMoreSentinel = null;
372
372
  var loadMoreObserver =
@@ -86,7 +86,7 @@
86
86
  var onClear = config.onClear;
87
87
  var popoverWrapperClassName = config.popoverWrapperClassName || "";
88
88
  var onOpenCallback = config.onOpenCallback || null;
89
- var dropdownMaxHeightVh = config.dropdownMaxHeightVh != null ? config.dropdownMaxHeightVh : 45;
89
+ var dropdownMaxHeightVh = config.dropdownMaxHeightVh != null ? config.dropdownMaxHeightVh : 30;
90
90
 
91
91
  var disabled = config.disabled === true;
92
92
  var value =
@@ -261,7 +261,7 @@
261
261
  align: "start",
262
262
  closeOnClickOutside: true,
263
263
  bodyClassName: "p-0 overflow-hidden",
264
- panelClassName: "min-w-[var(--trigger-width)] max-h-[45vh] overflow-hidden",
264
+ panelClassName: "min-w-[var(--trigger-width)] max-h-[30vh] overflow-hidden",
265
265
  wrapperClassName: popoverWrapperClassName,
266
266
  onOpen: function () {
267
267
  if (disabled) {
package/core/flow.js CHANGED
@@ -417,7 +417,7 @@
417
417
  * @param {Object} config - Configuration object
418
418
  * @param {string} config.label - Field label
419
419
  * @param {string} config.fieldId - State key for this field
420
- * @param {Date|string|null} config.value - Initial value (Date or ISO string)
420
+ * @param {Date|string|number|null} config.value - Initial value (Date, ISO string, or epoch ms)
421
421
  * @param {string} config.placeholder - Placeholder text
422
422
  * @param {boolean} config.required - Whether field is required
423
423
  * @param {Function} config.onChange - Optional change handler
@@ -453,22 +453,34 @@
453
453
 
454
454
  if (getComponent("DateTimePicker") && getComponent("DateTimePicker").create) {
455
455
  const raw = get(fieldId);
456
- let currentValue;
457
- if (initialValue instanceof Date) {
458
- currentValue = initialValue;
456
+ let currentValueMs;
457
+ if (typeof initialValue === "number" && !Number.isNaN(initialValue)) {
458
+ currentValueMs = initialValue;
459
+ } else if (initialValue instanceof Date) {
460
+ currentValueMs = initialValue.getTime();
461
+ } else if (typeof initialValue === "string" && initialValue.trim() !== "") {
462
+ try {
463
+ const d = new Date(initialValue);
464
+ currentValueMs = Number.isNaN(d.getTime()) ? undefined : d.getTime();
465
+ } catch (e) {
466
+ currentValueMs = undefined;
467
+ }
459
468
  } else if (raw === null || raw === undefined || raw === "") {
460
- currentValue = undefined;
469
+ currentValueMs = undefined;
461
470
  } else {
462
471
  try {
463
- currentValue = typeof raw === "string" ? new Date(raw) : raw;
464
- if (Number.isNaN(currentValue.getTime())) currentValue = undefined;
472
+ const parsed = typeof raw === "string" ? new Date(raw) : raw;
473
+ currentValueMs =
474
+ parsed && typeof parsed.getTime === "function" && !Number.isNaN(parsed.getTime())
475
+ ? parsed.getTime()
476
+ : undefined;
465
477
  } catch (e) {
466
- currentValue = undefined;
478
+ currentValueMs = undefined;
467
479
  }
468
480
  }
469
481
 
470
482
  const picker = getComponent("DateTimePicker").create({
471
- value: currentValue,
483
+ value: currentValueMs,
472
484
  placeholder: placeholder || `Pick ${label.toLowerCase()}`,
473
485
  disabled,
474
486
  hourCycle,
@@ -477,9 +489,9 @@
477
489
  variant,
478
490
  fromDate,
479
491
  toDate,
480
- onChange: (date) => {
481
- set(fieldId, date ? date.toISOString() : null);
482
- if (onChange) onChange(date);
492
+ onChange: (epochMs) => {
493
+ set(fieldId, epochMs != null ? new Date(epochMs).toISOString() : null);
494
+ if (onChange) onChange(epochMs != null ? new Date(epochMs) : undefined);
483
495
  },
484
496
  });
485
497