@flytedan/flytebot-design-system 0.32.0 → 0.33.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.d.cts CHANGED
@@ -1190,12 +1190,17 @@ interface FilterBarProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "on
1190
1190
  fields: FilterField[];
1191
1191
  /** Straight off useServerTable — a value of undefined/""/false reads as "not set",
1192
1192
  * same convention that hook already uses for activeFilterCount. multiSelect/range/
1193
- * date values are objects instead (see isFieldSet). */
1193
+ * date values are objects instead (see isFieldSet). Pass a new object whenever the
1194
+ * filters change (as a state update does): FilterBar re-reads this record only when
1195
+ * its identity changes, so a parent that re-renders before applying an emission
1196
+ * doesn't make FilterBar forget it. */
1194
1197
  filters: Record<string, unknown>;
1195
1198
  /** Fires on every value edit. Maps 1:1 onto useServerTable's setFilter(id, value).
1196
1199
  * FilterBar only ever calls this with a COMPLETE, valid value for the field — an
1197
- * emptied selection, a one-sided date range, an invalid numeric range or a cleared
1198
- * bound go through onFilterRemove (or nothing) instead. */
1200
+ * emptied selection, a one-sided date range, an invalid numeric range, number text
1201
+ * that isn't a valid value, or a cleared bound go through onFilterRemove (or nothing,
1202
+ * when the filter isn't set) instead. Number fields emit on every keystroke that
1203
+ * leaves them valid, so debounce the request this triggers. */
1199
1204
  onFilterChange: (key: string, value: unknown) => void;
1200
1205
  /** Fires when a chip's × is clicked, or when an edit leaves a field with no valid
1201
1206
  * value. Maps 1:1 onto useServerTable's clearFilter(id). */
package/dist/index.d.ts CHANGED
@@ -1190,12 +1190,17 @@ interface FilterBarProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "on
1190
1190
  fields: FilterField[];
1191
1191
  /** Straight off useServerTable — a value of undefined/""/false reads as "not set",
1192
1192
  * same convention that hook already uses for activeFilterCount. multiSelect/range/
1193
- * date values are objects instead (see isFieldSet). */
1193
+ * date values are objects instead (see isFieldSet). Pass a new object whenever the
1194
+ * filters change (as a state update does): FilterBar re-reads this record only when
1195
+ * its identity changes, so a parent that re-renders before applying an emission
1196
+ * doesn't make FilterBar forget it. */
1194
1197
  filters: Record<string, unknown>;
1195
1198
  /** Fires on every value edit. Maps 1:1 onto useServerTable's setFilter(id, value).
1196
1199
  * FilterBar only ever calls this with a COMPLETE, valid value for the field — an
1197
- * emptied selection, a one-sided date range, an invalid numeric range or a cleared
1198
- * bound go through onFilterRemove (or nothing) instead. */
1200
+ * emptied selection, a one-sided date range, an invalid numeric range, number text
1201
+ * that isn't a valid value, or a cleared bound go through onFilterRemove (or nothing,
1202
+ * when the filter isn't set) instead. Number fields emit on every keystroke that
1203
+ * leaves them valid, so debounce the request this triggers. */
1199
1204
  onFilterChange: (key: string, value: unknown) => void;
1200
1205
  /** Fires when a chip's × is clicked, or when an edit leaves a field with no valid
1201
1206
  * value. Maps 1:1 onto useServerTable's clearFilter(id). */
package/dist/index.js CHANGED
@@ -2977,7 +2977,8 @@ function NumberInput({
2977
2977
  )
2978
2978
  ] })
2979
2979
  ] }),
2980
- shownError ? /* @__PURE__ */ jsxs36("span", { id: errorId, className: "fd-field-error", children: [
2980
+ /* @__PURE__ */ jsx42("span", { id: errorId, className: "fd-sr", role: "status", children: shownError ?? "" }),
2981
+ shownError ? /* @__PURE__ */ jsxs36("span", { className: "fd-field-error", "aria-hidden": "true", children: [
2981
2982
  /* @__PURE__ */ jsx42("i", { className: "ph ph-warning-circle", "aria-hidden": "true" }),
2982
2983
  shownError
2983
2984
  ] }) : help ? /* @__PURE__ */ jsx42("span", { className: "fd-field-help", children: help }) : null
@@ -3418,9 +3419,7 @@ function MultiSelectPanel({
3418
3419
  mountedRef.current = false;
3419
3420
  };
3420
3421
  }, []);
3421
- const commit = (nextValues, nextOperator = operator) => {
3422
- onCommit({ operator: nextOperator, values: nextValues });
3423
- };
3422
+ const commit = (nextValues, nextOperator = operator) => onCommit({ operator: nextOperator, values: nextValues });
3424
3423
  const setOperator = (op) => {
3425
3424
  setDraftOperator(op);
3426
3425
  if (selectedRef.current.length > 0) commit(selectedRef.current, op);
@@ -3501,7 +3500,11 @@ function MultiSelectPanel({
3501
3500
  setPasteError("This filter allows at most " + field.maxValues + " values; adding this list would make " + merged.size + ".");
3502
3501
  return;
3503
3502
  }
3504
- commit(Array.from(merged.values()), operatorRef.current);
3503
+ const applied = commit(Array.from(merged.values()), operatorRef.current);
3504
+ if (!applied.ok) {
3505
+ setPasteError(applied.reason);
3506
+ return;
3507
+ }
3505
3508
  setPasteText("");
3506
3509
  }, (err) => {
3507
3510
  if (!mountedRef.current) return;
@@ -3638,6 +3641,34 @@ function MultiSelectPanel({
3638
3641
  }
3639
3642
  );
3640
3643
  }
3644
+ function useTextValidity(onFlip) {
3645
+ const invalidRef = React24.useRef(/* @__PURE__ */ new Set());
3646
+ const [, rerender] = React24.useReducer((n) => n + 1, 0);
3647
+ const onFlipRef = React24.useRef(onFlip);
3648
+ onFlipRef.current = onFlip;
3649
+ const record = (name, valid) => {
3650
+ const invalid = invalidRef.current;
3651
+ if (valid !== invalid.has(name)) return false;
3652
+ if (valid) invalid.delete(name);
3653
+ else invalid.add(name);
3654
+ rerender();
3655
+ return true;
3656
+ };
3657
+ return {
3658
+ allValid: () => invalidRef.current.size === 0,
3659
+ report: (name) => (valid) => {
3660
+ if (record(name, valid)) onFlipRef.current();
3661
+ },
3662
+ /** For an input that is about to unmount: it has nothing left to show. */
3663
+ forget: (name) => {
3664
+ record(name, true);
3665
+ }
3666
+ };
3667
+ }
3668
+ function applicable(field, candidate, textValid) {
3669
+ if (!textValid || candidate === void 0) return void 0;
3670
+ return validateFilterValue(field, candidate).ok ? candidate : void 0;
3671
+ }
3641
3672
  function RangePanel({
3642
3673
  field,
3643
3674
  value,
@@ -3647,19 +3678,22 @@ function RangePanel({
3647
3678
  }) {
3648
3679
  const [draft, setDraft] = React24.useState({ min: value?.min, max: value?.max });
3649
3680
  const draftRef = React24.useRef(draft);
3650
- const [minTextValid, setMinTextValid] = React24.useState(true);
3651
- const [maxTextValid, setMaxTextValid] = React24.useState(true);
3681
+ const publish = () => {
3682
+ const { min, max } = draftRef.current;
3683
+ onCommit(applicable(field, min == null && max == null ? void 0 : { min, max }, validity.allValid()));
3684
+ };
3685
+ const validity = useTextValidity(publish);
3652
3686
  const draftMin = draft.min;
3653
3687
  const draftMax = draft.max;
3654
3688
  const isEmpty = draftMin == null && draftMax == null;
3655
3689
  const validation = isEmpty ? { ok: true } : validateFilterValue(field, { min: draftMin, max: draftMax });
3656
3690
  const comboError = !isEmpty && !validation.ok ? validation.reason : void 0;
3657
- const doneDisabled = !minTextValid || !maxTextValid || !!comboError;
3691
+ const doneDisabled = !validity.allValid() || !!comboError;
3658
3692
  const apply = (patch) => {
3659
3693
  const next = { ...draftRef.current, ...patch };
3660
3694
  draftRef.current = next;
3661
3695
  setDraft(next);
3662
- onCommit(next.min == null && next.max == null ? void 0 : next);
3696
+ publish();
3663
3697
  };
3664
3698
  return /* @__PURE__ */ jsx45(
3665
3699
  Popover,
@@ -3684,7 +3718,7 @@ function RangePanel({
3684
3718
  integer: field.integer,
3685
3719
  placeholder: field.minPlaceholder,
3686
3720
  onChange: (e) => apply({ min: e.target.value }),
3687
- onValidityChange: setMinTextValid
3721
+ onValidityChange: validity.report("min")
3688
3722
  }
3689
3723
  ),
3690
3724
  /* @__PURE__ */ jsx45(
@@ -3699,7 +3733,7 @@ function RangePanel({
3699
3733
  error: comboError,
3700
3734
  placeholder: field.maxPlaceholder,
3701
3735
  onChange: (e) => apply({ max: e.target.value }),
3702
- onValidityChange: setMaxTextValid
3736
+ onValidityChange: validity.report("max")
3703
3737
  }
3704
3738
  )
3705
3739
  ] })
@@ -3715,34 +3749,47 @@ function DatePanel({
3715
3749
  }) {
3716
3750
  const defaultDays = field.defaultDays ?? 30;
3717
3751
  const maxDays = effectiveMaxDays(field);
3718
- const [daysTextValid, setDaysTextValid] = React24.useState(true);
3719
3752
  const [kind, setKind] = React24.useState(value?.kind ?? "withinDays");
3720
3753
  const [daysDraft, setDaysDraft] = React24.useState(
3721
3754
  value?.kind === "withinDays" ? value.days : defaultDays
3722
3755
  );
3756
+ const daysRef = React24.useRef(daysDraft);
3723
3757
  const [between, setBetween] = React24.useState(
3724
3758
  value?.kind === "between" ? { from: value.from, to: value.to } : { from: "", to: "" }
3725
3759
  );
3760
+ const shownWithinDays = () => {
3761
+ const days = daysRef.current;
3762
+ return applicable(field, days == null ? void 0 : { kind: "withinDays", days }, validity.allValid());
3763
+ };
3764
+ const validity = useTextValidity(() => onCommit(shownWithinDays()));
3726
3765
  const applyWithinDays = (days) => {
3766
+ daysRef.current = days;
3727
3767
  setDaysDraft(days);
3728
- onCommit(days == null ? void 0 : { kind: "withinDays", days });
3768
+ onCommit(shownWithinDays());
3729
3769
  };
3730
3770
  const applyBetween = (from, to) => {
3731
3771
  setBetween({ from, to });
3732
- onCommit(from && to ? { kind: "between", from, to } : void 0);
3772
+ onCommit(applicable(field, from && to ? { kind: "between", from, to } : void 0, true));
3733
3773
  };
3734
3774
  const switchKind = (next) => {
3735
3775
  if (next === kind) return;
3736
3776
  setKind(next);
3737
- if (next === "between") setBetween({ from: "", to: "" });
3738
- else setDaysDraft(defaultDays);
3739
- setDaysTextValid(true);
3777
+ if (next === "between") {
3778
+ setBetween({ from: "", to: "" });
3779
+ validity.forget("days");
3780
+ } else {
3781
+ daysRef.current = defaultDays;
3782
+ setDaysDraft(defaultDays);
3783
+ }
3740
3784
  onCommit(void 0);
3741
3785
  };
3742
- const doneDisabled = kind === "withinDays" && !daysTextValid;
3786
+ const doneDisabled = kind === "withinDays" && !validity.allValid();
3743
3787
  const confirm = () => {
3744
3788
  if (doneDisabled) return;
3745
- if (kind === "withinDays" && daysDraft != null) onCommit({ kind: "withinDays", days: daysDraft });
3789
+ if (kind === "withinDays") {
3790
+ const shown = shownWithinDays();
3791
+ if (shown) onCommit(shown);
3792
+ }
3746
3793
  onClose();
3747
3794
  };
3748
3795
  return /* @__PURE__ */ jsx45(
@@ -3779,7 +3826,7 @@ function DatePanel({
3779
3826
  integer: true,
3780
3827
  value: daysDraft ?? "",
3781
3828
  onChange: (e) => applyWithinDays(e.target.value),
3782
- onValidityChange: setDaysTextValid
3829
+ onValidityChange: validity.report("days")
3783
3830
  }
3784
3831
  ) : /* @__PURE__ */ jsx45(
3785
3832
  DatePicker,
@@ -3804,7 +3851,14 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
3804
3851
  for (const f of fields) assertValidFieldDefinition(f);
3805
3852
  }
3806
3853
  const latest = React24.useRef({ fields, filters, onFilterChange, onFilterRemove });
3807
- latest.current = { fields, filters, onFilterChange, onFilterRemove };
3854
+ const filtersProp = React24.useRef(filters);
3855
+ if (filters !== filtersProp.current) {
3856
+ filtersProp.current = filters;
3857
+ latest.current.filters = filters;
3858
+ }
3859
+ latest.current.fields = fields;
3860
+ latest.current.onFilterChange = onFilterChange;
3861
+ latest.current.onFilterRemove = onFilterRemove;
3808
3862
  const commit = (field, candidate) => {
3809
3863
  const current2 = latest.current.fields.find((f) => f.key === field.key);
3810
3864
  if (!current2) return { ok: false, reason: "This filter is no longer available" };