@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 +104 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -36
- package/dist/index.d.ts +40 -36
- package/dist/index.js +104 -71
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/styles/components.css +2 -2
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
|
-
/**
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
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
|
-
/**
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
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
|
-
/**
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
1165
|
-
*
|
|
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
|
|
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
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
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
|
-
*
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
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
|
-
/**
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
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
|
-
/**
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
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
|
-
/**
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
1165
|
-
*
|
|
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
|
|
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
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
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
|
-
*
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1209
|
-
*
|
|
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"
|
|
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)
|
|
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
|
|
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":
|
|
2840
|
-
"aria-describedby":
|
|
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
|
-
|
|
2857
|
-
|
|
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(
|
|
2862
|
-
if (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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
3310
|
+
useListboxScrollIntoView(nav.active, listRef, hasListbox);
|
|
3294
3311
|
React24.useEffect(() => {
|
|
3295
3312
|
nav.setActive(0);
|
|
3296
3313
|
}, [query, options.length]);
|
|
3297
|
-
const activeId =
|
|
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:
|
|
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":
|
|
3343
|
-
"aria-controls":
|
|
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
|
-
] }) :
|
|
3400
|
-
/* @__PURE__ */
|
|
3401
|
-
|
|
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
|
-
|
|
3411
|
-
|
|
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("
|
|
3420
|
-
/* @__PURE__ */ jsx45("span", { className: "fd-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3518
|
-
setDraft(next);
|
|
3551
|
+
setDaysDraft(days);
|
|
3519
3552
|
if (days != null) {
|
|
3520
3553
|
committedRef.current = true;
|
|
3521
|
-
onChange(
|
|
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
|
-
|
|
3526
|
-
setDraft(next);
|
|
3561
|
+
setBetween({ from, to });
|
|
3527
3562
|
if (from && to) {
|
|
3528
3563
|
committedRef.current = true;
|
|
3529
|
-
onChange(
|
|
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 = (
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
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 (
|
|
3581
|
+
if (kind === "withinDays" && daysDraft != null) {
|
|
3549
3582
|
committedRef.current = true;
|
|
3550
|
-
onChange(
|
|
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:
|
|
3607
|
+
value: kind,
|
|
3575
3608
|
onChange: (k) => switchKind(k)
|
|
3576
3609
|
}
|
|
3577
3610
|
),
|
|
3578
|
-
|
|
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:
|
|
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
|
)
|