@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.d.cts CHANGED
@@ -692,10 +692,11 @@ interface BannerProps {
692
692
  title: React.ReactNode;
693
693
  detail?: React.ReactNode;
694
694
  actions?: React.ReactNode;
695
- /** MP-243 Q3: a Banner reporting a failure (danger) or something invalid the user must
696
- * fix (warn) is a live-region announcement, not decoration — `role="alert"` for those
697
- * interrupts and announces immediately, `role="status"` for info/ok announces politely
698
- * once idle. Defaults by tone; a caller can still override either way. */
695
+ /** A Banner reporting a failure (danger) is a live-region announcement that interrupts
696
+ * and announces immediately (`role="alert"`); every other tone — warn, info, ok —
697
+ * announces politely once idle (`role="status"`), since a static warning or advisory
698
+ * banner isn't urgent enough to talk over whatever the user is doing. Defaults by
699
+ * tone; a caller can still override either way. */
699
700
  role?: "alert" | "status";
700
701
  }
701
702
  declare function Banner({ tone, title, detail, actions, role }: BannerProps): React.JSX.Element;
@@ -1080,7 +1081,7 @@ interface FilterFieldSelect extends FilterFieldBase {
1080
1081
  type: "select";
1081
1082
  options: FilterFieldOption[];
1082
1083
  }
1083
- /** MP-243: include/exclude, shared by every field whose value is a set of options. */
1084
+ /** Include/exclude, shared by every field whose value is a set of options. */
1084
1085
  type FilterOperator = "in" | "notIn";
1085
1086
  interface FilterMultiSelectValue {
1086
1087
  operator: FilterOperator;
@@ -1102,27 +1103,27 @@ interface FilterFieldMultiSelect extends FilterFieldBase {
1102
1103
  options?: FilterFieldOption[];
1103
1104
  /** Async option source for long lists (schools, imports, …), called ~300ms after the
1104
1105
  * last keystroke with the current query (possibly "") and an AbortSignal that fires
1105
- * once a newer query has superseded this call (MP-243 A6) — pass it straight to
1106
- * `fetch`/whatever transport does the lookup so a superseded request is actually
1107
- * cancelled, not just ignored on arrival. A rejection (that isn't the abort itself)
1108
- * renders as a visible error with a Retry action; it is never folded into "no
1109
- * matches", since a search that FAILED is a different thing to tell the user than a
1110
- * search that found nothing. */
1106
+ * once a newer query has superseded this call — pass it straight to `fetch`/whatever
1107
+ * transport does the lookup so a superseded request is actually cancelled, not just
1108
+ * ignored on arrival. A rejection (that isn't the abort itself) renders as a visible
1109
+ * error with a Retry action; it is never folded into "no matches", since a search
1110
+ * that FAILED is a different thing to tell the user than a search that found
1111
+ * nothing. */
1111
1112
  loadOptions?: (query: string, signal: AbortSignal) => Promise<FilterFieldOption[]>;
1112
- /** MP-243 A1: paste-a-list as a second input mode for THIS SAME field — search and
1113
- * paste both build one FilterMultiSelectValue for one dimension, so a school found by
1114
- * typing and a school found by pasting a list land as two entries in the same set
1115
- * instead of two separate filters that would AND together. Presence of `resolve` is
1116
- * what turns on the Search/Paste mode switch; omit it for a search-only field. Never
1117
- * called with an empty token list, and tokens are de-duplicated before the call. */
1113
+ /** Paste-a-list as a second input mode for THIS SAME field — search and paste both
1114
+ * build one FilterMultiSelectValue for one dimension, so a school found by typing and
1115
+ * a school found by pasting a list land as two entries in the same set instead of two
1116
+ * separate filters that would AND together. Presence of `resolve` is what turns on
1117
+ * the Search/Paste mode switch; omit it for a search-only field. Never called with an
1118
+ * empty token list, and tokens are de-duplicated before the call. */
1118
1119
  resolve?: (tokens: string[]) => Promise<FilterResolveResult>;
1119
1120
  placeholder?: string;
1120
1121
  /** Placeholder for the paste textarea, when `resolve` is given. Falls back to `placeholder`. */
1121
1122
  pastePlaceholder?: string;
1122
- /** MP-243 A2: restrict which operators this field's toggle offers, e.g. `["in"]` for a
1123
- * dimension that only ever means "is any of" and never "is none of" — a single-operator
1124
- * list hides the toggle entirely and that operator applies unconditionally, so an app
1125
- * can express an "any"/"is"-only dimension exactly rather than showing a switch with
1123
+ /** Restrict which operators this field's toggle offers, e.g. `["in"]` for a dimension
1124
+ * that only ever means "is any of" and never "is none of" — a single-operator list
1125
+ * hides the toggle entirely and that operator applies unconditionally, so an app can
1126
+ * express an "any"/"is"-only dimension exactly rather than showing a switch with
1126
1127
  * nothing to switch to. Defaults to both `["in", "notIn"]`. */
1127
1128
  operators?: FilterOperator[];
1128
1129
  }
@@ -1153,16 +1154,20 @@ type FilterDateValue = {
1153
1154
  interface FilterFieldDate extends FilterFieldBase {
1154
1155
  type: "date";
1155
1156
  /** Default days shown the first time "within the last N days" is picked — shown, not
1156
- * written: nothing is set until the user changes it or confirms it (MP-243 A4). */
1157
+ * written: nothing is set until the user changes it or confirms it. */
1157
1158
  defaultDays?: number;
1159
+ /** Upper bound enforced on the "within the last N days" input. Default 3650 (ten
1160
+ * years) — generous enough for any real "recent activity" window while still
1161
+ * guaranteeing a plausible, in-range value ever reaches `onFilterChange`. */
1162
+ maxDays?: number;
1158
1163
  }
1159
1164
  /** A declared filter field. Add a variant here (and a branch below) for a new value
1160
1165
  * picker. text/select keep their original bare-string value shape unchanged, so an
1161
1166
  * existing consumer needs no migration; every other variant's value is a small typed
1162
1167
  * object of its own (see FilterValue). */
1163
1168
  type FilterField = FilterFieldText | FilterFieldSelect | FilterFieldMultiSelect | FilterFieldRange | FilterFieldDate;
1164
- /** MP-243 A7: the value shape a given declared field reads/writes, so a consumer can type
1165
- * its own `filters` record precisely instead of `Record<string, unknown>` — e.g.
1169
+ /** The value shape a given declared field reads/writes, so a consumer can type its own
1170
+ * `filters` record precisely instead of `Record<string, unknown>` — e.g.
1166
1171
  * `Record<"schools", FilterValue<typeof schoolsField>>`. */
1167
1172
  type FilterValue<F extends FilterField> = F extends FilterFieldText ? string : F extends FilterFieldSelect ? string : F extends FilterFieldMultiSelect ? FilterMultiSelectValue : F extends FilterFieldRange ? FilterRangeValue : F extends FilterFieldDate ? FilterDateValue : never;
1168
1173
  interface FilterBarProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "onChange"> {
@@ -1175,7 +1180,7 @@ interface FilterBarProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "on
1175
1180
  /** Fires on every value edit. Maps 1:1 onto useServerTable's setFilter(id, value).
1176
1181
  * FilterBar only ever calls this with a COMPLETE, valid value for the field — an
1177
1182
  * emptied selection, a one-sided date range, an invalid numeric range or a cleared
1178
- * bound go through onFilterRemove (or nothing) instead (MP-243 A3). */
1183
+ * bound go through onFilterRemove (or nothing) instead. */
1179
1184
  onFilterChange: (key: string, value: unknown) => void;
1180
1185
  /** Fires when a chip's × is clicked, or when an edit leaves a field with no valid
1181
1186
  * value. Maps 1:1 onto useServerTable's clearFilter(id). */
@@ -1196,18 +1201,17 @@ interface FilterBarProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "on
1196
1201
  * (`panel`), the same way SortMenu holds `open` but never sort state itself.
1197
1202
  *
1198
1203
  * Every floating surface — a field's edit panel and the "add filter" menu — is a real
1199
- * Popover (or Menu-in-a-Popover) from this kit's own actions module (MP-243 A5): viewport
1200
- * clamping, nested-layer handling, Escape, and focus-return-to-the-chip all come from
1201
- * there, so FilterBar itself runs no document-level dismissal listener of its own. Only
1202
- * the text field's inline input manages its own dismissal (blur/Enter/Escape), since it
1203
- * isn't a floating panel at all.
1204
+ * Popover (or Menu-in-a-Popover) from this kit's own actions module: viewport clamping,
1205
+ * nested-layer handling, Escape, and focus-return-to-the-chip all come from there, so
1206
+ * FilterBar itself runs no document-level dismissal listener of its own. Only the text
1207
+ * field's inline input manages its own dismissal (blur/Enter/Escape), since it isn't a
1208
+ * floating panel at all.
1204
1209
  *
1205
- * MP-243 adds three variants on top of the original text/select, for audiences too big or
1206
- * too shaped for a single free-text box or a short dropdown: multiSelect (searchable, sync
1207
- * or async, with an optional paste-a-list mode for the same value), range (numeric
1208
- * min/max), and date ("within the last N days" or "between two dates"). text/select are
1209
- * unchanged and keep their bare-string value shape, so an existing consumer needs no
1210
- * migration.
1210
+ * FilterBar supports five field kinds: text, select, multiSelect (searchable, sync or
1211
+ * async, with an optional paste-a-list mode for the same value), range (numeric min/max),
1212
+ * and date ("within the last N days" or "between two dates"). text and select keep a bare
1213
+ * string value; multiSelect/range/date each read/write a small typed object of their own
1214
+ * (see FilterValue).
1211
1215
  */
1212
1216
  declare function FilterBar({ fields, filters, onFilterChange, onFilterRemove, align, className, ...rest }: FilterBarProps): React.JSX.Element;
1213
1217
 
package/dist/index.d.ts CHANGED
@@ -692,10 +692,11 @@ interface BannerProps {
692
692
  title: React.ReactNode;
693
693
  detail?: React.ReactNode;
694
694
  actions?: React.ReactNode;
695
- /** MP-243 Q3: a Banner reporting a failure (danger) or something invalid the user must
696
- * fix (warn) is a live-region announcement, not decoration — `role="alert"` for those
697
- * interrupts and announces immediately, `role="status"` for info/ok announces politely
698
- * once idle. Defaults by tone; a caller can still override either way. */
695
+ /** A Banner reporting a failure (danger) is a live-region announcement that interrupts
696
+ * and announces immediately (`role="alert"`); every other tone — warn, info, ok —
697
+ * announces politely once idle (`role="status"`), since a static warning or advisory
698
+ * banner isn't urgent enough to talk over whatever the user is doing. Defaults by
699
+ * tone; a caller can still override either way. */
699
700
  role?: "alert" | "status";
700
701
  }
701
702
  declare function Banner({ tone, title, detail, actions, role }: BannerProps): React.JSX.Element;
@@ -1080,7 +1081,7 @@ interface FilterFieldSelect extends FilterFieldBase {
1080
1081
  type: "select";
1081
1082
  options: FilterFieldOption[];
1082
1083
  }
1083
- /** MP-243: include/exclude, shared by every field whose value is a set of options. */
1084
+ /** Include/exclude, shared by every field whose value is a set of options. */
1084
1085
  type FilterOperator = "in" | "notIn";
1085
1086
  interface FilterMultiSelectValue {
1086
1087
  operator: FilterOperator;
@@ -1102,27 +1103,27 @@ interface FilterFieldMultiSelect extends FilterFieldBase {
1102
1103
  options?: FilterFieldOption[];
1103
1104
  /** Async option source for long lists (schools, imports, …), called ~300ms after the
1104
1105
  * last keystroke with the current query (possibly "") and an AbortSignal that fires
1105
- * once a newer query has superseded this call (MP-243 A6) — pass it straight to
1106
- * `fetch`/whatever transport does the lookup so a superseded request is actually
1107
- * cancelled, not just ignored on arrival. A rejection (that isn't the abort itself)
1108
- * renders as a visible error with a Retry action; it is never folded into "no
1109
- * matches", since a search that FAILED is a different thing to tell the user than a
1110
- * search that found nothing. */
1106
+ * once a newer query has superseded this call — pass it straight to `fetch`/whatever
1107
+ * transport does the lookup so a superseded request is actually cancelled, not just
1108
+ * ignored on arrival. A rejection (that isn't the abort itself) renders as a visible
1109
+ * error with a Retry action; it is never folded into "no matches", since a search
1110
+ * that FAILED is a different thing to tell the user than a search that found
1111
+ * nothing. */
1111
1112
  loadOptions?: (query: string, signal: AbortSignal) => Promise<FilterFieldOption[]>;
1112
- /** MP-243 A1: paste-a-list as a second input mode for THIS SAME field — search and
1113
- * paste both build one FilterMultiSelectValue for one dimension, so a school found by
1114
- * typing and a school found by pasting a list land as two entries in the same set
1115
- * instead of two separate filters that would AND together. Presence of `resolve` is
1116
- * what turns on the Search/Paste mode switch; omit it for a search-only field. Never
1117
- * called with an empty token list, and tokens are de-duplicated before the call. */
1113
+ /** Paste-a-list as a second input mode for THIS SAME field — search and paste both
1114
+ * build one FilterMultiSelectValue for one dimension, so a school found by typing and
1115
+ * a school found by pasting a list land as two entries in the same set instead of two
1116
+ * separate filters that would AND together. Presence of `resolve` is what turns on
1117
+ * the Search/Paste mode switch; omit it for a search-only field. Never called with an
1118
+ * empty token list, and tokens are de-duplicated before the call. */
1118
1119
  resolve?: (tokens: string[]) => Promise<FilterResolveResult>;
1119
1120
  placeholder?: string;
1120
1121
  /** Placeholder for the paste textarea, when `resolve` is given. Falls back to `placeholder`. */
1121
1122
  pastePlaceholder?: string;
1122
- /** MP-243 A2: restrict which operators this field's toggle offers, e.g. `["in"]` for a
1123
- * dimension that only ever means "is any of" and never "is none of" — a single-operator
1124
- * list hides the toggle entirely and that operator applies unconditionally, so an app
1125
- * can express an "any"/"is"-only dimension exactly rather than showing a switch with
1123
+ /** Restrict which operators this field's toggle offers, e.g. `["in"]` for a dimension
1124
+ * that only ever means "is any of" and never "is none of" — a single-operator list
1125
+ * hides the toggle entirely and that operator applies unconditionally, so an app can
1126
+ * express an "any"/"is"-only dimension exactly rather than showing a switch with
1126
1127
  * nothing to switch to. Defaults to both `["in", "notIn"]`. */
1127
1128
  operators?: FilterOperator[];
1128
1129
  }
@@ -1153,16 +1154,20 @@ type FilterDateValue = {
1153
1154
  interface FilterFieldDate extends FilterFieldBase {
1154
1155
  type: "date";
1155
1156
  /** Default days shown the first time "within the last N days" is picked — shown, not
1156
- * written: nothing is set until the user changes it or confirms it (MP-243 A4). */
1157
+ * written: nothing is set until the user changes it or confirms it. */
1157
1158
  defaultDays?: number;
1159
+ /** Upper bound enforced on the "within the last N days" input. Default 3650 (ten
1160
+ * years) — generous enough for any real "recent activity" window while still
1161
+ * guaranteeing a plausible, in-range value ever reaches `onFilterChange`. */
1162
+ maxDays?: number;
1158
1163
  }
1159
1164
  /** A declared filter field. Add a variant here (and a branch below) for a new value
1160
1165
  * picker. text/select keep their original bare-string value shape unchanged, so an
1161
1166
  * existing consumer needs no migration; every other variant's value is a small typed
1162
1167
  * object of its own (see FilterValue). */
1163
1168
  type FilterField = FilterFieldText | FilterFieldSelect | FilterFieldMultiSelect | FilterFieldRange | FilterFieldDate;
1164
- /** MP-243 A7: the value shape a given declared field reads/writes, so a consumer can type
1165
- * its own `filters` record precisely instead of `Record<string, unknown>` — e.g.
1169
+ /** The value shape a given declared field reads/writes, so a consumer can type its own
1170
+ * `filters` record precisely instead of `Record<string, unknown>` — e.g.
1166
1171
  * `Record<"schools", FilterValue<typeof schoolsField>>`. */
1167
1172
  type FilterValue<F extends FilterField> = F extends FilterFieldText ? string : F extends FilterFieldSelect ? string : F extends FilterFieldMultiSelect ? FilterMultiSelectValue : F extends FilterFieldRange ? FilterRangeValue : F extends FilterFieldDate ? FilterDateValue : never;
1168
1173
  interface FilterBarProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "onChange"> {
@@ -1175,7 +1180,7 @@ interface FilterBarProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "on
1175
1180
  /** Fires on every value edit. Maps 1:1 onto useServerTable's setFilter(id, value).
1176
1181
  * FilterBar only ever calls this with a COMPLETE, valid value for the field — an
1177
1182
  * emptied selection, a one-sided date range, an invalid numeric range or a cleared
1178
- * bound go through onFilterRemove (or nothing) instead (MP-243 A3). */
1183
+ * bound go through onFilterRemove (or nothing) instead. */
1179
1184
  onFilterChange: (key: string, value: unknown) => void;
1180
1185
  /** Fires when a chip's × is clicked, or when an edit leaves a field with no valid
1181
1186
  * value. Maps 1:1 onto useServerTable's clearFilter(id). */
@@ -1196,18 +1201,17 @@ interface FilterBarProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "on
1196
1201
  * (`panel`), the same way SortMenu holds `open` but never sort state itself.
1197
1202
  *
1198
1203
  * Every floating surface — a field's edit panel and the "add filter" menu — is a real
1199
- * Popover (or Menu-in-a-Popover) from this kit's own actions module (MP-243 A5): viewport
1200
- * clamping, nested-layer handling, Escape, and focus-return-to-the-chip all come from
1201
- * there, so FilterBar itself runs no document-level dismissal listener of its own. Only
1202
- * the text field's inline input manages its own dismissal (blur/Enter/Escape), since it
1203
- * isn't a floating panel at all.
1204
+ * Popover (or Menu-in-a-Popover) from this kit's own actions module: viewport clamping,
1205
+ * nested-layer handling, Escape, and focus-return-to-the-chip all come from there, so
1206
+ * FilterBar itself runs no document-level dismissal listener of its own. Only the text
1207
+ * field's inline input manages its own dismissal (blur/Enter/Escape), since it isn't a
1208
+ * floating panel at all.
1204
1209
  *
1205
- * MP-243 adds three variants on top of the original text/select, for audiences too big or
1206
- * too shaped for a single free-text box or a short dropdown: multiSelect (searchable, sync
1207
- * or async, with an optional paste-a-list mode for the same value), range (numeric
1208
- * min/max), and date ("within the last N days" or "between two dates"). text/select are
1209
- * unchanged and keep their bare-string value shape, so an existing consumer needs no
1210
- * migration.
1210
+ * FilterBar supports five field kinds: text, select, multiSelect (searchable, sync or
1211
+ * async, with an optional paste-a-list mode for the same value), range (numeric min/max),
1212
+ * and date ("within the last N days" or "between two dates"). text and select keep a bare
1213
+ * string value; multiSelect/range/date each read/write a small typed object of their own
1214
+ * (see FilterValue).
1211
1215
  */
1212
1216
  declare function FilterBar({ fields, filters, onFilterChange, onFilterRemove, align, className, ...rest }: FilterBarProps): React.JSX.Element;
1213
1217
 
package/dist/index.js CHANGED
@@ -1517,7 +1517,7 @@ var BANNER_TONE_STYLE = {
1517
1517
  };
1518
1518
  function Banner({ tone = "info", title, detail, actions, role }) {
1519
1519
  const t = BANNER_TONE_STYLE[tone];
1520
- const liveRole = role || (tone === "danger" || tone === "warn" ? "alert" : "status");
1520
+ const liveRole = role || (tone === "danger" ? "alert" : "status");
1521
1521
  return /* @__PURE__ */ jsxs19(
1522
1522
  "div",
1523
1523
  {
@@ -2786,17 +2786,29 @@ function NumberInput({
2786
2786
  const n = Number(s);
2787
2787
  return isNaN(n) ? null : n;
2788
2788
  };
2789
+ const requiresInteger = Number.isInteger(step);
2790
+ const validationMessage = (n) => {
2791
+ if (requiresInteger && !Number.isInteger(n)) return "Enter a whole number";
2792
+ if (n < min) return "Must be \u2265 " + min;
2793
+ if (n > max) return "Must be \u2264 " + max;
2794
+ return null;
2795
+ };
2789
2796
  const [text, setText] = React21.useState(value == null || value === "" ? "" : String(value));
2790
2797
  const [editing, setEditing] = React21.useState(false);
2798
+ const [invalidMessage, setInvalidMessage] = React21.useState(null);
2791
2799
  const timer = React21.useRef(null);
2792
2800
  React21.useEffect(() => {
2793
- if (!editing) setText(value == null || value === "" ? "" : String(value));
2801
+ if (!editing) {
2802
+ setText(value == null || value === "" ? "" : String(value));
2803
+ setInvalidMessage(null);
2804
+ }
2794
2805
  }, [value, editing]);
2795
2806
  const clamp = (n) => Math.min(max, Math.max(min, n));
2796
2807
  const fire = (n) => onChange && onChange({ target: { value: n } });
2797
2808
  const bump = (dir, mult = 1) => {
2798
2809
  const cur = num(text) ?? num(value) ?? (min === -Infinity ? 0 : min);
2799
2810
  const next = clamp(cur + dir * (bigStep && mult > 1 ? bigStep : step) * (bigStep ? 1 : mult));
2811
+ setInvalidMessage(null);
2800
2812
  setText(String(next));
2801
2813
  fire(next);
2802
2814
  };
@@ -2820,7 +2832,8 @@ function NumberInput({
2820
2832
  const n = num(text);
2821
2833
  return n == null ? "" : format ? n.toLocaleString() : String(n);
2822
2834
  })();
2823
- const box = ["fd-input", "fd-input-num", error ? "fd-input-error" : "", disabled ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
2835
+ const shownError = error || invalidMessage || void 0;
2836
+ const box = ["fd-input", "fd-input-num", shownError ? "fd-input-error" : "", disabled ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
2824
2837
  const fieldName = label || ariaLabel;
2825
2838
  const stepperName = (verb) => fieldName ? verb + " " + fieldName : verb;
2826
2839
  return /* @__PURE__ */ jsxs36("div", { className: ["fd-field", className].filter(Boolean).join(" "), style, children: [
@@ -2836,30 +2849,29 @@ function NumberInput({
2836
2849
  disabled,
2837
2850
  placeholder,
2838
2851
  value: shown,
2839
- "aria-invalid": error ? "true" : void 0,
2840
- "aria-describedby": error ? errorId : void 0,
2852
+ "aria-invalid": shownError ? "true" : void 0,
2853
+ "aria-describedby": shownError ? errorId : void 0,
2841
2854
  onFocus: () => setEditing(true),
2842
2855
  onBlur: () => {
2843
2856
  setEditing(false);
2844
- if (text.trim() === "") {
2845
- fire(void 0);
2846
- return;
2847
- }
2848
- const n = num(text);
2849
- if (n != null) {
2850
- const c = clamp(n);
2851
- setText(String(c));
2852
- fire(c);
2853
- }
2857
+ if (text.trim() === "") fire(void 0);
2854
2858
  },
2855
2859
  onChange: (e) => {
2856
- setText(e.target.value);
2857
- if (e.target.value.trim() === "") {
2860
+ const raw = e.target.value;
2861
+ setText(raw);
2862
+ if (raw.trim() === "") {
2863
+ setInvalidMessage(null);
2858
2864
  fire(void 0);
2859
2865
  return;
2860
2866
  }
2861
- const n = num(e.target.value);
2862
- if (n != null) fire(n);
2867
+ const n = num(raw);
2868
+ if (n == null) {
2869
+ setInvalidMessage("Enter a number");
2870
+ return;
2871
+ }
2872
+ const message = validationMessage(n);
2873
+ setInvalidMessage(message);
2874
+ if (!message) fire(n);
2863
2875
  },
2864
2876
  onKeyDown: (e) => {
2865
2877
  if (e.key === "ArrowUp") {
@@ -2902,9 +2914,9 @@ function NumberInput({
2902
2914
  )
2903
2915
  ] })
2904
2916
  ] }),
2905
- error ? /* @__PURE__ */ jsxs36("span", { id: errorId, className: "fd-field-error", children: [
2917
+ shownError ? /* @__PURE__ */ jsxs36("span", { id: errorId, className: "fd-field-error", children: [
2906
2918
  /* @__PURE__ */ jsx42("i", { className: "ph ph-warning-circle", "aria-hidden": "true" }),
2907
- error
2919
+ shownError
2908
2920
  ] }) : help ? /* @__PURE__ */ jsx42("span", { className: "fd-field-help", children: help }) : null
2909
2921
  ] });
2910
2922
  }
@@ -3239,7 +3251,8 @@ function MultiSelectPanel({
3239
3251
  onClose
3240
3252
  }) {
3241
3253
  const allowedOps = operatorsFor(field);
3242
- const operator = allowedOps.length === 1 ? allowedOps[0] : value?.operator ?? allowedOps[0];
3254
+ const [draftOperator, setDraftOperator] = React24.useState(value?.operator ?? allowedOps[0]);
3255
+ const operator = allowedOps.length === 1 ? allowedOps[0] : value?.operator ?? draftOperator;
3243
3256
  const selected = value?.values ?? [];
3244
3257
  const selectedRef = React24.useRef(selected);
3245
3258
  selectedRef.current = selected;
@@ -3247,6 +3260,10 @@ function MultiSelectPanel({
3247
3260
  if (nextValues.length === 0) onRemove();
3248
3261
  else onChange({ operator: nextOperator, values: nextValues });
3249
3262
  };
3263
+ const setOperator = (op) => {
3264
+ setDraftOperator(op);
3265
+ if (selected.length > 0) commit(selected, op);
3266
+ };
3250
3267
  const [mode2, setMode] = React24.useState("search");
3251
3268
  const [query, setQuery] = React24.useState("");
3252
3269
  const [asyncState, setAsyncState] = React24.useState(
@@ -3283,18 +3300,20 @@ function MultiSelectPanel({
3283
3300
  const options = field.loadOptions ? asyncState.options ?? [] : (field.options ?? []).filter((o) => o.label.toLowerCase().includes(query.toLowerCase()));
3284
3301
  const showLoading = !!field.loadOptions && asyncState.loading;
3285
3302
  const showError = !!field.loadOptions && asyncState.error;
3286
- const showListbox = !showLoading && !showError;
3303
+ const hasListbox = !showLoading && !showError && options.length > 0;
3287
3304
  const isSelected = (o) => selected.some((s) => s.value === o.value);
3288
3305
  const toggle = (o) => {
3289
3306
  const next = isSelected(o) ? selected.filter((s) => s.value !== o.value) : [...selected, o];
3290
3307
  commit(next);
3291
3308
  };
3292
3309
  const nav = useListboxNav({ count: options.length, onSelect: (i) => toggle(options[i]) });
3293
- useListboxScrollIntoView(nav.active, listRef, showListbox);
3310
+ useListboxScrollIntoView(nav.active, listRef, hasListbox);
3294
3311
  React24.useEffect(() => {
3295
3312
  nav.setActive(0);
3296
3313
  }, [query, options.length]);
3297
- const activeId = showListbox && options[nav.active] ? listId + "-opt-" + nav.active : void 0;
3314
+ const activeId = hasListbox && options[nav.active] ? listId + "-opt-" + nav.active : void 0;
3315
+ const liveRole = showError ? "alert" : "status";
3316
+ const liveMessage = showLoading ? "Loading\u2026" : showError ? "Couldn't load options." : options.length === 0 ? "No matches" : "";
3298
3317
  const [pasteText, setPasteText] = React24.useState("");
3299
3318
  const [pasteLoading, setPasteLoading] = React24.useState(false);
3300
3319
  const [unmatchedTokens, setUnmatchedTokens] = React24.useState([]);
@@ -3322,7 +3341,7 @@ function MultiSelectPanel({
3322
3341
  });
3323
3342
  };
3324
3343
  const header = /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3325
- allowedOps.length > 1 ? /* @__PURE__ */ jsx45(OperatorToggle, { operator, onChange: (op) => commit(selected, op), label: field.label }) : null,
3344
+ allowedOps.length > 1 ? /* @__PURE__ */ jsx45(OperatorToggle, { operator, onChange: setOperator, label: field.label }) : null,
3326
3345
  field.resolve ? /* @__PURE__ */ jsx45(
3327
3346
  SegmentedControl,
3328
3347
  {
@@ -3339,8 +3358,8 @@ function MultiSelectPanel({
3339
3358
  {
3340
3359
  ref: inputRef,
3341
3360
  role: "combobox",
3342
- "aria-expanded": showListbox,
3343
- "aria-controls": showListbox ? listId : void 0,
3361
+ "aria-expanded": hasListbox,
3362
+ "aria-controls": hasListbox ? listId : void 0,
3344
3363
  "aria-autocomplete": "list",
3345
3364
  "aria-activedescendant": activeId,
3346
3365
  "aria-label": field.label,
@@ -3396,33 +3415,41 @@ function MultiSelectPanel({
3396
3415
  ] }),
3397
3416
  /* @__PURE__ */ jsx45("span", { className: "fd-row fd-wrap fd-gap-xs", children: selected.map((v) => /* @__PURE__ */ jsx45(Tag, { onRemove: () => commit(selected.filter((s) => s.value !== v.value)), children: v.label }, v.value)) })
3398
3417
  ] }) : null
3399
- ] }) : showLoading ? /* @__PURE__ */ jsxs39("span", { className: "fd-row fd-gap-xs", role: "status", children: [
3400
- /* @__PURE__ */ jsx45("span", { className: "fd-spinner", "aria-hidden": "true" }),
3401
- " Loading\u2026"
3402
- ] }) : showError ? /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3403
- /* @__PURE__ */ jsx45("span", { className: "fd-body-sm", role: "alert", children: "Couldn't load options." }),
3404
- /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: () => setRetryTick((n) => n + 1), children: "Retry" })
3405
- ] }) : options.length === 0 ? /* @__PURE__ */ jsx45("span", { className: "fd-body-sm fd-muted", role: "status", children: "No matches" }) : /* @__PURE__ */ jsx45("span", { id: listId, role: "listbox", "aria-label": field.label, "aria-multiselectable": "true", ref: listRef, className: "fd-pop-list", children: options.map((o, i) => {
3406
- const on = isSelected(o);
3407
- return /* @__PURE__ */ jsxs39(
3408
- "div",
3418
+ ] }) : /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3419
+ /* @__PURE__ */ jsxs39(
3420
+ "span",
3409
3421
  {
3410
- id: listId + "-opt-" + i,
3411
- "data-i": i,
3412
- role: "option",
3413
- tabIndex: -1,
3414
- "aria-selected": on,
3415
- className: ["fd-opt", i === nav.active ? "is-active" : "", on ? "is-selected" : ""].filter(Boolean).join(" "),
3416
- onMouseEnter: () => nav.setActive(i),
3417
- onClick: () => toggle(o),
3422
+ className: "fd-row fd-gap-xs" + (liveMessage && !showError ? " fd-muted" : ""),
3423
+ role: liveRole,
3418
3424
  children: [
3419
- /* @__PURE__ */ jsx45("i", { className: "fd-filterbar-opt-check ph " + (on ? "ph-check-square is-checked" : "ph-square"), "aria-hidden": "true" }),
3420
- /* @__PURE__ */ jsx45("span", { className: "fd-opt-label", children: o.label })
3425
+ showLoading ? /* @__PURE__ */ jsx45("span", { className: "fd-spinner", "aria-hidden": "true" }) : null,
3426
+ liveMessage ? /* @__PURE__ */ jsx45("span", { className: "fd-body-sm", children: liveMessage }) : null
3421
3427
  ]
3422
- },
3423
- o.value
3424
- );
3425
- }) });
3428
+ }
3429
+ ),
3430
+ showError ? /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: () => setRetryTick((n) => n + 1), children: "Retry" }) : null,
3431
+ hasListbox ? /* @__PURE__ */ jsx45("span", { id: listId, role: "listbox", "aria-label": field.label, "aria-multiselectable": "true", ref: listRef, className: "fd-pop-list", children: options.map((o, i) => {
3432
+ const on = isSelected(o);
3433
+ return /* @__PURE__ */ jsxs39(
3434
+ "div",
3435
+ {
3436
+ id: listId + "-opt-" + i,
3437
+ "data-i": i,
3438
+ role: "option",
3439
+ tabIndex: -1,
3440
+ "aria-selected": on,
3441
+ className: ["fd-opt", i === nav.active ? "is-active" : "", on ? "is-selected" : ""].filter(Boolean).join(" "),
3442
+ onMouseEnter: () => nav.setActive(i),
3443
+ onClick: () => toggle(o),
3444
+ children: [
3445
+ /* @__PURE__ */ jsx45("i", { className: "fd-filterbar-opt-check ph " + (on ? "ph-check-square is-checked" : "ph-square"), "aria-hidden": "true" }),
3446
+ /* @__PURE__ */ jsx45("span", { className: "fd-opt-label", children: o.label })
3447
+ ]
3448
+ },
3449
+ o.value
3450
+ );
3451
+ }) }) : null
3452
+ ] });
3426
3453
  const footer = mode2 === "paste" ? /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: doResolve, disabled: pasteLoading || !pasteText.trim(), children: pasteLoading ? "Resolving\u2026" : "Resolve" }) : void 0;
3427
3454
  return /* @__PURE__ */ jsx45(
3428
3455
  Popover,
@@ -3471,7 +3498,7 @@ function RangePanel({
3471
3498
  offset: 6,
3472
3499
  width: PANEL_WIDTH.range,
3473
3500
  label: field.label,
3474
- footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: onClose, children: "Done" }),
3501
+ footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: onClose, disabled: invalid, children: "Done" }),
3475
3502
  children: /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3476
3503
  /* @__PURE__ */ jsx45(
3477
3504
  NumberInput,
@@ -3511,33 +3538,39 @@ function DatePanel({
3511
3538
  onClose
3512
3539
  }) {
3513
3540
  const defaultDays = field.defaultDays ?? 30;
3514
- const [draft, setDraft] = React24.useState(value ?? { kind: "withinDays", days: defaultDays });
3541
+ const maxDays = field.maxDays ?? 3650;
3542
+ const [kind, setKind] = React24.useState(value?.kind ?? "withinDays");
3543
+ const [daysDraft, setDaysDraft] = React24.useState(
3544
+ value?.kind === "withinDays" ? value.days : defaultDays
3545
+ );
3546
+ const [between, setBetween] = React24.useState(
3547
+ value?.kind === "between" ? { from: value.from, to: value.to } : { from: "", to: "" }
3548
+ );
3515
3549
  const committedRef = React24.useRef(!!value);
3516
3550
  const applyWithinDays = (days) => {
3517
- const next = { kind: "withinDays", days: days ?? defaultDays };
3518
- setDraft(next);
3551
+ setDaysDraft(days);
3519
3552
  if (days != null) {
3520
3553
  committedRef.current = true;
3521
- onChange(next);
3554
+ onChange({ kind: "withinDays", days });
3555
+ } else if (committedRef.current) {
3556
+ committedRef.current = false;
3557
+ onRemove();
3522
3558
  }
3523
3559
  };
3524
3560
  const applyBetween = (from, to) => {
3525
- const next = { kind: "between", from, to };
3526
- setDraft(next);
3561
+ setBetween({ from, to });
3527
3562
  if (from && to) {
3528
3563
  committedRef.current = true;
3529
- onChange(next);
3564
+ onChange({ kind: "between", from, to });
3530
3565
  } else if (committedRef.current) {
3531
3566
  committedRef.current = false;
3532
3567
  onRemove();
3533
3568
  }
3534
3569
  };
3535
- const switchKind = (kind) => {
3536
- if (kind === "withinDays") {
3537
- const days = draft.kind === "withinDays" ? draft.days : defaultDays;
3538
- setDraft({ kind: "withinDays", days });
3539
- } else {
3540
- setDraft({ kind: "between", from: "", to: "" });
3570
+ const switchKind = (next) => {
3571
+ setKind(next);
3572
+ if (next === "between") {
3573
+ setBetween({ from: "", to: "" });
3541
3574
  if (committedRef.current) {
3542
3575
  committedRef.current = false;
3543
3576
  onRemove();
@@ -3545,9 +3578,9 @@ function DatePanel({
3545
3578
  }
3546
3579
  };
3547
3580
  const confirm = () => {
3548
- if (draft.kind === "withinDays") {
3581
+ if (kind === "withinDays" && daysDraft != null) {
3549
3582
  committedRef.current = true;
3550
- onChange(draft);
3583
+ onChange({ kind: "withinDays", days: daysDraft });
3551
3584
  }
3552
3585
  onClose();
3553
3586
  };
@@ -3571,16 +3604,16 @@ function DatePanel({
3571
3604
  { value: "withinDays", label: "Within last N days" },
3572
3605
  { value: "between", label: "Between dates" }
3573
3606
  ],
3574
- value: draft.kind,
3607
+ value: kind,
3575
3608
  onChange: (k) => switchKind(k)
3576
3609
  }
3577
3610
  ),
3578
- draft.kind === "withinDays" ? /* @__PURE__ */ jsx45(NumberInput, { label: "Days", min: 1, value: draft.days, onChange: (e) => applyWithinDays(e.target.value) }) : /* @__PURE__ */ jsx45(
3611
+ kind === "withinDays" ? /* @__PURE__ */ jsx45(NumberInput, { label: "Days", min: 1, max: maxDays, step: 1, value: daysDraft ?? "", onChange: (e) => applyWithinDays(e.target.value) }) : /* @__PURE__ */ jsx45(
3579
3612
  DatePicker,
3580
3613
  {
3581
3614
  range: true,
3582
3615
  label: "Date range",
3583
- value: { start: draft.from, end: draft.to },
3616
+ value: { start: between.from, end: between.to },
3584
3617
  onChange: (e) => applyBetween(e.target.value.start || "", e.target.value.end || "")
3585
3618
  }
3586
3619
  )