@cwellt_software/cwellt-reactjs-lib 1.4.12 → 1.4.13

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/index.cjs.js CHANGED
@@ -6512,7 +6512,10 @@ function CwTimePicker({ value, onChange, interval = 15, minTime, maxTime, labelP
6512
6512
  const handleClear = (e) => {
6513
6513
  e.stopPropagation();
6514
6514
  onChange(undefined);
6515
- setInputValue("");
6515
+ // Keep showing the current value prop: a controlled parent that accepts the
6516
+ // clear empties it via the value-sync effect, while a parent that rejects or
6517
+ // transforms the clear (e.g. resets to a default) stays visually in sync.
6518
+ setInputValue(value ?? "");
6516
6519
  };
6517
6520
  const handleInputClick = () => {
6518
6521
  if (!disabled) {
@@ -6540,15 +6543,18 @@ function CwTimePicker({ value, onChange, interval = 15, minTime, maxTime, labelP
6540
6543
  const [hours, minutes] = newValue.split(":");
6541
6544
  const h = parseInt(hours, 10);
6542
6545
  const m = parseInt(minutes, 10);
6543
- if (h >= 0 && h <= 23 && m >= 0 && m <= 59) {
6544
- const formattedTime = `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`;
6545
- // Check min/max time restrictions
6546
- const isValid = (!minTime || formattedTime >= minTime) &&
6547
- (!maxTime || formattedTime <= maxTime);
6548
- if (isValid) {
6549
- onChange(formattedTime);
6550
- setInputValue(formattedTime);
6551
- }
6546
+ const formattedTime = `${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`;
6547
+ // Check well-formedness and min/max time restrictions
6548
+ const isValid = h >= 0 && h <= 23 && m >= 0 && m <= 59 &&
6549
+ (!minTime || formattedTime >= minTime) &&
6550
+ (!maxTime || formattedTime <= maxTime);
6551
+ if (isValid) {
6552
+ onChange(formattedTime);
6553
+ setInputValue(formattedTime);
6554
+ }
6555
+ else {
6556
+ // Completed entry is invalid/out of range: snap back to the previous value
6557
+ setInputValue(value ?? "");
6552
6558
  }
6553
6559
  }
6554
6560
  else if (newValue === "") {
@@ -7264,7 +7270,8 @@ function formatPlainDate(pd, fmt) {
7264
7270
 
7265
7271
  function CwDatePickerTemporal({ value, onChange, minDate, maxDate, disabledDates, disabledMatcher, defaultMonth, labelProps, alignProps, placeholder = "Select a date", displayFormat = "dd.MM.yyyy", disabled, required, className, showClear = true, popupPosition = "left-bottom", numberOfMonths = 1, showTodayButton = false, locale: locale$1 = locale.enGB, todayLabel = "Today", yearRangeStart, yearRangeEnd, feedback, }) {
7266
7272
  const [isOpen, setIsOpen] = React.useState(false);
7267
- const [inputValue, setInputValue] = React.useState();
7273
+ // Initialize with the formatted value so the input is controlled from the first render
7274
+ const [inputValue, setInputValue] = React.useState(() => value ? formatPlainDate(value, displayFormat) : "");
7268
7275
  const containerRef = React.useRef(null);
7269
7276
  const feedbackMessages = feedback
7270
7277
  ? Array.isArray(feedback) ? feedback : [feedback]
@@ -7382,6 +7389,298 @@ function CwDatePickerTemporal({ value, onChange, minDate, maxDate, disabledDates
7382
7389
  } }), showTodayButton && (jsxRuntime.jsx("footer", { className: "cw-flex-row cw-align-right-center", children: jsxRuntime.jsx(CwButton, { type: "button", variant: "outline", onClick: handleTodayClick, text: todayLabel }) }))] }))] })] }), feedbackMessages.map((feedbackItem, index) => (jsxRuntime.jsx("p", { className: "cw-input-info", "data-color": feedbackItem.type, children: feedbackItem.message }, index)))] }));
7383
7390
  }
7384
7391
 
7392
+ // DateTimePickerState - immutable domain model for the Temporal date-time picker.
7393
+ //
7394
+ // Invariants:
7395
+ // - `time` is NEVER null: this is a date-TIME picker, so the time always has a
7396
+ // value and defaults to 00:00. Only the date can be unselected, and without
7397
+ // a date the whole `value` is undefined.
7398
+ // - Minute precision everywhere: times AND constraints are truncated to HH:mm
7399
+ // on the way in, so emitted values always carry :00 seconds and boundary
7400
+ // checks never fail on sub-minute residue.
7401
+ // - NO reachable state holds an out-of-range selection when minDateTime /
7402
+ // maxDateTime are set. User operations that would leave the state invalid
7403
+ // return THE SAME instance (===), which the hook uses to skip the emission
7404
+ // and let the UI snap back; entry paths that cannot reject (fromValue,
7405
+ // withConstraints, withTimeCleared) clamp into the range instead.
7406
+ //
7407
+ // Because of these invariants the emission is state-scoped: after any accepted
7408
+ // operation the value to emit is simply the new state's `value`. Rejected
7409
+ // operations are detected by identity (`next === prev`).
7410
+ /** Truncate to minute precision (HH:mm) - the picker's working precision */
7411
+ function truncateToMinute(time) {
7412
+ return new Temporal.PlainTime(time.hour, time.minute);
7413
+ }
7414
+ function midnight() {
7415
+ return new Temporal.PlainTime(0, 0);
7416
+ }
7417
+ function truncateDateTimeToMinute(dateTime) {
7418
+ return dateTime
7419
+ ? dateTime.toPlainDate().toPlainDateTime(truncateToMinute(dateTime.toPlainTime()))
7420
+ : undefined;
7421
+ }
7422
+ /** Normalize constraints to the picker's minute precision */
7423
+ function normalizeConstraints(constraints) {
7424
+ return {
7425
+ minDateTime: truncateDateTimeToMinute(constraints.minDateTime),
7426
+ maxDateTime: truncateDateTimeToMinute(constraints.maxDateTime),
7427
+ minTime: constraints.minTime ? truncateToMinute(constraints.minTime) : undefined,
7428
+ maxTime: constraints.maxTime ? truncateToMinute(constraints.maxTime) : undefined,
7429
+ };
7430
+ }
7431
+ class DateTimePickerState {
7432
+ date;
7433
+ time;
7434
+ constraints;
7435
+ constructor(date, time, constraints) {
7436
+ this.date = date;
7437
+ this.time = time;
7438
+ this.constraints = normalizeConstraints(constraints);
7439
+ Object.freeze(this);
7440
+ }
7441
+ static empty(constraints = {}) {
7442
+ return new DateTimePickerState(undefined, midnight(), constraints);
7443
+ }
7444
+ /**
7445
+ * Build a state from an external value (the value-prop sync path).
7446
+ * The time is truncated to minute precision and the whole value is
7447
+ * clamped into [minDateTime, maxDateTime]: no state can ever hold an
7448
+ * out-of-range selection, regardless of where the value came from.
7449
+ */
7450
+ static fromValue(value, constraints = {}) {
7451
+ if (!value)
7452
+ return new DateTimePickerState(undefined, midnight(), constraints);
7453
+ const base = new DateTimePickerState(value.toPlainDate(), truncateToMinute(value.toPlainTime()), constraints);
7454
+ return base.clampedIntoRange();
7455
+ }
7456
+ /** Combined date+time; undefined when no date is selected. */
7457
+ get value() {
7458
+ return this.date ? this.date.toPlainDateTime(this.time) : undefined;
7459
+ }
7460
+ /**
7461
+ * Effective minimum time of day for the currently selected date:
7462
+ * when the selected date is the minDateTime boundary date, the boundary's
7463
+ * time (minute-truncated) replaces the static minTime.
7464
+ */
7465
+ get effectiveMinTime() {
7466
+ return this.effectiveMinTimeFor(this.date);
7467
+ }
7468
+ /** Effective maximum time of day for the currently selected date (see effectiveMinTime). */
7469
+ get effectiveMaxTime() {
7470
+ return this.effectiveMaxTimeFor(this.date);
7471
+ }
7472
+ /**
7473
+ * Select (or clear) the date.
7474
+ * - Clearing the date resets the whole selection (time back to 00:00).
7475
+ * - When the current time is out of range for the new date, it falls back
7476
+ * to the effective minimum time for that date (or 00:00).
7477
+ * - When even the fallback is out of range the date is rejected and the
7478
+ * same instance is returned.
7479
+ */
7480
+ withDate(date) {
7481
+ if (!date) {
7482
+ return new DateTimePickerState(undefined, midnight(), this.constraints);
7483
+ }
7484
+ if (this.isInRange(date.toPlainDateTime(this.time))) {
7485
+ return new DateTimePickerState(date, this.time, this.constraints);
7486
+ }
7487
+ const fallbackTime = this.effectiveMinTimeFor(date) ?? midnight();
7488
+ if (this.isInRange(date.toPlainDateTime(fallbackTime))) {
7489
+ return new DateTimePickerState(date, fallbackTime, this.constraints);
7490
+ }
7491
+ return this;
7492
+ }
7493
+ /**
7494
+ * Select the time (minute-truncated). An out-of-range time for the
7495
+ * selected date is rejected: the same instance is returned so the UI can
7496
+ * snap back to the previous time. Without a date any time is accepted
7497
+ * (there is no combination to validate yet).
7498
+ */
7499
+ withTime(time) {
7500
+ const truncated = truncateToMinute(time);
7501
+ if (this.date && !this.isInRange(this.date.toPlainDateTime(truncated))) {
7502
+ return this;
7503
+ }
7504
+ return new DateTimePickerState(this.date, truncated, this.constraints);
7505
+ }
7506
+ /**
7507
+ * Apply "now" (Now button), truncated to minute precision. Rejected
7508
+ * (same instance returned) when the truncated now is out of range.
7509
+ */
7510
+ withNow(now) {
7511
+ const date = now.toPlainDate();
7512
+ const time = truncateToMinute(now.toPlainTime());
7513
+ if (!this.isInRange(date.toPlainDateTime(time))) {
7514
+ return this;
7515
+ }
7516
+ return new DateTimePickerState(date, time, this.constraints);
7517
+ }
7518
+ /**
7519
+ * Reset the time to its default: the effective minimum for the current
7520
+ * date when 00:00 would be out of range (boundary dates), else 00:00.
7521
+ * The result is always in range - this is the clear-time path.
7522
+ */
7523
+ withTimeCleared() {
7524
+ if (!this.date) {
7525
+ return new DateTimePickerState(undefined, midnight(), this.constraints);
7526
+ }
7527
+ return new DateTimePickerState(this.date, midnight(), this.constraints).clampedIntoRange();
7528
+ }
7529
+ /** Reset the selection (no date, time back to 00:00), keeping the constraints. */
7530
+ clear() {
7531
+ return new DateTimePickerState(undefined, midnight(), this.constraints);
7532
+ }
7533
+ /**
7534
+ * Replace the constraints. The current selection is clamped into the new
7535
+ * [minDateTime, maxDateTime] range so it can never be left out of range.
7536
+ */
7537
+ withConstraints(constraints) {
7538
+ return new DateTimePickerState(this.date, this.time, constraints).clampedIntoRange();
7539
+ }
7540
+ /**
7541
+ * Clamp the selection into [minDateTime, maxDateTime]: a combined value
7542
+ * below the minimum becomes the minimum (date and time), one above the
7543
+ * maximum becomes the maximum. In-range states return themselves.
7544
+ */
7545
+ clampedIntoRange() {
7546
+ const combined = this.value;
7547
+ if (!combined)
7548
+ return this;
7549
+ const { minDateTime, maxDateTime } = this.constraints;
7550
+ if (minDateTime && Temporal.PlainDateTime.compare(combined, minDateTime) < 0) {
7551
+ return new DateTimePickerState(minDateTime.toPlainDate(), truncateToMinute(minDateTime.toPlainTime()), this.constraints);
7552
+ }
7553
+ if (maxDateTime && Temporal.PlainDateTime.compare(combined, maxDateTime) > 0) {
7554
+ return new DateTimePickerState(maxDateTime.toPlainDate(), truncateToMinute(maxDateTime.toPlainTime()), this.constraints);
7555
+ }
7556
+ return this;
7557
+ }
7558
+ effectiveMinTimeFor(date) {
7559
+ const { minDateTime, minTime } = this.constraints;
7560
+ if (date && minDateTime && date.equals(minDateTime.toPlainDate())) {
7561
+ return truncateToMinute(minDateTime.toPlainTime());
7562
+ }
7563
+ return minTime;
7564
+ }
7565
+ effectiveMaxTimeFor(date) {
7566
+ const { maxDateTime, maxTime } = this.constraints;
7567
+ if (date && maxDateTime && date.equals(maxDateTime.toPlainDate())) {
7568
+ return truncateToMinute(maxDateTime.toPlainTime());
7569
+ }
7570
+ return maxTime;
7571
+ }
7572
+ isInRange(combined) {
7573
+ const { minDateTime, maxDateTime } = this.constraints;
7574
+ if (minDateTime && Temporal.PlainDateTime.compare(combined, minDateTime) < 0)
7575
+ return false;
7576
+ if (maxDateTime && Temporal.PlainDateTime.compare(combined, maxDateTime) > 0)
7577
+ return false;
7578
+ return true;
7579
+ }
7580
+ }
7581
+
7582
+ // useCwDateTimePickerTemporal - thin glue between DateTimePickerState and the
7583
+ // CwDateTimePickerTemporal component. All picker logic lives in the domain
7584
+ // class; this hook only holds the instance in React state, syncs it with the
7585
+ // external value prop, and converts PlainTime <-> "HH:mm" at the CwTimePicker
7586
+ // string boundary.
7587
+ //
7588
+ // Emission model: an accepted operation returns a NEW instance and the hook
7589
+ // emits its `value`; a rejected operation returns the SAME instance and
7590
+ // nothing is emitted (CwTimePicker snaps its text back to the value prop as
7591
+ // soon as the typed entry is complete and invalid).
7592
+ //
7593
+ // The time string is empty until a date is selected (visual only): without a
7594
+ // date the whole value is undefined, so showing the default 00:00 would
7595
+ // suggest a selection that doesn't exist.
7596
+ function plainTimeToHHmm(time) {
7597
+ return `${String(time.hour).padStart(2, "0")}:${String(time.minute).padStart(2, "0")}`;
7598
+ }
7599
+ /** "HH:mm" -> PlainTime; unparseable input yields undefined (ignored by the caller) */
7600
+ function hhmmToPlainTime(time) {
7601
+ try {
7602
+ return Temporal.PlainTime.from(time);
7603
+ }
7604
+ catch {
7605
+ return undefined;
7606
+ }
7607
+ }
7608
+ function optionalTimeToHHmm(time) {
7609
+ return time ? plainTimeToHHmm(time) : undefined;
7610
+ }
7611
+ function plainDateTimesEqual(a, b) {
7612
+ if (!a || !b)
7613
+ return a === b;
7614
+ return a.equals(b);
7615
+ }
7616
+ function useCwDateTimePickerTemporal({ value, onChange, minDateTime, maxDateTime, minTime, maxTime, }) {
7617
+ const constraints = React.useMemo(() => ({
7618
+ minDateTime,
7619
+ maxDateTime,
7620
+ minTime: minTime ? hhmmToPlainTime(minTime) : undefined,
7621
+ maxTime: maxTime ? hhmmToPlainTime(maxTime) : undefined,
7622
+ // ISO-string deps keep the object stable across re-created Temporal instances
7623
+ }), [minDateTime?.toString(), maxDateTime?.toString(), minTime, maxTime]);
7624
+ const [state, setState] = React.useState(() => DateTimePickerState.fromValue(value, constraints));
7625
+ // Track the previous value prop to detect external changes.
7626
+ const prevValueRef = React.useRef(value);
7627
+ React.useEffect(() => {
7628
+ if (!plainDateTimesEqual(value, prevValueRef.current)) {
7629
+ prevValueRef.current = value;
7630
+ setState(prev => DateTimePickerState.fromValue(value, prev.constraints));
7631
+ }
7632
+ }, [value]);
7633
+ React.useEffect(() => {
7634
+ setState(prev => prev.withConstraints(constraints));
7635
+ }, [constraints]);
7636
+ const apply = React.useCallback((next) => {
7637
+ if (next === state)
7638
+ return; // operation rejected by the domain - no emission
7639
+ setState(next);
7640
+ onChange(next.value);
7641
+ }, [state, onChange]);
7642
+ const selectDate = React.useCallback((date) => {
7643
+ apply(state.withDate(date));
7644
+ }, [state, apply]);
7645
+ const selectTime = React.useCallback((time) => {
7646
+ if (!time) {
7647
+ // Cleared time input: reset to the in-range default for the current date
7648
+ apply(state.withTimeCleared());
7649
+ return;
7650
+ }
7651
+ const parsed = hhmmToPlainTime(time);
7652
+ if (parsed) {
7653
+ apply(state.withTime(parsed));
7654
+ }
7655
+ }, [state, apply]);
7656
+ const selectNow = React.useCallback(() => {
7657
+ apply(state.withNow(Temporal.Now.plainDateTimeISO()));
7658
+ }, [state, apply]);
7659
+ return {
7660
+ date: state.date,
7661
+ time: state.date ? plainTimeToHHmm(state.time) : "",
7662
+ minDate: minDateTime?.toPlainDate(),
7663
+ maxDate: maxDateTime?.toPlainDate(),
7664
+ effectiveMinTime: optionalTimeToHHmm(state.effectiveMinTime),
7665
+ effectiveMaxTime: optionalTimeToHHmm(state.effectiveMaxTime),
7666
+ selectDate,
7667
+ selectTime,
7668
+ selectNow,
7669
+ };
7670
+ }
7671
+
7672
+ function CwDateTimePickerTemporal({ value, onChange, minDateTime, maxDateTime, disabledDates, disabledMatcher, timeInterval = 15, minTime, maxTime, labelProps, alignProps, datePlaceholder = "dd.MM.yyyy", timePlaceholder = "HH:mm", disabled, required, className, showClear = true, popupPosition = "left-bottom", numberOfMonths = 1, showNowButton = false, nowLabel = "Now", locale: locale$1 = locale.enGB, }) {
7673
+ const picker = useCwDateTimePickerTemporal({
7674
+ value,
7675
+ onChange,
7676
+ minDateTime,
7677
+ maxDateTime,
7678
+ minTime,
7679
+ maxTime,
7680
+ });
7681
+ return jsxRuntime.jsx("div", { className: `cw-datetimepicker cw-datetimepicker-separate ${className || ""}`, children: jsxRuntime.jsxs(CwAlign, { ...alignProps, itemProp: required ? "required" : "", children: [labelProps && (jsxRuntime.jsx(CwLabel, { ...labelProps, children: labelProps.text })), jsxRuntime.jsxs("div", { className: "cw-flex-row cw-align-left-center cw-gap-small", children: [jsxRuntime.jsx(CwDatePickerTemporal, { value: picker.date, onChange: picker.selectDate, minDate: picker.minDate, maxDate: picker.maxDate, disabledDates: disabledDates, disabledMatcher: disabledMatcher, placeholder: datePlaceholder, disabled: disabled, required: required, showClear: showClear, numberOfMonths: numberOfMonths, popupPosition: popupPosition, locale: locale$1 }), jsxRuntime.jsx(CwTimePicker, { value: picker.time, onChange: picker.selectTime, interval: timeInterval, minTime: picker.effectiveMinTime, maxTime: picker.effectiveMaxTime, placeholder: timePlaceholder, disabled: disabled, required: required, showClear: showClear, popupPosition: popupPosition }), showNowButton && (jsxRuntime.jsx(CwButton, { type: "button", variant: "outline", onClick: picker.selectNow, disabled: disabled, text: nowLabel }))] })] }) });
7682
+ }
7683
+
7385
7684
  // dateRangePresetsTemporal.ts
7386
7685
  // Temporal-typed counterpart of dateRangePresets.ts (used by CwDateRangePickerTemporal).
7387
7686
  // Each preset is evaluated at click time; a reference date can be injected for testing.
@@ -10792,6 +11091,7 @@ exports.CwDateRangePicker = CwDateRangePicker;
10792
11091
  exports.CwDateRangePickerTemporal = CwDateRangePickerTemporal;
10793
11092
  exports.CwDateTimePicker = CwDateTimePicker;
10794
11093
  exports.CwDateTimePickerCompact = CwDateTimePickerCompact;
11094
+ exports.CwDateTimePickerTemporal = CwDateTimePickerTemporal;
10795
11095
  exports.CwDialog = CwDialog;
10796
11096
  exports.CwDigit = CwDigit;
10797
11097
  exports.CwDisplayMessage = CwDisplayMessage;