@flytedan/flytebot-design-system 0.31.0 → 0.32.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.js CHANGED
@@ -2757,6 +2757,48 @@ function useTriggerLabelling(label, id) {
2757
2757
 
2758
2758
  // src/components/forms/NumberInput.tsx
2759
2759
  import { jsx as jsx42, jsxs as jsxs36 } from "react/jsx-runtime";
2760
+ var HOLD_DELAY_MS = 400;
2761
+ var HOLD_BIG_AFTER = 6;
2762
+ var HOLD_FAST_AFTER = 12;
2763
+ var holdInterval = (repeats) => repeats > HOLD_FAST_AFTER ? 40 : repeats > HOLD_BIG_AFTER ? 70 : 120;
2764
+ function decimalPlaces(n) {
2765
+ if (!Number.isFinite(n)) return 0;
2766
+ const [mantissa, exp] = String(n).toLowerCase().split("e");
2767
+ const frac = (mantissa.split(".")[1] || "").length;
2768
+ return Math.max(0, frac - (exp ? Number(exp) : 0));
2769
+ }
2770
+ function parseNumberText(text) {
2771
+ const s = text.replace(/,/g, "").trim();
2772
+ if (s === "") return null;
2773
+ if (!/^-?(\d+\.?\d*|\.\d+)$/.test(s)) return NaN;
2774
+ return Number(s);
2775
+ }
2776
+ function allowedRange(min, max, integer) {
2777
+ return integer ? { lo: Math.ceil(min), hi: Math.floor(max) } : { lo: min, hi: max };
2778
+ }
2779
+ function stepNumber({ from, direction, amount, step, min, max, integer }) {
2780
+ const { lo, hi } = allowedRange(min, max, integer);
2781
+ if (lo > hi) return null;
2782
+ const base = from ?? 0;
2783
+ const anchor = Number.isFinite(lo) ? lo : 0;
2784
+ const unit = step > 0 ? step : 1;
2785
+ const travel = amount > 0 ? amount : unit;
2786
+ const magnitude = Math.max(Math.abs(base), Math.abs(anchor), travel, unit, 1);
2787
+ const maxPlaces = Math.max(0, Math.floor(Math.log10(Number.MAX_SAFE_INTEGER / (4 * magnitude))));
2788
+ const places = Math.min(maxPlaces, Math.max(decimalPlaces(base), decimalPlaces(anchor), decimalPlaces(travel), decimalPlaces(unit)));
2789
+ const scale = 10 ** places;
2790
+ const b = Math.round(base * scale);
2791
+ const a = Math.round(anchor * scale);
2792
+ const s = Math.round(unit * scale);
2793
+ const t = Math.round(travel * scale);
2794
+ const target = b + direction * t;
2795
+ let k = direction > 0 ? Math.floor((target - a) / s) : Math.ceil((target - a) / s);
2796
+ if (direction > 0 && a + k * s <= b) k = Math.floor((b - a) / s) + 1;
2797
+ if (direction < 0 && a + k * s >= b) k = Math.ceil((b - a) / s) - 1;
2798
+ let next = (a + k * s) / scale;
2799
+ if (integer && !Number.isInteger(next)) next = direction > 0 ? Math.ceil(next) : Math.floor(next);
2800
+ return Math.min(hi, Math.max(lo, next));
2801
+ }
2760
2802
  function NumberInput({
2761
2803
  label,
2762
2804
  help,
@@ -2782,70 +2824,78 @@ function NumberInput({
2782
2824
  }) {
2783
2825
  const fieldId = useFieldId(id);
2784
2826
  const errorId = fieldId + "-error";
2785
- const num = (v) => {
2786
- const s = String(v == null ? "" : v).replace(/[^0-9.-]/g, "");
2787
- if (s === "" || s === "-") return null;
2788
- const n = Number(s);
2789
- return isNaN(n) ? null : n;
2790
- };
2791
- const validationMessage = (n) => {
2792
- if (integer && !Number.isInteger(n)) return "Enter a whole number";
2793
- if (n < min) return "Must be \u2265 " + min;
2794
- if (n > max) return "Must be \u2264 " + max;
2795
- return null;
2827
+ const { lo, hi } = allowedRange(min, max, integer);
2828
+ const big = bigStep ?? Number((step * 10).toFixed(decimalPlaces(step)));
2829
+ const toText = (v) => v == null || v === "" ? "" : String(v);
2830
+ const toNumber = (v) => {
2831
+ if (v == null || v === "") return null;
2832
+ const n = typeof v === "number" ? v : parseNumberText(v);
2833
+ return n == null || Number.isNaN(n) ? null : n;
2796
2834
  };
2797
- const [text, setText] = React21.useState(value == null || value === "" ? "" : String(value));
2835
+ const [text, setText] = React21.useState(() => toText(value));
2798
2836
  const [editing, setEditing] = React21.useState(false);
2799
- const [invalidMessage, setInvalidMessage] = React21.useState(null);
2800
- const timer = React21.useRef(null);
2837
+ const parsed = parseNumberText(text);
2838
+ const invalidMessage = parsed == null ? null : Number.isNaN(parsed) ? "Enter a number" : integer && !Number.isInteger(parsed) ? "Enter a whole number" : parsed < lo ? "Must be \u2265 " + lo : parsed > hi ? "Must be \u2264 " + hi : null;
2839
+ const valid = invalidMessage == null;
2840
+ const committedRef = React21.useRef(toNumber(value));
2841
+ const onChangeRef = React21.useRef(onChange);
2842
+ onChangeRef.current = onChange;
2843
+ const onValidityChangeRef = React21.useRef(onValidityChange);
2844
+ onValidityChangeRef.current = onValidityChange;
2845
+ const focusedRef = React21.useRef(false);
2801
2846
  React21.useEffect(() => {
2802
- if (!editing) {
2803
- setText(value == null || value === "" ? "" : String(value));
2804
- setInvalidMessage(null);
2805
- }
2806
- }, [value, editing]);
2807
- React21.useEffect(() => {
2808
- onValidityChange && onValidityChange(invalidMessage == null);
2809
- }, [invalidMessage]);
2810
- const clamp = (n) => Math.min(max, Math.max(min, n));
2811
- const fire = (n) => onChange && onChange({ target: { value: n } });
2812
- const bump = (dir, mult = 1) => {
2813
- const cur = (invalidMessage ? null : num(text)) ?? num(value) ?? (min === -Infinity ? 0 : min);
2814
- const amount = (bigStep && mult > 1 ? bigStep : step) * (bigStep ? 1 : mult);
2815
- let next = cur + dir * amount;
2816
- if (integer) next = Math.round(next);
2817
- next = clamp(next);
2818
- setInvalidMessage(null);
2847
+ committedRef.current = toNumber(value);
2848
+ if (!focusedRef.current) setText(toText(value));
2849
+ }, [value]);
2850
+ React21.useLayoutEffect(() => {
2851
+ onValidityChangeRef.current?.(valid);
2852
+ }, [valid]);
2853
+ const fire = (n) => {
2854
+ committedRef.current = n ?? null;
2855
+ onChangeRef.current?.({ target: { value: n } });
2856
+ };
2857
+ const bump = (direction, amount) => {
2858
+ const next = stepNumber({ from: committedRef.current, direction, amount, step, min, max, integer });
2859
+ if (next == null) return false;
2819
2860
  setText(String(next));
2861
+ if (next === committedRef.current) return false;
2820
2862
  fire(next);
2863
+ return true;
2821
2864
  };
2822
- const hold = (dir) => {
2823
- bump(dir);
2824
- let n = 0;
2865
+ const bumpRef = React21.useRef(bump);
2866
+ bumpRef.current = bump;
2867
+ const holdTimer = React21.useRef(null);
2868
+ const release = () => {
2869
+ if (holdTimer.current) clearTimeout(holdTimer.current);
2870
+ holdTimer.current = null;
2871
+ };
2872
+ const hold = (direction) => {
2873
+ release();
2874
+ bump(direction, step);
2875
+ let repeats = 0;
2825
2876
  const tick = () => {
2826
- n++;
2827
- bump(dir, n > 6 ? 4 : 1);
2828
- timer.current = setTimeout(tick, n > 12 ? 40 : n > 6 ? 70 : 120);
2877
+ repeats++;
2878
+ if (!bumpRef.current(direction, repeats > HOLD_BIG_AFTER ? big : step)) {
2879
+ release();
2880
+ return;
2881
+ }
2882
+ holdTimer.current = setTimeout(tick, holdInterval(repeats));
2829
2883
  };
2830
- timer.current = setTimeout(tick, 420);
2831
- };
2832
- const release = () => {
2833
- if (timer.current) clearTimeout(timer.current);
2884
+ holdTimer.current = setTimeout(tick, HOLD_DELAY_MS);
2834
2885
  };
2835
2886
  React21.useEffect(() => () => {
2836
- if (timer.current) clearTimeout(timer.current);
2887
+ if (holdTimer.current) clearTimeout(holdTimer.current);
2837
2888
  }, []);
2838
- const shown = editing ? text : (() => {
2839
- const n = num(text);
2840
- return n == null ? "" : format ? n.toLocaleString() : String(n);
2841
- })();
2889
+ const current2 = committedRef.current;
2890
+ const shown = editing || !valid ? text : parsed == null ? "" : format ? parsed.toLocaleString() : String(parsed);
2842
2891
  const shownError = error || invalidMessage || void 0;
2843
2892
  const box = ["fd-input", "fd-input-num", shownError ? "fd-input-error" : "", disabled ? "fd-input-disabled" : ""].filter(Boolean).join(" ");
2844
2893
  const fieldName = label || ariaLabel;
2845
2894
  const stepperName = (verb) => fieldName ? verb + " " + fieldName : verb;
2895
+ const noRange = lo > hi;
2846
2896
  return /* @__PURE__ */ jsxs36("div", { className: ["fd-field", className].filter(Boolean).join(" "), style, children: [
2847
2897
  label ? /* @__PURE__ */ jsx42(FieldLabel, { htmlFor: fieldId, required, children: label }) : null,
2848
- /* @__PURE__ */ jsxs36("div", { className: box, style: { gap: 8 }, children: [
2898
+ /* @__PURE__ */ jsxs36("div", { className: box, children: [
2849
2899
  prefix ? /* @__PURE__ */ jsx42("span", { className: "fd-input-affix", children: prefix }) : null,
2850
2900
  /* @__PURE__ */ jsx42(
2851
2901
  "input",
@@ -2858,52 +2908,54 @@ function NumberInput({
2858
2908
  value: shown,
2859
2909
  "aria-invalid": shownError ? "true" : void 0,
2860
2910
  "aria-describedby": shownError ? errorId : void 0,
2861
- onFocus: () => setEditing(true),
2911
+ onFocus: () => {
2912
+ focusedRef.current = true;
2913
+ setEditing(true);
2914
+ },
2862
2915
  onBlur: () => {
2916
+ focusedRef.current = false;
2863
2917
  setEditing(false);
2864
- if (text.trim() === "") fire(void 0);
2918
+ if (text.trim() === "" && committedRef.current != null) fire(void 0);
2865
2919
  },
2866
2920
  onChange: (e) => {
2867
2921
  const raw = e.target.value;
2868
2922
  setText(raw);
2869
- if (raw.trim() === "") {
2870
- setInvalidMessage(null);
2871
- fire(void 0);
2872
- return;
2873
- }
2874
- const n = num(raw);
2923
+ const n = parseNumberText(raw);
2875
2924
  if (n == null) {
2876
- setInvalidMessage("Enter a number");
2925
+ if (committedRef.current != null) fire(void 0);
2877
2926
  return;
2878
2927
  }
2879
- const message = validationMessage(n);
2880
- setInvalidMessage(message);
2881
- if (!message) fire(n);
2928
+ if (Number.isNaN(n) || integer && !Number.isInteger(n) || n < lo || n > hi) return;
2929
+ fire(n);
2882
2930
  },
2883
2931
  onKeyDown: (e) => {
2884
2932
  if (e.key === "ArrowUp") {
2885
2933
  e.preventDefault();
2886
- bump(1, e.shiftKey ? 2 : 1);
2934
+ bump(1, e.shiftKey ? big : step);
2887
2935
  } else if (e.key === "ArrowDown") {
2888
2936
  e.preventDefault();
2889
- bump(-1, e.shiftKey ? 2 : 1);
2937
+ bump(-1, e.shiftKey ? big : step);
2890
2938
  }
2891
2939
  }
2892
2940
  }
2893
2941
  ),
2894
2942
  suffix ? /* @__PURE__ */ jsx42("span", { className: "fd-input-affix", children: suffix }) : null,
2895
- /* @__PURE__ */ jsxs36("span", { className: "fd-row", style: { gap: 4, flex: "none" }, children: [
2943
+ /* @__PURE__ */ jsxs36("span", { className: "fd-input-steppers", children: [
2896
2944
  /* @__PURE__ */ jsx42(
2897
2945
  "button",
2898
2946
  {
2899
2947
  type: "button",
2900
2948
  className: "fd-stepbtn",
2901
2949
  "aria-label": stepperName("Decrease"),
2902
- disabled: disabled || (num(text) ?? 0) <= min,
2950
+ disabled: disabled || noRange || current2 != null && current2 <= lo,
2903
2951
  onPointerDown: () => hold(-1),
2904
2952
  onPointerUp: release,
2905
2953
  onPointerLeave: release,
2906
- children: /* @__PURE__ */ jsx42("i", { className: "ph ph-minus" })
2954
+ onPointerCancel: release,
2955
+ onClick: (e) => {
2956
+ if (e.detail === 0) bump(-1, step);
2957
+ },
2958
+ children: /* @__PURE__ */ jsx42("i", { className: "ph ph-minus", "aria-hidden": "true" })
2907
2959
  }
2908
2960
  ),
2909
2961
  /* @__PURE__ */ jsx42(
@@ -2912,11 +2964,15 @@ function NumberInput({
2912
2964
  type: "button",
2913
2965
  className: "fd-stepbtn",
2914
2966
  "aria-label": stepperName("Increase"),
2915
- disabled: disabled || (num(text) ?? 0) >= max,
2967
+ disabled: disabled || noRange || current2 != null && current2 >= hi,
2916
2968
  onPointerDown: () => hold(1),
2917
2969
  onPointerUp: release,
2918
2970
  onPointerLeave: release,
2919
- children: /* @__PURE__ */ jsx42("i", { className: "ph ph-plus" })
2971
+ onPointerCancel: release,
2972
+ onClick: (e) => {
2973
+ if (e.detail === 0) bump(1, step);
2974
+ },
2975
+ children: /* @__PURE__ */ jsx42("i", { className: "ph ph-plus", "aria-hidden": "true" })
2920
2976
  }
2921
2977
  )
2922
2978
  ] })
@@ -3201,16 +3257,31 @@ function isFieldSet(field, value) {
3201
3257
  }
3202
3258
  }
3203
3259
  }
3260
+ var MAX_WITHIN_DAYS = 3650;
3261
+ function effectiveMaxDays(field) {
3262
+ return Math.min(field.maxDays ?? MAX_WITHIN_DAYS, MAX_WITHIN_DAYS);
3263
+ }
3264
+ function isIsoDate(v) {
3265
+ if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(v)) return false;
3266
+ const d = /* @__PURE__ */ new Date(v + "T00:00:00Z");
3267
+ return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === v;
3268
+ }
3269
+ var isOption = (o) => !!o && typeof o.value === "string" && typeof o.label === "string";
3204
3270
  function validateFilterValue(field, value) {
3205
3271
  switch (field.type) {
3206
3272
  case "text":
3273
+ return typeof value === "string" ? { ok: true } : { ok: false, reason: "Enter text" };
3207
3274
  case "select":
3208
- return { ok: true };
3275
+ return field.options.some((o) => o.value === value) ? { ok: true } : { ok: false, reason: "Pick one of the options" };
3209
3276
  case "multiSelect": {
3210
3277
  const v = value;
3211
3278
  if (!v || !Array.isArray(v.values) || v.values.length === 0) {
3212
3279
  return { ok: false, reason: "Select at least one value" };
3213
3280
  }
3281
+ if (!v.values.every(isOption)) return { ok: false, reason: "Each value needs a value and a label" };
3282
+ if (field.maxValues != null && v.values.length > field.maxValues) {
3283
+ return { ok: false, reason: "Select at most " + field.maxValues + " values" };
3284
+ }
3214
3285
  if (!operatorsFor(field).includes(v.operator)) {
3215
3286
  return { ok: false, reason: "That operator isn't allowed for " + field.label };
3216
3287
  }
@@ -3225,6 +3296,7 @@ function validateFilterValue(field, value) {
3225
3296
  const hi = field.max ?? Infinity;
3226
3297
  for (const n of [v.min, v.max]) {
3227
3298
  if (n == null) continue;
3299
+ if (typeof n !== "number" || !Number.isFinite(n)) return { ok: false, reason: "Enter a number" };
3228
3300
  if (field.integer && !Number.isInteger(n)) return { ok: false, reason: "Enter a whole number" };
3229
3301
  if (n < lo) return { ok: false, reason: "Must be \u2265 " + lo };
3230
3302
  if (n > hi) return { ok: false, reason: "Must be \u2264 " + hi };
@@ -3238,46 +3310,36 @@ function validateFilterValue(field, value) {
3238
3310
  const v = value;
3239
3311
  if (!v) return { ok: false, reason: "Pick a date value" };
3240
3312
  if (v.kind === "withinDays") {
3241
- const maxDays = field.maxDays ?? 3650;
3313
+ const maxDays = effectiveMaxDays(field);
3242
3314
  if (v.days == null) return { ok: false, reason: "Enter a number of days" };
3243
3315
  if (!Number.isInteger(v.days)) return { ok: false, reason: "Enter a whole number" };
3244
3316
  if (v.days < 1) return { ok: false, reason: "Must be \u2265 1" };
3245
3317
  if (v.days > maxDays) return { ok: false, reason: "Must be \u2264 " + maxDays };
3246
3318
  return { ok: true };
3247
3319
  }
3320
+ if (v.kind !== "between") return { ok: false, reason: "Pick a date value" };
3248
3321
  if (!v.from || !v.to) return { ok: false, reason: "Pick both dates" };
3322
+ if (!isIsoDate(v.from) || !isIsoDate(v.to)) return { ok: false, reason: "Dates must be YYYY-MM-DD" };
3249
3323
  if (v.from > v.to) return { ok: false, reason: "The start date must be on or before the end date" };
3250
3324
  return { ok: true };
3251
3325
  }
3252
3326
  }
3253
3327
  }
3254
3328
  function isEmptyCandidate(field, candidate) {
3255
- switch (field.type) {
3256
- case "text":
3257
- case "select":
3258
- return false;
3259
- case "multiSelect": {
3260
- const v = candidate;
3261
- return !v || !Array.isArray(v.values) || v.values.length === 0;
3262
- }
3263
- case "range": {
3264
- const v = candidate;
3265
- return !v || v.min == null && v.max == null;
3266
- }
3267
- case "date": {
3268
- const v = candidate;
3269
- if (!v) return true;
3270
- return v.kind === "withinDays" ? v.days == null : !(v.from && v.to);
3271
- }
3272
- }
3329
+ return field.type !== "text" && field.type !== "select" && !isFieldSet(field, candidate);
3273
3330
  }
3274
- function assertValidDateField(field) {
3275
- if (process.env.NODE_ENV === "production") return;
3331
+ function assertValidFieldDefinition(field) {
3332
+ if (field.type === "multiSelect" && field.maxValues != null && (!Number.isInteger(field.maxValues) || field.maxValues < 1)) {
3333
+ throw new Error(
3334
+ 'FilterBar: multiSelect field "' + field.key + '" has an invalid maxValues (' + field.maxValues + ") \u2014 must be a whole number of at least 1."
3335
+ );
3336
+ }
3337
+ if (field.type !== "date") return;
3276
3338
  const defaultDays = field.defaultDays ?? 30;
3277
- const maxDays = field.maxDays ?? 3650;
3278
- if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays > 3650) {
3339
+ const maxDays = field.maxDays ?? MAX_WITHIN_DAYS;
3340
+ if (!Number.isInteger(maxDays) || maxDays < 1 || maxDays > MAX_WITHIN_DAYS) {
3279
3341
  throw new Error(
3280
- 'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to 3650."
3342
+ 'FilterBar: date field "' + field.key + '" has an invalid maxDays (' + field.maxDays + ") \u2014 must be a whole number from 1 to " + MAX_WITHIN_DAYS + "."
3281
3343
  );
3282
3344
  }
3283
3345
  if (!Number.isInteger(defaultDays) || defaultDays < 1 || defaultDays > maxDays) {
@@ -3361,7 +3423,7 @@ function MultiSelectPanel({
3361
3423
  };
3362
3424
  const setOperator = (op) => {
3363
3425
  setDraftOperator(op);
3364
- if (selected.length > 0) commit(selected, op);
3426
+ if (selectedRef.current.length > 0) commit(selectedRef.current, op);
3365
3427
  };
3366
3428
  const [mode2, setMode] = React24.useState("search");
3367
3429
  const [query, setQuery] = React24.useState("");
@@ -3401,9 +3463,12 @@ function MultiSelectPanel({
3401
3463
  const showError = !!field.loadOptions && asyncState.error;
3402
3464
  const hasListbox = !showLoading && !showError && options.length > 0;
3403
3465
  const isSelected = (o) => selected.some((s) => s.value === o.value);
3466
+ const atLimit = field.maxValues != null && selected.length >= field.maxValues;
3404
3467
  const toggle = (o) => {
3405
- const next = isSelected(o) ? selected.filter((s) => s.value !== o.value) : [...selected, o];
3406
- commit(next);
3468
+ const current2 = selectedRef.current;
3469
+ const on = current2.some((s) => s.value === o.value);
3470
+ if (!on && field.maxValues != null && current2.length >= field.maxValues) return;
3471
+ commit(on ? current2.filter((s) => s.value !== o.value) : [...current2, o]);
3407
3472
  };
3408
3473
  const nav = useListboxNav({ count: options.length, onSelect: (i) => toggle(options[i]) });
3409
3474
  useListboxScrollIntoView(nav.active, listRef, hasListbox);
@@ -3411,8 +3476,7 @@ function MultiSelectPanel({
3411
3476
  nav.setActive(0);
3412
3477
  }, [query, options.length]);
3413
3478
  const activeId = hasListbox && options[nav.active] ? listId + "-opt-" + nav.active : void 0;
3414
- const liveRole = showError ? "alert" : "status";
3415
- const liveMessage = showLoading ? "Loading\u2026" : showError ? "Couldn't load options." : options.length === 0 ? "No matches" : "";
3479
+ const liveMessage = showLoading ? "Loading\u2026" : showError ? "Couldn't load options." : options.length === 0 ? "No matches" : atLimit ? "Limit reached: at most " + field.maxValues + " values" : "";
3416
3480
  const [pasteText, setPasteText] = React24.useState("");
3417
3481
  const [pasteLoading, setPasteLoading] = React24.useState(false);
3418
3482
  const [unmatchedTokens, setUnmatchedTokens] = React24.useState([]);
@@ -3433,9 +3497,13 @@ function MultiSelectPanel({
3433
3497
  setUnmatchedTokens(result.unmatched);
3434
3498
  const merged = new Map(selectedRef.current.map((s) => [s.value, s]));
3435
3499
  for (const m of result.matched) merged.set(m.value, { value: m.value, label: m.label });
3500
+ if (field.maxValues != null && merged.size > field.maxValues) {
3501
+ setPasteError("This filter allows at most " + field.maxValues + " values; adding this list would make " + merged.size + ".");
3502
+ return;
3503
+ }
3436
3504
  commit(Array.from(merged.values()), operatorRef.current);
3437
3505
  setPasteText("");
3438
- }).catch((err) => {
3506
+ }, (err) => {
3439
3507
  if (!mountedRef.current) return;
3440
3508
  setPasteLoading(false);
3441
3509
  setPasteError(err instanceof Error ? err.message : "Could not resolve the pasted list.");
@@ -3514,14 +3582,14 @@ function MultiSelectPanel({
3514
3582
  selected.length,
3515
3583
  " matched"
3516
3584
  ] }),
3517
- /* @__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)) })
3585
+ /* @__PURE__ */ jsx45("span", { className: "fd-row fd-wrap fd-gap-xs", children: selected.map((v) => /* @__PURE__ */ jsx45(Tag, { onRemove: () => commit(selectedRef.current.filter((s) => s.value !== v.value)), children: v.label }, v.value)) })
3518
3586
  ] }) : null
3519
3587
  ] }) : /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3520
3588
  /* @__PURE__ */ jsxs39(
3521
3589
  "span",
3522
3590
  {
3523
3591
  className: "fd-row fd-gap-xs" + (liveMessage && !showError ? " fd-muted" : ""),
3524
- role: liveRole,
3592
+ role: "status",
3525
3593
  children: [
3526
3594
  showLoading ? /* @__PURE__ */ jsx45("span", { className: "fd-spinner", "aria-hidden": "true" }) : null,
3527
3595
  liveMessage ? /* @__PURE__ */ jsx45("span", { className: "fd-body-sm", children: liveMessage }) : null
@@ -3531,6 +3599,7 @@ function MultiSelectPanel({
3531
3599
  showError ? /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: () => setRetryTick((n) => n + 1), children: "Retry" }) : null,
3532
3600
  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) => {
3533
3601
  const on = isSelected(o);
3602
+ const blocked = !on && atLimit;
3534
3603
  return /* @__PURE__ */ jsxs39(
3535
3604
  "div",
3536
3605
  {
@@ -3539,6 +3608,7 @@ function MultiSelectPanel({
3539
3608
  role: "option",
3540
3609
  tabIndex: -1,
3541
3610
  "aria-selected": on,
3611
+ "aria-disabled": blocked ? "true" : void 0,
3542
3612
  className: ["fd-opt", i === nav.active ? "is-active" : "", on ? "is-selected" : ""].filter(Boolean).join(" "),
3543
3613
  onMouseEnter: () => nav.setActive(i),
3544
3614
  onClick: () => toggle(o),
@@ -3575,18 +3645,21 @@ function RangePanel({
3575
3645
  onCommit,
3576
3646
  onClose
3577
3647
  }) {
3578
- const [draftMin, setDraftMin] = React24.useState(value?.min);
3579
- const [draftMax, setDraftMax] = React24.useState(value?.max);
3648
+ const [draft, setDraft] = React24.useState({ min: value?.min, max: value?.max });
3649
+ const draftRef = React24.useRef(draft);
3580
3650
  const [minTextValid, setMinTextValid] = React24.useState(true);
3581
3651
  const [maxTextValid, setMaxTextValid] = React24.useState(true);
3652
+ const draftMin = draft.min;
3653
+ const draftMax = draft.max;
3582
3654
  const isEmpty = draftMin == null && draftMax == null;
3583
3655
  const validation = isEmpty ? { ok: true } : validateFilterValue(field, { min: draftMin, max: draftMax });
3584
3656
  const comboError = !isEmpty && !validation.ok ? validation.reason : void 0;
3585
3657
  const doneDisabled = !minTextValid || !maxTextValid || !!comboError;
3586
- const apply = (nextMin, nextMax) => {
3587
- setDraftMin(nextMin);
3588
- setDraftMax(nextMax);
3589
- onCommit(nextMin == null && nextMax == null ? void 0 : { min: nextMin, max: nextMax });
3658
+ const apply = (patch) => {
3659
+ const next = { ...draftRef.current, ...patch };
3660
+ draftRef.current = next;
3661
+ setDraft(next);
3662
+ onCommit(next.min == null && next.max == null ? void 0 : next);
3590
3663
  };
3591
3664
  return /* @__PURE__ */ jsx45(
3592
3665
  Popover,
@@ -3610,7 +3683,7 @@ function RangePanel({
3610
3683
  step: field.step,
3611
3684
  integer: field.integer,
3612
3685
  placeholder: field.minPlaceholder,
3613
- onChange: (e) => apply(e.target.value, draftMax),
3686
+ onChange: (e) => apply({ min: e.target.value }),
3614
3687
  onValidityChange: setMinTextValid
3615
3688
  }
3616
3689
  ),
@@ -3625,7 +3698,7 @@ function RangePanel({
3625
3698
  integer: field.integer,
3626
3699
  error: comboError,
3627
3700
  placeholder: field.maxPlaceholder,
3628
- onChange: (e) => apply(draftMin, e.target.value),
3701
+ onChange: (e) => apply({ max: e.target.value }),
3629
3702
  onValidityChange: setMaxTextValid
3630
3703
  }
3631
3704
  )
@@ -3641,7 +3714,8 @@ function DatePanel({
3641
3714
  onClose
3642
3715
  }) {
3643
3716
  const defaultDays = field.defaultDays ?? 30;
3644
- const maxDays = field.maxDays ?? 3650;
3717
+ const maxDays = effectiveMaxDays(field);
3718
+ const [daysTextValid, setDaysTextValid] = React24.useState(true);
3645
3719
  const [kind, setKind] = React24.useState(value?.kind ?? "withinDays");
3646
3720
  const [daysDraft, setDaysDraft] = React24.useState(
3647
3721
  value?.kind === "withinDays" ? value.days : defaultDays
@@ -3662,9 +3736,12 @@ function DatePanel({
3662
3736
  setKind(next);
3663
3737
  if (next === "between") setBetween({ from: "", to: "" });
3664
3738
  else setDaysDraft(defaultDays);
3739
+ setDaysTextValid(true);
3665
3740
  onCommit(void 0);
3666
3741
  };
3742
+ const doneDisabled = kind === "withinDays" && !daysTextValid;
3667
3743
  const confirm = () => {
3744
+ if (doneDisabled) return;
3668
3745
  if (kind === "withinDays" && daysDraft != null) onCommit({ kind: "withinDays", days: daysDraft });
3669
3746
  onClose();
3670
3747
  };
@@ -3678,7 +3755,7 @@ function DatePanel({
3678
3755
  offset: 6,
3679
3756
  width: PANEL_WIDTH.date,
3680
3757
  label: field.label,
3681
- footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: confirm, children: "Done" }),
3758
+ footer: /* @__PURE__ */ jsx45(Button, { size: "sm", variant: "secondary", onClick: confirm, disabled: doneDisabled, children: "Done" }),
3682
3759
  children: /* @__PURE__ */ jsxs39("span", { className: "fd-stack fd-gap-sm", children: [
3683
3760
  /* @__PURE__ */ jsx45(
3684
3761
  SegmentedControl,
@@ -3692,7 +3769,19 @@ function DatePanel({
3692
3769
  onChange: (k) => switchKind(k)
3693
3770
  }
3694
3771
  ),
3695
- kind === "withinDays" ? /* @__PURE__ */ jsx45(NumberInput, { label: "Days", min: 1, max: maxDays, step: 1, integer: true, value: daysDraft ?? "", onChange: (e) => applyWithinDays(e.target.value) }) : /* @__PURE__ */ jsx45(
3772
+ kind === "withinDays" ? /* @__PURE__ */ jsx45(
3773
+ NumberInput,
3774
+ {
3775
+ label: "Days",
3776
+ min: 1,
3777
+ max: maxDays,
3778
+ step: 1,
3779
+ integer: true,
3780
+ value: daysDraft ?? "",
3781
+ onChange: (e) => applyWithinDays(e.target.value),
3782
+ onValidityChange: setDaysTextValid
3783
+ }
3784
+ ) : /* @__PURE__ */ jsx45(
3696
3785
  DatePicker,
3697
3786
  {
3698
3787
  range: true,
@@ -3712,19 +3801,31 @@ function FilterBar({ fields = [], filters, onFilterChange, onFilterRemove, align
3712
3801
  const textInputRef = React24.useRef(null);
3713
3802
  const optionsRef = React24.useRef(null);
3714
3803
  if (process.env.NODE_ENV !== "production") {
3715
- for (const f of fields) if (f.type === "date") assertValidDateField(f);
3804
+ for (const f of fields) assertValidFieldDefinition(f);
3716
3805
  }
3806
+ const latest = React24.useRef({ fields, filters, onFilterChange, onFilterRemove });
3807
+ latest.current = { fields, filters, onFilterChange, onFilterRemove };
3717
3808
  const commit = (field, candidate) => {
3718
- if (isEmptyCandidate(field, candidate)) {
3719
- removeField(field);
3809
+ const current2 = latest.current.fields.find((f) => f.key === field.key);
3810
+ if (!current2) return { ok: false, reason: "This filter is no longer available" };
3811
+ if (isEmptyCandidate(current2, candidate)) {
3812
+ removeField(current2);
3720
3813
  return { ok: true };
3721
3814
  }
3722
- const result = validateFilterValue(field, candidate);
3723
- if (result.ok) onFilterChange(field.key, candidate);
3815
+ const result = validateFilterValue(current2, candidate);
3816
+ if (result.ok) {
3817
+ latest.current.filters = { ...latest.current.filters, [current2.key]: candidate };
3818
+ latest.current.onFilterChange(current2.key, candidate);
3819
+ }
3724
3820
  return result;
3725
3821
  };
3726
3822
  const removeField = (field) => {
3727
- if (isFieldSet(field, filters[field.key])) onFilterRemove(field.key);
3823
+ const { filters: now, onFilterRemove: remove } = latest.current;
3824
+ if (!isFieldSet(field, now[field.key])) return;
3825
+ const next = { ...now };
3826
+ delete next[field.key];
3827
+ latest.current.filters = next;
3828
+ remove(field.key);
3728
3829
  };
3729
3830
  const editingField = panel && panel.kind === "edit" ? fields.find((f) => f.key === panel.key) : void 0;
3730
3831
  const startEditing = (key) => setPanel({ kind: "edit", key });
@@ -13718,6 +13819,7 @@ export {
13718
13819
  useServerTable,
13719
13820
  useSessionContext,
13720
13821
  useStagedFiles,
13822
+ validateFilterValue,
13721
13823
  visibleMessages
13722
13824
  };
13723
13825
  //# sourceMappingURL=index.js.map