@flytedan/flytebot-design-system 0.29.0 → 0.31.0

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 CHANGED
@@ -1756,7 +1756,7 @@ var BANNER_TONE_STYLE = {
1756
1756
  };
1757
1757
  function Banner({ tone = "info", title, detail, actions, role }) {
1758
1758
  const t = BANNER_TONE_STYLE[tone];
1759
- const liveRole = role || (tone === "danger" || tone === "warn" ? "alert" : "status");
1759
+ const liveRole = role || (tone === "danger" ? "alert" : "status");
1760
1760
  return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
1761
1761
  "div",
1762
1762
  {
@@ -3008,8 +3008,10 @@ function NumberInput({
3008
3008
  max = Infinity,
3009
3009
  step = 1,
3010
3010
  bigStep,
3011
+ integer = false,
3011
3012
  value,
3012
3013
  onChange,
3014
+ onValidityChange,
3013
3015
  format = true,
3014
3016
  className = "",
3015
3017
  style,
@@ -3025,17 +3027,34 @@ function NumberInput({
3025
3027
  const n = Number(s);
3026
3028
  return isNaN(n) ? null : n;
3027
3029
  };
3030
+ const validationMessage = (n) => {
3031
+ if (integer && !Number.isInteger(n)) return "Enter a whole number";
3032
+ if (n < min) return "Must be \u2265 " + min;
3033
+ if (n > max) return "Must be \u2264 " + max;
3034
+ return null;
3035
+ };
3028
3036
  const [text, setText] = React21.useState(value == null || value === "" ? "" : String(value));
3029
3037
  const [editing, setEditing] = React21.useState(false);
3038
+ const [invalidMessage, setInvalidMessage] = React21.useState(null);
3030
3039
  const timer = React21.useRef(null);
3031
3040
  React21.useEffect(() => {
3032
- if (!editing) setText(value == null || value === "" ? "" : String(value));
3041
+ if (!editing) {
3042
+ setText(value == null || value === "" ? "" : String(value));
3043
+ setInvalidMessage(null);
3044
+ }
3033
3045
  }, [value, editing]);
3046
+ React21.useEffect(() => {
3047
+ onValidityChange && onValidityChange(invalidMessage == null);
3048
+ }, [invalidMessage]);
3034
3049
  const clamp = (n) => Math.min(max, Math.max(min, n));
3035
3050
  const fire = (n) => onChange && onChange({ target: { value: n } });
3036
3051
  const bump = (dir, mult = 1) => {
3037
- const cur = num(text) ?? num(value) ?? (min === -Infinity ? 0 : min);
3038
- const next = clamp(cur + dir * (bigStep && mult > 1 ? bigStep : step) * (bigStep ? 1 : mult));
3052
+ const cur = (invalidMessage ? null : num(text)) ?? num(value) ?? (min === -Infinity ? 0 : min);
3053
+ const amount = (bigStep && mult > 1 ? bigStep : step) * (bigStep ? 1 : mult);
3054
+ let next = cur + dir * amount;
3055
+ if (integer) next = Math.round(next);
3056
+ next = clamp(next);
3057
+ setInvalidMessage(null);
3039
3058
  setText(String(next));
3040
3059
  fire(next);
3041
3060
  };
@@ -3059,7 +3078,8 @@ function NumberInput({
3059
3078
  const n = num(text);
3060
3079
  return n == null ? "" : format ? n.toLocaleString() : String(n);
3061
3080
  })();
3062
- const box = ["fd-input", "fd-input-num", error ? "fd-input-error" : "", disabled ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
3081
+ const shownError = error || invalidMessage || void 0;
3082
+ const box = ["fd-input", "fd-input-num", shownError ? "fd-input-error" : "", disabled ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
3063
3083
  const fieldName = label || ariaLabel;
3064
3084
  const stepperName = (verb) => fieldName ? verb + " " + fieldName : verb;
3065
3085
  return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: ["fd-field", className].filter(Boolean).join(" "), style, children: [
@@ -3075,30 +3095,29 @@ function NumberInput({
3075
3095
  disabled,
3076
3096
  placeholder,
3077
3097
  value: shown,
3078
- "aria-invalid": error ? "true" : void 0,
3079
- "aria-describedby": error ? errorId : void 0,
3098
+ "aria-invalid": shownError ? "true" : void 0,
3099
+ "aria-describedby": shownError ? errorId : void 0,
3080
3100
  onFocus: () => setEditing(true),
3081
3101
  onBlur: () => {
3082
3102
  setEditing(false);
3083
- if (text.trim() === "") {
3084
- fire(void 0);
3085
- return;
3086
- }
3087
- const n = num(text);
3088
- if (n != null) {
3089
- const c = clamp(n);
3090
- setText(String(c));
3091
- fire(c);
3092
- }
3103
+ if (text.trim() === "") fire(void 0);
3093
3104
  },
3094
3105
  onChange: (e) => {
3095
- setText(e.target.value);
3096
- if (e.target.value.trim() === "") {
3106
+ const raw = e.target.value;
3107
+ setText(raw);
3108
+ if (raw.trim() === "") {
3109
+ setInvalidMessage(null);
3097
3110
  fire(void 0);
3098
3111
  return;
3099
3112
  }
3100
- const n = num(e.target.value);
3101
- if (n != null) fire(n);
3113
+ const n = num(raw);
3114
+ if (n == null) {
3115
+ setInvalidMessage("Enter a number");
3116
+ return;
3117
+ }
3118
+ const message = validationMessage(n);
3119
+ setInvalidMessage(message);
3120
+ if (!message) fire(n);
3102
3121
  },
3103
3122
  onKeyDown: (e) => {
3104
3123
  if (e.key === "ArrowUp") {
@@ -3141,9 +3160,9 @@ function NumberInput({
3141
3160
  )
3142
3161
  ] })
3143
3162
  ] }),
3144
- error ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { id: errorId, className: "fd-field-error", children: [
3163
+ shownError ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { id: errorId, className: "fd-field-error", children: [
3145
3164
  /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("i", { className: "ph ph-warning-circle", "aria-hidden": "true" }),
3146
- error
3165
+ shownError
3147
3166
  ] }) : help ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "fd-field-help", children: help }) : null
3148
3167
  ] });
3149
3168
  }
@@ -3421,6 +3440,91 @@ function isFieldSet(field, value) {
3421
3440
  }
3422
3441
  }
3423
3442
  }
3443
+ function validateFilterValue(field, value) {
3444
+ switch (field.type) {
3445
+ case "text":
3446
+ case "select":
3447
+ return { ok: true };
3448
+ case "multiSelect": {
3449
+ const v = value;
3450
+ if (!v || !Array.isArray(v.values) || v.values.length === 0) {
3451
+ return { ok: false, reason: "Select at least one value" };
3452
+ }
3453
+ if (!operatorsFor(field).includes(v.operator)) {
3454
+ return { ok: false, reason: "That operator isn't allowed for " + field.label };
3455
+ }
3456
+ return { ok: true };
3457
+ }
3458
+ case "range": {
3459
+ const v = value;
3460
+ if (!v || v.min == null && v.max == null) {
3461
+ return { ok: false, reason: "Enter at least one bound" };
3462
+ }
3463
+ const lo = field.min ?? -Infinity;
3464
+ const hi = field.max ?? Infinity;
3465
+ for (const n of [v.min, v.max]) {
3466
+ if (n == null) continue;
3467
+ if (field.integer && !Number.isInteger(n)) return { ok: false, reason: "Enter a whole number" };
3468
+ if (n < lo) return { ok: false, reason: "Must be \u2265 " + lo };
3469
+ if (n > hi) return { ok: false, reason: "Must be \u2264 " + hi };
3470
+ }
3471
+ if (v.min != null && v.max != null && v.min > v.max) {
3472
+ return { ok: false, reason: "Must be \u2265 " + v.min };
3473
+ }
3474
+ return { ok: true };
3475
+ }
3476
+ case "date": {
3477
+ const v = value;
3478
+ if (!v) return { ok: false, reason: "Pick a date value" };
3479
+ if (v.kind === "withinDays") {
3480
+ const maxDays = field.maxDays ?? 3650;
3481
+ if (v.days == null) return { ok: false, reason: "Enter a number of days" };
3482
+ if (!Number.isInteger(v.days)) return { ok: false, reason: "Enter a whole number" };
3483
+ if (v.days < 1) return { ok: false, reason: "Must be \u2265 1" };
3484
+ if (v.days > maxDays) return { ok: false, reason: "Must be \u2264 " + maxDays };
3485
+ return { ok: true };
3486
+ }
3487
+ if (!v.from || !v.to) return { ok: false, reason: "Pick both dates" };
3488
+ if (v.from > v.to) return { ok: false, reason: "The start date must be on or before the end date" };
3489
+ return { ok: true };
3490
+ }
3491
+ }
3492
+ }
3493
+ function isEmptyCandidate(field, candidate) {
3494
+ switch (field.type) {
3495
+ case "text":
3496
+ case "select":
3497
+ return false;
3498
+ case "multiSelect": {
3499
+ const v = candidate;
3500
+ return !v || !Array.isArray(v.values) || v.values.length === 0;
3501
+ }
3502
+ case "range": {
3503
+ const v = candidate;
3504
+ return !v || v.min == null && v.max == null;
3505
+ }
3506
+ case "date": {
3507
+ const v = candidate;
3508
+ if (!v) return true;
3509
+ return v.kind === "withinDays" ? v.days == null : !(v.from && v.to);
3510
+ }
3511
+ }
3512
+ }
3513
+ function assertValidDateField(field) {
3514
+ if (process.env.NODE_ENV === "production") return;
3515
+ const defaultDays = field.defaultDays ?? 30;
3516
+ const maxDays = field.maxDays ?? 3650;
3517
+ if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays > 3650) {
3518
+ throw new Error(
3519
+ 'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to 3650."
3520
+ );
3521
+ }
3522
+ if (!Number.isInteger(defaultDays) || defaultDays < 1 || defaultDays > maxDays) {
3523
+ throw new Error(
3524
+ 'FilterBar: date field "' + field.key + '" has an invalid defaultDays (' + field.defaultDays + ") \u2014 must be a whole number from 1 to maxDays (" + maxDays + ")."
3525
+ );
3526
+ }
3527
+ }
3424
3528
  function chipLabel(field, value) {
3425
3529
  switch (field.type) {
3426
3530
  case "text":
@@ -3473,18 +3577,30 @@ function MultiSelectPanel({
3473
3577
  field,
3474
3578
  value,
3475
3579
  anchorRef,
3476
- onChange,
3477
- onRemove,
3580
+ onCommit,
3478
3581
  onClose
3479
3582
  }) {
3480
3583
  const allowedOps = operatorsFor(field);
3481
- const operator = allowedOps.length === 1 ? allowedOps[0] : value?.operator ?? allowedOps[0];
3584
+ const [draftOperator, setDraftOperator] = React24.useState(value?.operator ?? allowedOps[0]);
3585
+ const operator = allowedOps.length === 1 ? allowedOps[0] : value?.operator ?? draftOperator;
3482
3586
  const selected = value?.values ?? [];
3483
3587
  const selectedRef = React24.useRef(selected);
3484
3588
  selectedRef.current = selected;
3589
+ const operatorRef = React24.useRef(operator);
3590
+ operatorRef.current = operator;
3591
+ const mountedRef = React24.useRef(true);
3592
+ React24.useEffect(() => {
3593
+ mountedRef.current = true;
3594
+ return () => {
3595
+ mountedRef.current = false;
3596
+ };
3597
+ }, []);
3485
3598
  const commit = (nextValues, nextOperator = operator) => {
3486
- if (nextValues.length === 0) onRemove();
3487
- else onChange({ operator: nextOperator, values: nextValues });
3599
+ onCommit({ operator: nextOperator, values: nextValues });
3600
+ };
3601
+ const setOperator = (op) => {
3602
+ setDraftOperator(op);
3603
+ if (selected.length > 0) commit(selected, op);
3488
3604
  };
3489
3605
  const [mode2, setMode] = React24.useState("search");
3490
3606
  const [query, setQuery] = React24.useState("");
@@ -3522,18 +3638,20 @@ function MultiSelectPanel({
3522
3638
  const options = field.loadOptions ? asyncState.options ?? [] : (field.options ?? []).filter((o) => o.label.toLowerCase().includes(query.toLowerCase()));
3523
3639
  const showLoading = !!field.loadOptions && asyncState.loading;
3524
3640
  const showError = !!field.loadOptions && asyncState.error;
3525
- const showListbox = !showLoading && !showError;
3641
+ const hasListbox = !showLoading && !showError && options.length > 0;
3526
3642
  const isSelected = (o) => selected.some((s) => s.value === o.value);
3527
3643
  const toggle = (o) => {
3528
3644
  const next = isSelected(o) ? selected.filter((s) => s.value !== o.value) : [...selected, o];
3529
3645
  commit(next);
3530
3646
  };
3531
3647
  const nav = useListboxNav({ count: options.length, onSelect: (i) => toggle(options[i]) });
3532
- useListboxScrollIntoView(nav.active, listRef, showListbox);
3648
+ useListboxScrollIntoView(nav.active, listRef, hasListbox);
3533
3649
  React24.useEffect(() => {
3534
3650
  nav.setActive(0);
3535
3651
  }, [query, options.length]);
3536
- const activeId = showListbox && options[nav.active] ? listId + "-opt-" + nav.active : void 0;
3652
+ const activeId = hasListbox && options[nav.active] ? listId + "-opt-" + nav.active : void 0;
3653
+ const liveRole = showError ? "alert" : "status";
3654
+ const liveMessage = showLoading ? "Loading\u2026" : showError ? "Couldn't load options." : options.length === 0 ? "No matches" : "";
3537
3655
  const [pasteText, setPasteText] = React24.useState("");
3538
3656
  const [pasteLoading, setPasteLoading] = React24.useState(false);
3539
3657
  const [unmatchedTokens, setUnmatchedTokens] = React24.useState([]);
@@ -3549,19 +3667,21 @@ function MultiSelectPanel({
3549
3667
  setPasteLoading(true);
3550
3668
  setPasteError(null);
3551
3669
  field.resolve(tokens).then((result) => {
3670
+ if (!mountedRef.current) return;
3552
3671
  setPasteLoading(false);
3553
3672
  setUnmatchedTokens(result.unmatched);
3554
3673
  const merged = new Map(selectedRef.current.map((s) => [s.value, s]));
3555
3674
  for (const m of result.matched) merged.set(m.value, { value: m.value, label: m.label });
3556
- commit(Array.from(merged.values()));
3675
+ commit(Array.from(merged.values()), operatorRef.current);
3557
3676
  setPasteText("");
3558
3677
  }).catch((err) => {
3678
+ if (!mountedRef.current) return;
3559
3679
  setPasteLoading(false);
3560
3680
  setPasteError(err instanceof Error ? err.message : "Could not resolve the pasted list.");
3561
3681
  });
3562
3682
  };
3563
3683
  const header = /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "fd-stack fd-gap-sm", children: [
3564
- allowedOps.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(OperatorToggle, { operator, onChange: (op) => commit(selected, op), label: field.label }) : null,
3684
+ allowedOps.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(OperatorToggle, { operator, onChange: setOperator, label: field.label }) : null,
3565
3685
  field.resolve ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3566
3686
  SegmentedControl,
3567
3687
  {
@@ -3578,8 +3698,8 @@ function MultiSelectPanel({
3578
3698
  {
3579
3699
  ref: inputRef,
3580
3700
  role: "combobox",
3581
- "aria-expanded": showListbox,
3582
- "aria-controls": showListbox ? listId : void 0,
3701
+ "aria-expanded": hasListbox,
3702
+ "aria-controls": hasListbox ? listId : void 0,
3583
3703
  "aria-autocomplete": "list",
3584
3704
  "aria-activedescendant": activeId,
3585
3705
  "aria-label": field.label,
@@ -3635,33 +3755,41 @@ function MultiSelectPanel({
3635
3755
  ] }),
3636
3756
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-row fd-wrap fd-gap-xs", children: selected.map((v) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Tag, { onRemove: () => commit(selected.filter((s) => s.value !== v.value)), children: v.label }, v.value)) })
3637
3757
  ] }) : null
3638
- ] }) : showLoading ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "fd-row fd-gap-xs", role: "status", children: [
3639
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-spinner", "aria-hidden": "true" }),
3640
- " Loading\u2026"
3641
- ] }) : showError ? /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "fd-stack fd-gap-sm", children: [
3642
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-body-sm", role: "alert", children: "Couldn't load options." }),
3643
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: () => setRetryTick((n) => n + 1), children: "Retry" })
3644
- ] }) : options.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-body-sm fd-muted", role: "status", children: "No matches" }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { id: listId, role: "listbox", "aria-label": field.label, "aria-multiselectable": "true", ref: listRef, className: "fd-pop-list", children: options.map((o, i) => {
3645
- const on = isSelected(o);
3646
- return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3647
- "div",
3758
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "fd-stack fd-gap-sm", children: [
3759
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3760
+ "span",
3648
3761
  {
3649
- id: listId + "-opt-" + i,
3650
- "data-i": i,
3651
- role: "option",
3652
- tabIndex: -1,
3653
- "aria-selected": on,
3654
- className: ["fd-opt", i === nav.active ? "is-active" : "", on ? "is-selected" : ""].filter(Boolean).join(" "),
3655
- onMouseEnter: () => nav.setActive(i),
3656
- onClick: () => toggle(o),
3762
+ className: "fd-row fd-gap-xs" + (liveMessage && !showError ? " fd-muted" : ""),
3763
+ role: liveRole,
3657
3764
  children: [
3658
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("i", { className: "fd-filterbar-opt-check ph " + (on ? "ph-check-square is-checked" : "ph-square"), "aria-hidden": "true" }),
3659
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-opt-label", children: o.label })
3765
+ showLoading ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-spinner", "aria-hidden": "true" }) : null,
3766
+ liveMessage ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-body-sm", children: liveMessage }) : null
3660
3767
  ]
3661
- },
3662
- o.value
3663
- );
3664
- }) });
3768
+ }
3769
+ ),
3770
+ showError ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: () => setRetryTick((n) => n + 1), children: "Retry" }) : null,
3771
+ hasListbox ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { id: listId, role: "listbox", "aria-label": field.label, "aria-multiselectable": "true", ref: listRef, className: "fd-pop-list", children: options.map((o, i) => {
3772
+ const on = isSelected(o);
3773
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3774
+ "div",
3775
+ {
3776
+ id: listId + "-opt-" + i,
3777
+ "data-i": i,
3778
+ role: "option",
3779
+ tabIndex: -1,
3780
+ "aria-selected": on,
3781
+ className: ["fd-opt", i === nav.active ? "is-active" : "", on ? "is-selected" : ""].filter(Boolean).join(" "),
3782
+ onMouseEnter: () => nav.setActive(i),
3783
+ onClick: () => toggle(o),
3784
+ children: [
3785
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("i", { className: "fd-filterbar-opt-check ph " + (on ? "ph-check-square is-checked" : "ph-square"), "aria-hidden": "true" }),
3786
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-opt-label", children: o.label })
3787
+ ]
3788
+ },
3789
+ o.value
3790
+ );
3791
+ }) }) : null
3792
+ ] });
3665
3793
  const footer = mode2 === "paste" ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: doResolve, disabled: pasteLoading || !pasteText.trim(), children: pasteLoading ? "Resolving\u2026" : "Resolve" }) : void 0;
3666
3794
  return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3667
3795
  Popover,
@@ -3683,22 +3811,21 @@ function RangePanel({
3683
3811
  field,
3684
3812
  value,
3685
3813
  anchorRef,
3686
- onChange,
3687
- onRemove,
3814
+ onCommit,
3688
3815
  onClose
3689
3816
  }) {
3690
3817
  const [draftMin, setDraftMin] = React24.useState(value?.min);
3691
3818
  const [draftMax, setDraftMax] = React24.useState(value?.max);
3692
- const invalid = draftMin != null && draftMax != null && draftMin > draftMax;
3819
+ const [minTextValid, setMinTextValid] = React24.useState(true);
3820
+ const [maxTextValid, setMaxTextValid] = React24.useState(true);
3821
+ const isEmpty = draftMin == null && draftMax == null;
3822
+ const validation = isEmpty ? { ok: true } : validateFilterValue(field, { min: draftMin, max: draftMax });
3823
+ const comboError = !isEmpty && !validation.ok ? validation.reason : void 0;
3824
+ const doneDisabled = !minTextValid || !maxTextValid || !!comboError;
3693
3825
  const apply = (nextMin, nextMax) => {
3694
3826
  setDraftMin(nextMin);
3695
3827
  setDraftMax(nextMax);
3696
- if (nextMin == null && nextMax == null) {
3697
- onRemove();
3698
- return;
3699
- }
3700
- if (nextMin != null && nextMax != null && nextMin > nextMax) return;
3701
- onChange({ min: nextMin, max: nextMax });
3828
+ onCommit(nextMin == null && nextMax == null ? void 0 : { min: nextMin, max: nextMax });
3702
3829
  };
3703
3830
  return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3704
3831
  Popover,
@@ -3710,7 +3837,7 @@ function RangePanel({
3710
3837
  offset: 6,
3711
3838
  width: PANEL_WIDTH.range,
3712
3839
  label: field.label,
3713
- footer: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: onClose, children: "Done" }),
3840
+ footer: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: onClose, disabled: doneDisabled, children: "Done" }),
3714
3841
  children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "fd-stack fd-gap-sm", children: [
3715
3842
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3716
3843
  NumberInput,
@@ -3720,8 +3847,10 @@ function RangePanel({
3720
3847
  min: field.min,
3721
3848
  max: field.max,
3722
3849
  step: field.step,
3850
+ integer: field.integer,
3723
3851
  placeholder: field.minPlaceholder,
3724
- onChange: (e) => apply(e.target.value, draftMax)
3852
+ onChange: (e) => apply(e.target.value, draftMax),
3853
+ onValidityChange: setMinTextValid
3725
3854
  }
3726
3855
  ),
3727
3856
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
@@ -3732,9 +3861,11 @@ function RangePanel({
3732
3861
  min: field.min,
3733
3862
  max: field.max,
3734
3863
  step: field.step,
3735
- error: invalid ? "Must be \u2265 " + draftMin : void 0,
3864
+ integer: field.integer,
3865
+ error: comboError,
3736
3866
  placeholder: field.maxPlaceholder,
3737
- onChange: (e) => apply(draftMin, e.target.value)
3867
+ onChange: (e) => apply(draftMin, e.target.value),
3868
+ onValidityChange: setMaxTextValid
3738
3869
  }
3739
3870
  )
3740
3871
  ] })
@@ -3745,49 +3876,35 @@ function DatePanel({
3745
3876
  field,
3746
3877
  value,
3747
3878
  anchorRef,
3748
- onChange,
3749
- onRemove,
3879
+ onCommit,
3750
3880
  onClose
3751
3881
  }) {
3752
3882
  const defaultDays = field.defaultDays ?? 30;
3753
- const [draft, setDraft] = React24.useState(value ?? { kind: "withinDays", days: defaultDays });
3754
- const committedRef = React24.useRef(!!value);
3883
+ const maxDays = field.maxDays ?? 3650;
3884
+ const [kind, setKind] = React24.useState(value?.kind ?? "withinDays");
3885
+ const [daysDraft, setDaysDraft] = React24.useState(
3886
+ value?.kind === "withinDays" ? value.days : defaultDays
3887
+ );
3888
+ const [between, setBetween] = React24.useState(
3889
+ value?.kind === "between" ? { from: value.from, to: value.to } : { from: "", to: "" }
3890
+ );
3755
3891
  const applyWithinDays = (days) => {
3756
- const next = { kind: "withinDays", days: days ?? defaultDays };
3757
- setDraft(next);
3758
- if (days != null) {
3759
- committedRef.current = true;
3760
- onChange(next);
3761
- }
3892
+ setDaysDraft(days);
3893
+ onCommit(days == null ? void 0 : { kind: "withinDays", days });
3762
3894
  };
3763
3895
  const applyBetween = (from, to) => {
3764
- const next = { kind: "between", from, to };
3765
- setDraft(next);
3766
- if (from && to) {
3767
- committedRef.current = true;
3768
- onChange(next);
3769
- } else if (committedRef.current) {
3770
- committedRef.current = false;
3771
- onRemove();
3772
- }
3896
+ setBetween({ from, to });
3897
+ onCommit(from && to ? { kind: "between", from, to } : void 0);
3773
3898
  };
3774
- const switchKind = (kind) => {
3775
- if (kind === "withinDays") {
3776
- const days = draft.kind === "withinDays" ? draft.days : defaultDays;
3777
- setDraft({ kind: "withinDays", days });
3778
- } else {
3779
- setDraft({ kind: "between", from: "", to: "" });
3780
- if (committedRef.current) {
3781
- committedRef.current = false;
3782
- onRemove();
3783
- }
3784
- }
3899
+ const switchKind = (next) => {
3900
+ if (next === kind) return;
3901
+ setKind(next);
3902
+ if (next === "between") setBetween({ from: "", to: "" });
3903
+ else setDaysDraft(defaultDays);
3904
+ onCommit(void 0);
3785
3905
  };
3786
3906
  const confirm = () => {
3787
- if (draft.kind === "withinDays") {
3788
- committedRef.current = true;
3789
- onChange(draft);
3790
- }
3907
+ if (kind === "withinDays" && daysDraft != null) onCommit({ kind: "withinDays", days: daysDraft });
3791
3908
  onClose();
3792
3909
  };
3793
3910
  return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
@@ -3810,16 +3927,16 @@ function DatePanel({
3810
3927
  { value: "withinDays", label: "Within last N days" },
3811
3928
  { value: "between", label: "Between dates" }
3812
3929
  ],
3813
- value: draft.kind,
3930
+ value: kind,
3814
3931
  onChange: (k) => switchKind(k)
3815
3932
  }
3816
3933
  ),
3817
- draft.kind === "withinDays" ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(NumberInput, { label: "Days", min: 1, value: draft.days, onChange: (e) => applyWithinDays(e.target.value) }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3934
+ kind === "withinDays" ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(NumberInput, { label: "Days", min: 1, max: maxDays, step: 1, integer: true, value: daysDraft ?? "", onChange: (e) => applyWithinDays(e.target.value) }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3818
3935
  DatePicker,
3819
3936
  {
3820
3937
  range: true,
3821
3938
  label: "Date range",
3822
- value: { start: draft.from, end: draft.to },
3939
+ value: { start: between.from, end: between.to },
3823
3940
  onChange: (e) => applyBetween(e.target.value.start || "", e.target.value.end || "")
3824
3941
  }
3825
3942
  )
@@ -3833,6 +3950,21 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
3833
3950
  const addAnchorRef = React24.useRef(null);
3834
3951
  const textInputRef = React24.useRef(null);
3835
3952
  const optionsRef = React24.useRef(null);
3953
+ if (process.env.NODE_ENV !== "production") {
3954
+ for (const f of fields) if (f.type === "date") assertValidDateField(f);
3955
+ }
3956
+ const commit = (field, candidate) => {
3957
+ if (isEmptyCandidate(field, candidate)) {
3958
+ removeField(field);
3959
+ return { ok: true };
3960
+ }
3961
+ const result = validateFilterValue(field, candidate);
3962
+ if (result.ok) onFilterChange(field.key, candidate);
3963
+ return result;
3964
+ };
3965
+ const removeField = (field) => {
3966
+ if (isFieldSet(field, filters[field.key])) onFilterRemove(field.key);
3967
+ };
3836
3968
  const editingField = panel && panel.kind === "edit" ? fields.find((f) => f.key === panel.key) : void 0;
3837
3969
  const startEditing = (key) => setPanel({ kind: "edit", key });
3838
3970
  const closeEditing = () => setPanel(null);
@@ -3864,7 +3996,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
3864
3996
  value: value ?? "",
3865
3997
  placeholder: field.placeholder || field.label,
3866
3998
  "aria-label": field.label,
3867
- onChange: (e) => onFilterChange(field.key, e.target.value),
3999
+ onChange: (e) => commit(field, e.target.value),
3868
4000
  onKeyDown: (e) => {
3869
4001
  if (e.key === "Enter") setPanel(null);
3870
4002
  else if (e.key === "Escape") {
@@ -3883,7 +4015,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
3883
4015
  "aria-label": "Remove " + field.label,
3884
4016
  onClick: (e) => {
3885
4017
  e.stopPropagation();
3886
- onFilterRemove(field.key);
4018
+ removeField(field);
3887
4019
  setPanel(null);
3888
4020
  },
3889
4021
  children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("i", { className: "ph ph-x", "aria-hidden": "true" })
@@ -3896,7 +4028,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
3896
4028
  const chipRef = editing ? editAnchorRef : void 0;
3897
4029
  const toggleOrClose = () => editing ? closeEditing() : startEditing(field.key);
3898
4030
  const remove = () => {
3899
- if (set) onFilterRemove(field.key);
4031
+ removeField(field);
3900
4032
  closeEditing();
3901
4033
  };
3902
4034
  const chipText = field.label + (set ? ": " + chipLabel(field, value) : "");
@@ -3926,7 +4058,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
3926
4058
  "aria-selected": on,
3927
4059
  className: ["fd-opt", on ? "is-selected" : ""].filter(Boolean).join(" "),
3928
4060
  onClick: () => {
3929
- onFilterChange(field.key, o.value);
4061
+ commit(field, o.value);
3930
4062
  setPanel(null);
3931
4063
  },
3932
4064
  children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-opt-label", children: o.label })
@@ -3947,8 +4079,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
3947
4079
  field,
3948
4080
  value,
3949
4081
  anchorRef: editAnchorRef,
3950
- onChange: (v) => onFilterChange(field.key, v),
3951
- onRemove: () => onFilterRemove(field.key),
4082
+ onCommit: (v) => commit(field, v),
3952
4083
  onClose: closeEditing
3953
4084
  }
3954
4085
  ) : null
@@ -3963,8 +4094,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
3963
4094
  field,
3964
4095
  value,
3965
4096
  anchorRef: editAnchorRef,
3966
- onChange: (v) => onFilterChange(field.key, v),
3967
- onRemove: () => onFilterRemove(field.key),
4097
+ onCommit: (v) => commit(field, v),
3968
4098
  onClose: closeEditing
3969
4099
  }
3970
4100
  ) : null
@@ -3979,8 +4109,7 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
3979
4109
  field,
3980
4110
  value,
3981
4111
  anchorRef: editAnchorRef,
3982
- onChange: (v) => onFilterChange(field.key, v),
3983
- onRemove: () => onFilterRemove(field.key),
4112
+ onCommit: (v) => commit(field, v),
3984
4113
  onClose: closeEditing
3985
4114
  }
3986
4115
  ) : null