@flytedan/flytebot-design-system 0.29.0 → 0.30.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
  {
@@ -3025,17 +3025,29 @@ function NumberInput({
3025
3025
  const n = Number(s);
3026
3026
  return isNaN(n) ? null : n;
3027
3027
  };
3028
+ const requiresInteger = Number.isInteger(step);
3029
+ const validationMessage = (n) => {
3030
+ if (requiresInteger && !Number.isInteger(n)) return "Enter a whole number";
3031
+ if (n < min) return "Must be \u2265 " + min;
3032
+ if (n > max) return "Must be \u2264 " + max;
3033
+ return null;
3034
+ };
3028
3035
  const [text, setText] = React21.useState(value == null || value === "" ? "" : String(value));
3029
3036
  const [editing, setEditing] = React21.useState(false);
3037
+ const [invalidMessage, setInvalidMessage] = React21.useState(null);
3030
3038
  const timer = React21.useRef(null);
3031
3039
  React21.useEffect(() => {
3032
- if (!editing) setText(value == null || value === "" ? "" : String(value));
3040
+ if (!editing) {
3041
+ setText(value == null || value === "" ? "" : String(value));
3042
+ setInvalidMessage(null);
3043
+ }
3033
3044
  }, [value, editing]);
3034
3045
  const clamp = (n) => Math.min(max, Math.max(min, n));
3035
3046
  const fire = (n) => onChange && onChange({ target: { value: n } });
3036
3047
  const bump = (dir, mult = 1) => {
3037
3048
  const cur = num(text) ?? num(value) ?? (min === -Infinity ? 0 : min);
3038
3049
  const next = clamp(cur + dir * (bigStep && mult > 1 ? bigStep : step) * (bigStep ? 1 : mult));
3050
+ setInvalidMessage(null);
3039
3051
  setText(String(next));
3040
3052
  fire(next);
3041
3053
  };
@@ -3059,7 +3071,8 @@ function NumberInput({
3059
3071
  const n = num(text);
3060
3072
  return n == null ? "" : format ? n.toLocaleString() : String(n);
3061
3073
  })();
3062
- const box = ["fd-input", "fd-input-num", error ? "fd-input-error" : "", disabled ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
3074
+ const shownError = error || invalidMessage || void 0;
3075
+ const box = ["fd-input", "fd-input-num", shownError ? "fd-input-error" : "", disabled ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
3063
3076
  const fieldName = label || ariaLabel;
3064
3077
  const stepperName = (verb) => fieldName ? verb + " " + fieldName : verb;
3065
3078
  return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("div", { className: ["fd-field", className].filter(Boolean).join(" "), style, children: [
@@ -3075,30 +3088,29 @@ function NumberInput({
3075
3088
  disabled,
3076
3089
  placeholder,
3077
3090
  value: shown,
3078
- "aria-invalid": error ? "true" : void 0,
3079
- "aria-describedby": error ? errorId : void 0,
3091
+ "aria-invalid": shownError ? "true" : void 0,
3092
+ "aria-describedby": shownError ? errorId : void 0,
3080
3093
  onFocus: () => setEditing(true),
3081
3094
  onBlur: () => {
3082
3095
  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
- }
3096
+ if (text.trim() === "") fire(void 0);
3093
3097
  },
3094
3098
  onChange: (e) => {
3095
- setText(e.target.value);
3096
- if (e.target.value.trim() === "") {
3099
+ const raw = e.target.value;
3100
+ setText(raw);
3101
+ if (raw.trim() === "") {
3102
+ setInvalidMessage(null);
3097
3103
  fire(void 0);
3098
3104
  return;
3099
3105
  }
3100
- const n = num(e.target.value);
3101
- if (n != null) fire(n);
3106
+ const n = num(raw);
3107
+ if (n == null) {
3108
+ setInvalidMessage("Enter a number");
3109
+ return;
3110
+ }
3111
+ const message = validationMessage(n);
3112
+ setInvalidMessage(message);
3113
+ if (!message) fire(n);
3102
3114
  },
3103
3115
  onKeyDown: (e) => {
3104
3116
  if (e.key === "ArrowUp") {
@@ -3141,9 +3153,9 @@ function NumberInput({
3141
3153
  )
3142
3154
  ] })
3143
3155
  ] }),
3144
- error ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { id: errorId, className: "fd-field-error", children: [
3156
+ shownError ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)("span", { id: errorId, className: "fd-field-error", children: [
3145
3157
  /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("i", { className: "ph ph-warning-circle", "aria-hidden": "true" }),
3146
- error
3158
+ shownError
3147
3159
  ] }) : help ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("span", { className: "fd-field-help", children: help }) : null
3148
3160
  ] });
3149
3161
  }
@@ -3478,7 +3490,8 @@ function MultiSelectPanel({
3478
3490
  onClose
3479
3491
  }) {
3480
3492
  const allowedOps = operatorsFor(field);
3481
- const operator = allowedOps.length === 1 ? allowedOps[0] : value?.operator ?? allowedOps[0];
3493
+ const [draftOperator, setDraftOperator] = React24.useState(value?.operator ?? allowedOps[0]);
3494
+ const operator = allowedOps.length === 1 ? allowedOps[0] : value?.operator ?? draftOperator;
3482
3495
  const selected = value?.values ?? [];
3483
3496
  const selectedRef = React24.useRef(selected);
3484
3497
  selectedRef.current = selected;
@@ -3486,6 +3499,10 @@ function MultiSelectPanel({
3486
3499
  if (nextValues.length === 0) onRemove();
3487
3500
  else onChange({ operator: nextOperator, values: nextValues });
3488
3501
  };
3502
+ const setOperator = (op) => {
3503
+ setDraftOperator(op);
3504
+ if (selected.length > 0) commit(selected, op);
3505
+ };
3489
3506
  const [mode2, setMode] = React24.useState("search");
3490
3507
  const [query, setQuery] = React24.useState("");
3491
3508
  const [asyncState, setAsyncState] = React24.useState(
@@ -3522,18 +3539,20 @@ function MultiSelectPanel({
3522
3539
  const options = field.loadOptions ? asyncState.options ?? [] : (field.options ?? []).filter((o) => o.label.toLowerCase().includes(query.toLowerCase()));
3523
3540
  const showLoading = !!field.loadOptions && asyncState.loading;
3524
3541
  const showError = !!field.loadOptions && asyncState.error;
3525
- const showListbox = !showLoading && !showError;
3542
+ const hasListbox = !showLoading && !showError && options.length > 0;
3526
3543
  const isSelected = (o) => selected.some((s) => s.value === o.value);
3527
3544
  const toggle = (o) => {
3528
3545
  const next = isSelected(o) ? selected.filter((s) => s.value !== o.value) : [...selected, o];
3529
3546
  commit(next);
3530
3547
  };
3531
3548
  const nav = useListboxNav({ count: options.length, onSelect: (i) => toggle(options[i]) });
3532
- useListboxScrollIntoView(nav.active, listRef, showListbox);
3549
+ useListboxScrollIntoView(nav.active, listRef, hasListbox);
3533
3550
  React24.useEffect(() => {
3534
3551
  nav.setActive(0);
3535
3552
  }, [query, options.length]);
3536
- const activeId = showListbox && options[nav.active] ? listId + "-opt-" + nav.active : void 0;
3553
+ const activeId = hasListbox && options[nav.active] ? listId + "-opt-" + nav.active : void 0;
3554
+ const liveRole = showError ? "alert" : "status";
3555
+ const liveMessage = showLoading ? "Loading\u2026" : showError ? "Couldn't load options." : options.length === 0 ? "No matches" : "";
3537
3556
  const [pasteText, setPasteText] = React24.useState("");
3538
3557
  const [pasteLoading, setPasteLoading] = React24.useState(false);
3539
3558
  const [unmatchedTokens, setUnmatchedTokens] = React24.useState([]);
@@ -3561,7 +3580,7 @@ function MultiSelectPanel({
3561
3580
  });
3562
3581
  };
3563
3582
  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,
3583
+ allowedOps.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(OperatorToggle, { operator, onChange: setOperator, label: field.label }) : null,
3565
3584
  field.resolve ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3566
3585
  SegmentedControl,
3567
3586
  {
@@ -3578,8 +3597,8 @@ function MultiSelectPanel({
3578
3597
  {
3579
3598
  ref: inputRef,
3580
3599
  role: "combobox",
3581
- "aria-expanded": showListbox,
3582
- "aria-controls": showListbox ? listId : void 0,
3600
+ "aria-expanded": hasListbox,
3601
+ "aria-controls": hasListbox ? listId : void 0,
3583
3602
  "aria-autocomplete": "list",
3584
3603
  "aria-activedescendant": activeId,
3585
3604
  "aria-label": field.label,
@@ -3635,33 +3654,41 @@ function MultiSelectPanel({
3635
3654
  ] }),
3636
3655
  /* @__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
3656
  ] }) : 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",
3657
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "fd-stack fd-gap-sm", children: [
3658
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3659
+ "span",
3648
3660
  {
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),
3661
+ className: "fd-row fd-gap-xs" + (liveMessage && !showError ? " fd-muted" : ""),
3662
+ role: liveRole,
3657
3663
  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 })
3664
+ showLoading ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-spinner", "aria-hidden": "true" }) : null,
3665
+ liveMessage ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-body-sm", children: liveMessage }) : null
3660
3666
  ]
3661
- },
3662
- o.value
3663
- );
3664
- }) });
3667
+ }
3668
+ ),
3669
+ showError ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: () => setRetryTick((n) => n + 1), children: "Retry" }) : null,
3670
+ 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) => {
3671
+ const on = isSelected(o);
3672
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3673
+ "div",
3674
+ {
3675
+ id: listId + "-opt-" + i,
3676
+ "data-i": i,
3677
+ role: "option",
3678
+ tabIndex: -1,
3679
+ "aria-selected": on,
3680
+ className: ["fd-opt", i === nav.active ? "is-active" : "", on ? "is-selected" : ""].filter(Boolean).join(" "),
3681
+ onMouseEnter: () => nav.setActive(i),
3682
+ onClick: () => toggle(o),
3683
+ children: [
3684
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("i", { className: "fd-filterbar-opt-check ph " + (on ? "ph-check-square is-checked" : "ph-square"), "aria-hidden": "true" }),
3685
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "fd-opt-label", children: o.label })
3686
+ ]
3687
+ },
3688
+ o.value
3689
+ );
3690
+ }) }) : null
3691
+ ] });
3665
3692
  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
3693
  return /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3667
3694
  Popover,
@@ -3710,7 +3737,7 @@ function RangePanel({
3710
3737
  offset: 6,
3711
3738
  width: PANEL_WIDTH.range,
3712
3739
  label: field.label,
3713
- footer: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: onClose, children: "Done" }),
3740
+ footer: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(Button, { size: "sm", variant: "secondary", onClick: onClose, disabled: invalid, children: "Done" }),
3714
3741
  children: /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("span", { className: "fd-stack fd-gap-sm", children: [
3715
3742
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3716
3743
  NumberInput,
@@ -3750,33 +3777,39 @@ function DatePanel({
3750
3777
  onClose
3751
3778
  }) {
3752
3779
  const defaultDays = field.defaultDays ?? 30;
3753
- const [draft, setDraft] = React24.useState(value ?? { kind: "withinDays", days: defaultDays });
3780
+ const maxDays = field.maxDays ?? 3650;
3781
+ const [kind, setKind] = React24.useState(value?.kind ?? "withinDays");
3782
+ const [daysDraft, setDaysDraft] = React24.useState(
3783
+ value?.kind === "withinDays" ? value.days : defaultDays
3784
+ );
3785
+ const [between, setBetween] = React24.useState(
3786
+ value?.kind === "between" ? { from: value.from, to: value.to } : { from: "", to: "" }
3787
+ );
3754
3788
  const committedRef = React24.useRef(!!value);
3755
3789
  const applyWithinDays = (days) => {
3756
- const next = { kind: "withinDays", days: days ?? defaultDays };
3757
- setDraft(next);
3790
+ setDaysDraft(days);
3758
3791
  if (days != null) {
3759
3792
  committedRef.current = true;
3760
- onChange(next);
3793
+ onChange({ kind: "withinDays", days });
3794
+ } else if (committedRef.current) {
3795
+ committedRef.current = false;
3796
+ onRemove();
3761
3797
  }
3762
3798
  };
3763
3799
  const applyBetween = (from, to) => {
3764
- const next = { kind: "between", from, to };
3765
- setDraft(next);
3800
+ setBetween({ from, to });
3766
3801
  if (from && to) {
3767
3802
  committedRef.current = true;
3768
- onChange(next);
3803
+ onChange({ kind: "between", from, to });
3769
3804
  } else if (committedRef.current) {
3770
3805
  committedRef.current = false;
3771
3806
  onRemove();
3772
3807
  }
3773
3808
  };
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: "" });
3809
+ const switchKind = (next) => {
3810
+ setKind(next);
3811
+ if (next === "between") {
3812
+ setBetween({ from: "", to: "" });
3780
3813
  if (committedRef.current) {
3781
3814
  committedRef.current = false;
3782
3815
  onRemove();
@@ -3784,9 +3817,9 @@ function DatePanel({
3784
3817
  }
3785
3818
  };
3786
3819
  const confirm = () => {
3787
- if (draft.kind === "withinDays") {
3820
+ if (kind === "withinDays" && daysDraft != null) {
3788
3821
  committedRef.current = true;
3789
- onChange(draft);
3822
+ onChange({ kind: "withinDays", days: daysDraft });
3790
3823
  }
3791
3824
  onClose();
3792
3825
  };
@@ -3810,16 +3843,16 @@ function DatePanel({
3810
3843
  { value: "withinDays", label: "Within last N days" },
3811
3844
  { value: "between", label: "Between dates" }
3812
3845
  ],
3813
- value: draft.kind,
3846
+ value: kind,
3814
3847
  onChange: (k) => switchKind(k)
3815
3848
  }
3816
3849
  ),
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)(
3850
+ kind === "withinDays" ? /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(NumberInput, { label: "Days", min: 1, max: maxDays, step: 1, value: daysDraft ?? "", onChange: (e) => applyWithinDays(e.target.value) }) : /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3818
3851
  DatePicker,
3819
3852
  {
3820
3853
  range: true,
3821
3854
  label: "Date range",
3822
- value: { start: draft.from, end: draft.to },
3855
+ value: { start: between.from, end: between.to },
3823
3856
  onChange: (e) => applyBetween(e.target.value.start || "", e.target.value.end || "")
3824
3857
  }
3825
3858
  )