@rufous/ui 0.3.40 → 0.3.41

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/main.cjs CHANGED
@@ -1422,14 +1422,14 @@ var SubmitButton = ({
1422
1422
  setLoading(false);
1423
1423
  }
1424
1424
  };
1425
- const handleClick = (e) => runProtected(e, onClick);
1426
- const handleDoubleClick = (e) => runProtected(e, onDoubleClick);
1425
+ const handleClick = onClick ? (e) => runProtected(e, onClick) : void 0;
1426
+ const handleDoubleClick = onDoubleClick ? (e) => runProtected(e, onDoubleClick) : void 0;
1427
1427
  const showLoader = loading || isLoading;
1428
1428
  return /* @__PURE__ */ React60.createElement(
1429
1429
  "button",
1430
1430
  {
1431
1431
  ...props,
1432
- type: onClick ? void 0 : type,
1432
+ type,
1433
1433
  className: ["btn submit-btn", bgGradiant ? "submit-btn-gradiant" : "", sxClass, className].filter(Boolean).join(" "),
1434
1434
  disabled: props.disabled || showLoader,
1435
1435
  onClick: handleClick,
@@ -10665,6 +10665,14 @@ function buildLookup(options, getChildren, getValue, map = /* @__PURE__ */ new M
10665
10665
  }
10666
10666
  return map;
10667
10667
  }
10668
+ function safeLabel(getOptionLabel, option) {
10669
+ try {
10670
+ const l = getOptionLabel(option);
10671
+ return typeof l === "string" ? l : "";
10672
+ } catch {
10673
+ return "";
10674
+ }
10675
+ }
10668
10676
  function SmartSelect({
10669
10677
  options,
10670
10678
  value,
@@ -10681,6 +10689,7 @@ function SmartSelect({
10681
10689
  getOptionChildren,
10682
10690
  multiple = false,
10683
10691
  allowChildNodesSelection = true,
10692
+ strictSelection = false,
10684
10693
  loading = false,
10685
10694
  loadingText,
10686
10695
  filterOptions: filterOptionsProp,
@@ -10704,15 +10713,6 @@ function SmartSelect({
10704
10713
  (0, import_react54.useEffect)(() => () => {
10705
10714
  if (debounceTimer.current) clearTimeout(debounceTimer.current);
10706
10715
  }, []);
10707
- const [internalInput, setInternalInput] = (0, import_react54.useState)(
10708
- () => !multiple && value != null ? getOptionLabel(value) : ""
10709
- );
10710
- (0, import_react54.useEffect)(() => {
10711
- if (!isControlled && !multiple) {
10712
- setInternalInput(value != null ? getOptionLabel(value) : "");
10713
- }
10714
- }, [value, multiple, getOptionLabel, isControlled]);
10715
- const activeInput = isControlled ? inputValueProp : internalInput;
10716
10716
  const getValue = (0, import_react54.useCallback)(
10717
10717
  (o) => getOptionValue ? getOptionValue(o) : String(getOptionLabel(o)),
10718
10718
  [getOptionValue, getOptionLabel]
@@ -10722,6 +10722,55 @@ function SmartSelect({
10722
10722
  return flattenTree(options, getOptionChildren);
10723
10723
  }, [options, getOptionChildren]);
10724
10724
  const flatOptionsList = (0, import_react54.useMemo)(() => flatItems.map((f) => f.option), [flatItems]);
10725
+ const strictValidation = (0, import_react54.useMemo)(() => {
10726
+ if (!strictSelection || multiple) return { valid: true, canonicalValue: value };
10727
+ if (value == null || value === "") {
10728
+ return { valid: false, displayText: "" };
10729
+ }
10730
+ const invalidDisplayText = (v) => {
10731
+ const fromLabel = safeLabel(getOptionLabel, v).trim();
10732
+ if (fromLabel) return fromLabel;
10733
+ if (typeof v === "string" || typeof v === "number") return String(v);
10734
+ return "";
10735
+ };
10736
+ try {
10737
+ const key = getValue(value);
10738
+ const canonicalOpt = flatOptionsList.find((o) => getValue(o) === key) ?? searchResults.find((o) => getValue(o) === key);
10739
+ if (!canonicalOpt) {
10740
+ return { valid: false, displayText: invalidDisplayText(value) };
10741
+ }
10742
+ if (typeof canonicalOpt === "object" && canonicalOpt !== null && typeof value === "object" && value !== null) {
10743
+ const src = canonicalOpt;
10744
+ const inc = value;
10745
+ const fullMatch = Object.keys(src).every((k) => k in inc && inc[k] === src[k]);
10746
+ if (!fullMatch) {
10747
+ return { valid: false, displayText: invalidDisplayText(value) };
10748
+ }
10749
+ }
10750
+ return { valid: true, canonicalValue: canonicalOpt };
10751
+ } catch {
10752
+ return { valid: false, displayText: invalidDisplayText(value) };
10753
+ }
10754
+ }, [strictSelection, multiple, value, getValue, flatOptionsList, searchResults, getOptionLabel]);
10755
+ const [internalInput, setInternalInput] = (0, import_react54.useState)(
10756
+ () => !multiple && value != null ? getOptionLabel(value) : ""
10757
+ );
10758
+ const strictValidationRef = (0, import_react54.useRef)(strictValidation);
10759
+ strictValidationRef.current = strictValidation;
10760
+ const lastSeededValue = (0, import_react54.useRef)(/* @__PURE__ */ Symbol("init"));
10761
+ (0, import_react54.useEffect)(() => {
10762
+ if (isControlled || multiple) return;
10763
+ if (strictSelection && !strictValidationRef.current.valid) {
10764
+ if (Object.is(value, lastSeededValue.current)) return;
10765
+ lastSeededValue.current = value;
10766
+ const sv = strictValidationRef.current;
10767
+ setInternalInput(sv.displayText);
10768
+ return;
10769
+ }
10770
+ lastSeededValue.current = value;
10771
+ setInternalInput(value != null ? getOptionLabel(value) : "");
10772
+ }, [value, multiple, getOptionLabel, isControlled, strictSelection]);
10773
+ const activeInput = isControlled ? inputValueProp : internalInput;
10725
10774
  const displayOptions = (0, import_react54.useMemo)(() => {
10726
10775
  let base = flatOptionsList;
10727
10776
  if (searchResults.length) {
@@ -10729,17 +10778,26 @@ function SmartSelect({
10729
10778
  const serverOnly = searchResults.filter((o) => !localKeys.has(getValue(o)));
10730
10779
  base = [...flatOptionsList, ...serverOnly];
10731
10780
  }
10732
- if (!multiple && value != null) {
10733
- const key = getValue(value);
10734
- if (!base.some((o) => getValue(o) === key)) base = [value, ...base];
10735
- }
10736
- if (multiple && Array.isArray(value) && value.length > 0) {
10781
+ if (!multiple) {
10782
+ if (strictSelection) {
10783
+ const injectValue = strictValidation.valid ? strictValidation.canonicalValue : null;
10784
+ if (injectValue != null) {
10785
+ const key = getValue(injectValue);
10786
+ if (!base.some((o) => getValue(o) === key)) base = [injectValue, ...base];
10787
+ }
10788
+ } else {
10789
+ if (value != null) {
10790
+ const key = getValue(value);
10791
+ if (!base.some((o) => getValue(o) === key)) base = [value, ...base];
10792
+ }
10793
+ }
10794
+ } else if (Array.isArray(value) && value.length > 0) {
10737
10795
  const baseKeys = new Set(base.map((o) => getValue(o)));
10738
10796
  const missing = value.filter((v) => !baseKeys.has(getValue(v)));
10739
10797
  if (missing.length > 0) base = [...base, ...missing];
10740
10798
  }
10741
10799
  return base;
10742
- }, [flatOptionsList, searchResults, getValue, value, multiple]);
10800
+ }, [flatOptionsList, searchResults, getValue, value, multiple, strictSelection, strictValidation]);
10743
10801
  const depthMap = (0, import_react54.useMemo)(() => {
10744
10802
  const map = /* @__PURE__ */ new Map();
10745
10803
  flatItems.forEach(({ option, depth }) => map.set(getValue(option), depth));
@@ -10753,13 +10811,31 @@ function SmartSelect({
10753
10811
  if (multiple) {
10754
10812
  return new Set((Array.isArray(value) ? value : []).map((v) => getValue(v)));
10755
10813
  }
10814
+ if (strictSelection) {
10815
+ const sv = strictValidation;
10816
+ if (!strictValidation.valid || sv.canonicalValue == null) return /* @__PURE__ */ new Set();
10817
+ return /* @__PURE__ */ new Set([getValue(sv.canonicalValue)]);
10818
+ }
10756
10819
  return value != null ? /* @__PURE__ */ new Set([getValue(value)]) : /* @__PURE__ */ new Set();
10757
- }, [multiple, value, getValue]);
10820
+ }, [multiple, value, getValue, strictSelection, strictValidation]);
10821
+ let autocompleteValue;
10822
+ let autocompleteInputValue;
10823
+ if (strictSelection && !multiple) {
10824
+ const sv = strictValidation;
10825
+ if (!strictValidation.valid) {
10826
+ autocompleteValue = null;
10827
+ autocompleteInputValue = isControlled ? inputValueProp : internalInput;
10828
+ } else {
10829
+ autocompleteValue = sv.canonicalValue ?? null;
10830
+ autocompleteInputValue = isControlled ? inputValueProp : activeInput;
10831
+ }
10832
+ } else {
10833
+ autocompleteValue = value ?? (multiple ? [] : null);
10834
+ autocompleteInputValue = multiple ? isControlled ? inputValueProp : void 0 : activeInput;
10835
+ }
10758
10836
  const handleInputChange = (0, import_react54.useCallback)((_, val, reason) => {
10759
10837
  const resolvedReason = reason ?? "input";
10760
- if (!isControlled) {
10761
- setInternalInput(val);
10762
- }
10838
+ if (!isControlled) setInternalInput(val);
10763
10839
  onInputChange?.(val, resolvedReason);
10764
10840
  if (!onSearchChange) return;
10765
10841
  if (debounceTimer.current) clearTimeout(debounceTimer.current);
@@ -10781,9 +10857,7 @@ function SmartSelect({
10781
10857
  if (debounceMs <= 0) {
10782
10858
  onSearchChange(val, needed);
10783
10859
  } else {
10784
- debounceTimer.current = setTimeout(() => {
10785
- onSearchChange(val, needed);
10786
- }, debounceMs);
10860
+ debounceTimer.current = setTimeout(() => onSearchChange(val, needed), debounceMs);
10787
10861
  }
10788
10862
  }, [isControlled, onInputChange, onSearchChange, debounceMs, searchThreshold, flatOptionsList, getOptionLabel, getOptionSubLabel]);
10789
10863
  const handleChange = (0, import_react54.useCallback)((_, newValue) => {
@@ -10863,6 +10937,14 @@ function SmartSelect({
10863
10937
  }, [depthMap, getValue, getOptionLabel, getOptionSubLabel, selectedKeys]);
10864
10938
  const computedFilterOptions = (0, import_react54.useCallback)((opts, inputVal) => {
10865
10939
  if (filterOptionsProp) return filterOptionsProp(opts, inputVal);
10940
+ if (strictSelection && !strictValidation.valid) {
10941
+ const sv = strictValidation;
10942
+ if (!inputVal || inputVal === sv.displayText) return opts;
10943
+ const q2 = inputVal.toLowerCase();
10944
+ return opts.filter(
10945
+ (opt) => getOptionLabel(opt).toLowerCase().includes(q2) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q2)
10946
+ ).slice(0, searchThreshold);
10947
+ }
10866
10948
  if (multiple) {
10867
10949
  const selected = opts.filter((o) => selectedKeys.has(getValue(o)));
10868
10950
  const unselected = opts.filter((o) => !selectedKeys.has(getValue(o)));
@@ -10873,11 +10955,12 @@ function SmartSelect({
10873
10955
  ).slice(0, searchThreshold);
10874
10956
  return [...selected, ...filteredUnselected];
10875
10957
  }
10876
- if (value != null) {
10877
- const selectedKey = getValue(value);
10878
- const selectedLabel = getOptionLabel(value);
10958
+ const effectiveVal = strictSelection ? strictValidation.canonicalValue : value;
10959
+ if (effectiveVal != null) {
10960
+ const selectedKey = getValue(effectiveVal);
10961
+ const selectedLabel = getOptionLabel(effectiveVal);
10879
10962
  const inOpts = opts.some((o) => getValue(o) === selectedKey);
10880
- const selectedFallback = inOpts ? [] : [value];
10963
+ const selectedFallback = inOpts ? [] : [effectiveVal];
10881
10964
  if (!inputVal || inputVal === selectedLabel) {
10882
10965
  return [
10883
10966
  ...selectedFallback,
@@ -10891,21 +10974,27 @@ function SmartSelect({
10891
10974
  return opts.filter(
10892
10975
  (opt) => getOptionLabel(opt).toLowerCase().includes(q) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q)
10893
10976
  ).slice(0, searchThreshold);
10894
- }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value, searchThreshold]);
10977
+ }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value, searchThreshold, strictSelection, strictValidation]);
10895
10978
  return /* @__PURE__ */ import_react54.default.createElement(
10896
10979
  Autocomplete,
10897
10980
  {
10898
10981
  options: displayOptions,
10899
- value: value ?? (multiple ? [] : null),
10982
+ value: autocompleteValue,
10900
10983
  onChange: handleChange,
10901
- inputValue: multiple ? isControlled ? inputValueProp : void 0 : activeInput,
10984
+ inputValue: autocompleteInputValue,
10902
10985
  onInputChange: handleInputChange,
10903
10986
  multiple,
10904
10987
  limitTags,
10905
10988
  loading,
10906
10989
  loadingText: loadingText ?? /* @__PURE__ */ import_react54.default.createElement("span", { style: { fontSize: "0.875rem", color: "var(--text-secondary)" } }, "Loading\u2026"),
10907
10990
  getOptionLabel,
10908
- isOptionEqualToValue: (opt, val) => getValue(opt) === getValue(val),
10991
+ isOptionEqualToValue: (opt, val) => {
10992
+ try {
10993
+ return getValue(opt) === getValue(val);
10994
+ } catch {
10995
+ return false;
10996
+ }
10997
+ },
10909
10998
  filterOptions: computedFilterOptions,
10910
10999
  renderOption: renderOptionProp ?? defaultRenderOption,
10911
11000
  label,
package/dist/main.d.cts CHANGED
@@ -2264,8 +2264,7 @@ interface SmartSelectProps<T = any> {
2264
2264
  onInputChange?: (value: string, reason: 'input' | 'reset' | 'clear') => void;
2265
2265
  /**
2266
2266
  * Called when local matches fall below `searchThreshold`.
2267
- * Receives the current query and how many more records are needed to fill the threshold.
2268
- * Use this to trigger an API / server search and update `searchResults`.
2267
+ * Receives the current query and how many more records are needed.
2269
2268
  */
2270
2269
  onSearchChange?: (query: string, needed: number) => void;
2271
2270
  /**
@@ -2275,14 +2274,14 @@ interface SmartSelectProps<T = any> {
2275
2274
  */
2276
2275
  searchResults?: T[];
2277
2276
  /**
2278
- * Debounce delay in ms before `onSearchChange` fires.
2279
- * Defaults to 300 ms. Pass 0 to disable debouncing.
2277
+ * Debounce delay in ms before `onSearchChange` fires. Default: 300.
2278
+ * Pass 0 to disable.
2280
2279
  */
2281
2280
  debounceMs?: number;
2282
2281
  /**
2283
2282
  * Max results to show when a query is active.
2284
- * If local matches are fewer than this, `onSearchChange` fires with how many more are needed.
2285
- * Defaults to 10. Pass 0 to always call the API.
2283
+ * `onSearchChange` fires only when local matches < this threshold.
2284
+ * Default: 10. Pass 0 to always call the API.
2286
2285
  */
2287
2286
  searchThreshold?: number;
2288
2287
  /** Primary display label for an option (required) */
@@ -2304,6 +2303,22 @@ interface SmartSelectProps<T = any> {
2304
2303
  * Only relevant when getOptionChildren is provided and multiple is true.
2305
2304
  */
2306
2305
  allowChildNodesSelection?: boolean;
2306
+ /**
2307
+ * When **true**, the incoming `value` is validated against the source options
2308
+ * before being used. Three cases are handled strictly:
2309
+ *
2310
+ * - **Case 1 — Empty**: `value` is `null`, `undefined`, or `""` → treated as no selection.
2311
+ * - **Case 2 — Missing**: value's key is not present in `options` or `searchResults`
2312
+ * → ignored; not injected into the dropdown list.
2313
+ * - **Case 3 — Incomplete**: value's key exists in the source but the canonical source
2314
+ * option returns an empty label from `getOptionLabel` → not treated as selected.
2315
+ *
2316
+ * When invalid, `null` is passed to Autocomplete (nothing selected) and the raw text
2317
+ * is shown in the input field. No `onChange` fires.
2318
+ *
2319
+ * Only applies to single-select mode. Default: **false**.
2320
+ */
2321
+ strictSelection?: boolean;
2307
2322
  /** Show a loading spinner in the dropdown */
2308
2323
  loading?: boolean;
2309
2324
  /** Content shown while loading */
@@ -2338,7 +2353,7 @@ interface SmartSelectProps<T = any> {
2338
2353
  style?: CSSProperties;
2339
2354
  sx?: SxProp;
2340
2355
  }
2341
- declare function SmartSelect<T = any>({ options, value, onChange, inputValue: inputValueProp, onInputChange, onSearchChange, searchResults, debounceMs, searchThreshold, getOptionLabel, getOptionValue, getOptionSubLabel, getOptionChildren, multiple, allowChildNodesSelection, loading, loadingText, filterOptions: filterOptionsProp, renderOption: renderOptionProp, limitTags, label, placeholder, variant, size, disabled, error, helperText, fullWidth, required, className, style, sx, }: SmartSelectProps<T>): React__default.JSX.Element;
2356
+ declare function SmartSelect<T = any>({ options, value, onChange, inputValue: inputValueProp, onInputChange, onSearchChange, searchResults, debounceMs, searchThreshold, getOptionLabel, getOptionValue, getOptionSubLabel, getOptionChildren, multiple, allowChildNodesSelection, strictSelection, loading, loadingText, filterOptions: filterOptionsProp, renderOption: renderOptionProp, limitTags, label, placeholder, variant, size, disabled, error, helperText, fullWidth, required, className, style, sx, }: SmartSelectProps<T>): React__default.JSX.Element;
2342
2357
 
2343
2358
  type ToolbarButton = 'undo' | 'redo' | 'ai' | 'paragraph' | 'fontsize' | 'font' | 'color' | 'bold' | 'italic' | 'strike' | 'link' | 'lineheight' | 'ul' | 'ol' | 'align' | 'indent' | 'outdent' | 'table' | 'image' | 'video' | 'cut' | 'copy' | 'paste' | 'specialchars' | 'code' | 'fullscreen' | 'tts' | 'stt' | 'translate' | 'todo' | '|';
2344
2359
  type EditorVariant = 'default' | 'basic';
package/dist/main.d.ts CHANGED
@@ -2264,8 +2264,7 @@ interface SmartSelectProps<T = any> {
2264
2264
  onInputChange?: (value: string, reason: 'input' | 'reset' | 'clear') => void;
2265
2265
  /**
2266
2266
  * Called when local matches fall below `searchThreshold`.
2267
- * Receives the current query and how many more records are needed to fill the threshold.
2268
- * Use this to trigger an API / server search and update `searchResults`.
2267
+ * Receives the current query and how many more records are needed.
2269
2268
  */
2270
2269
  onSearchChange?: (query: string, needed: number) => void;
2271
2270
  /**
@@ -2275,14 +2274,14 @@ interface SmartSelectProps<T = any> {
2275
2274
  */
2276
2275
  searchResults?: T[];
2277
2276
  /**
2278
- * Debounce delay in ms before `onSearchChange` fires.
2279
- * Defaults to 300 ms. Pass 0 to disable debouncing.
2277
+ * Debounce delay in ms before `onSearchChange` fires. Default: 300.
2278
+ * Pass 0 to disable.
2280
2279
  */
2281
2280
  debounceMs?: number;
2282
2281
  /**
2283
2282
  * Max results to show when a query is active.
2284
- * If local matches are fewer than this, `onSearchChange` fires with how many more are needed.
2285
- * Defaults to 10. Pass 0 to always call the API.
2283
+ * `onSearchChange` fires only when local matches < this threshold.
2284
+ * Default: 10. Pass 0 to always call the API.
2286
2285
  */
2287
2286
  searchThreshold?: number;
2288
2287
  /** Primary display label for an option (required) */
@@ -2304,6 +2303,22 @@ interface SmartSelectProps<T = any> {
2304
2303
  * Only relevant when getOptionChildren is provided and multiple is true.
2305
2304
  */
2306
2305
  allowChildNodesSelection?: boolean;
2306
+ /**
2307
+ * When **true**, the incoming `value` is validated against the source options
2308
+ * before being used. Three cases are handled strictly:
2309
+ *
2310
+ * - **Case 1 — Empty**: `value` is `null`, `undefined`, or `""` → treated as no selection.
2311
+ * - **Case 2 — Missing**: value's key is not present in `options` or `searchResults`
2312
+ * → ignored; not injected into the dropdown list.
2313
+ * - **Case 3 — Incomplete**: value's key exists in the source but the canonical source
2314
+ * option returns an empty label from `getOptionLabel` → not treated as selected.
2315
+ *
2316
+ * When invalid, `null` is passed to Autocomplete (nothing selected) and the raw text
2317
+ * is shown in the input field. No `onChange` fires.
2318
+ *
2319
+ * Only applies to single-select mode. Default: **false**.
2320
+ */
2321
+ strictSelection?: boolean;
2307
2322
  /** Show a loading spinner in the dropdown */
2308
2323
  loading?: boolean;
2309
2324
  /** Content shown while loading */
@@ -2338,7 +2353,7 @@ interface SmartSelectProps<T = any> {
2338
2353
  style?: CSSProperties;
2339
2354
  sx?: SxProp;
2340
2355
  }
2341
- declare function SmartSelect<T = any>({ options, value, onChange, inputValue: inputValueProp, onInputChange, onSearchChange, searchResults, debounceMs, searchThreshold, getOptionLabel, getOptionValue, getOptionSubLabel, getOptionChildren, multiple, allowChildNodesSelection, loading, loadingText, filterOptions: filterOptionsProp, renderOption: renderOptionProp, limitTags, label, placeholder, variant, size, disabled, error, helperText, fullWidth, required, className, style, sx, }: SmartSelectProps<T>): React__default.JSX.Element;
2356
+ declare function SmartSelect<T = any>({ options, value, onChange, inputValue: inputValueProp, onInputChange, onSearchChange, searchResults, debounceMs, searchThreshold, getOptionLabel, getOptionValue, getOptionSubLabel, getOptionChildren, multiple, allowChildNodesSelection, strictSelection, loading, loadingText, filterOptions: filterOptionsProp, renderOption: renderOptionProp, limitTags, label, placeholder, variant, size, disabled, error, helperText, fullWidth, required, className, style, sx, }: SmartSelectProps<T>): React__default.JSX.Element;
2342
2357
 
2343
2358
  type ToolbarButton = 'undo' | 'redo' | 'ai' | 'paragraph' | 'fontsize' | 'font' | 'color' | 'bold' | 'italic' | 'strike' | 'link' | 'lineheight' | 'ul' | 'ol' | 'align' | 'indent' | 'outdent' | 'table' | 'image' | 'video' | 'cut' | 'copy' | 'paste' | 'specialchars' | 'code' | 'fullscreen' | 'tts' | 'stt' | 'translate' | 'todo' | '|';
2344
2359
  type EditorVariant = 'default' | 'basic';
package/dist/main.js CHANGED
@@ -1209,14 +1209,14 @@ var SubmitButton = ({
1209
1209
  setLoading(false);
1210
1210
  }
1211
1211
  };
1212
- const handleClick = (e) => runProtected(e, onClick);
1213
- const handleDoubleClick = (e) => runProtected(e, onDoubleClick);
1212
+ const handleClick = onClick ? (e) => runProtected(e, onClick) : void 0;
1213
+ const handleDoubleClick = onDoubleClick ? (e) => runProtected(e, onDoubleClick) : void 0;
1214
1214
  const showLoader = loading || isLoading;
1215
1215
  return /* @__PURE__ */ React60.createElement(
1216
1216
  "button",
1217
1217
  {
1218
1218
  ...props,
1219
- type: onClick ? void 0 : type,
1219
+ type,
1220
1220
  className: ["btn submit-btn", bgGradiant ? "submit-btn-gradiant" : "", sxClass, className].filter(Boolean).join(" "),
1221
1221
  disabled: props.disabled || showLoader,
1222
1222
  onClick: handleClick,
@@ -10548,6 +10548,14 @@ function buildLookup(options, getChildren, getValue, map = /* @__PURE__ */ new M
10548
10548
  }
10549
10549
  return map;
10550
10550
  }
10551
+ function safeLabel(getOptionLabel, option) {
10552
+ try {
10553
+ const l = getOptionLabel(option);
10554
+ return typeof l === "string" ? l : "";
10555
+ } catch {
10556
+ return "";
10557
+ }
10558
+ }
10551
10559
  function SmartSelect({
10552
10560
  options,
10553
10561
  value,
@@ -10564,6 +10572,7 @@ function SmartSelect({
10564
10572
  getOptionChildren,
10565
10573
  multiple = false,
10566
10574
  allowChildNodesSelection = true,
10575
+ strictSelection = false,
10567
10576
  loading = false,
10568
10577
  loadingText,
10569
10578
  filterOptions: filterOptionsProp,
@@ -10587,15 +10596,6 @@ function SmartSelect({
10587
10596
  useEffect21(() => () => {
10588
10597
  if (debounceTimer.current) clearTimeout(debounceTimer.current);
10589
10598
  }, []);
10590
- const [internalInput, setInternalInput] = useState27(
10591
- () => !multiple && value != null ? getOptionLabel(value) : ""
10592
- );
10593
- useEffect21(() => {
10594
- if (!isControlled && !multiple) {
10595
- setInternalInput(value != null ? getOptionLabel(value) : "");
10596
- }
10597
- }, [value, multiple, getOptionLabel, isControlled]);
10598
- const activeInput = isControlled ? inputValueProp : internalInput;
10599
10599
  const getValue = useCallback12(
10600
10600
  (o) => getOptionValue ? getOptionValue(o) : String(getOptionLabel(o)),
10601
10601
  [getOptionValue, getOptionLabel]
@@ -10605,6 +10605,55 @@ function SmartSelect({
10605
10605
  return flattenTree(options, getOptionChildren);
10606
10606
  }, [options, getOptionChildren]);
10607
10607
  const flatOptionsList = useMemo3(() => flatItems.map((f) => f.option), [flatItems]);
10608
+ const strictValidation = useMemo3(() => {
10609
+ if (!strictSelection || multiple) return { valid: true, canonicalValue: value };
10610
+ if (value == null || value === "") {
10611
+ return { valid: false, displayText: "" };
10612
+ }
10613
+ const invalidDisplayText = (v) => {
10614
+ const fromLabel = safeLabel(getOptionLabel, v).trim();
10615
+ if (fromLabel) return fromLabel;
10616
+ if (typeof v === "string" || typeof v === "number") return String(v);
10617
+ return "";
10618
+ };
10619
+ try {
10620
+ const key = getValue(value);
10621
+ const canonicalOpt = flatOptionsList.find((o) => getValue(o) === key) ?? searchResults.find((o) => getValue(o) === key);
10622
+ if (!canonicalOpt) {
10623
+ return { valid: false, displayText: invalidDisplayText(value) };
10624
+ }
10625
+ if (typeof canonicalOpt === "object" && canonicalOpt !== null && typeof value === "object" && value !== null) {
10626
+ const src = canonicalOpt;
10627
+ const inc = value;
10628
+ const fullMatch = Object.keys(src).every((k) => k in inc && inc[k] === src[k]);
10629
+ if (!fullMatch) {
10630
+ return { valid: false, displayText: invalidDisplayText(value) };
10631
+ }
10632
+ }
10633
+ return { valid: true, canonicalValue: canonicalOpt };
10634
+ } catch {
10635
+ return { valid: false, displayText: invalidDisplayText(value) };
10636
+ }
10637
+ }, [strictSelection, multiple, value, getValue, flatOptionsList, searchResults, getOptionLabel]);
10638
+ const [internalInput, setInternalInput] = useState27(
10639
+ () => !multiple && value != null ? getOptionLabel(value) : ""
10640
+ );
10641
+ const strictValidationRef = useRef25(strictValidation);
10642
+ strictValidationRef.current = strictValidation;
10643
+ const lastSeededValue = useRef25(/* @__PURE__ */ Symbol("init"));
10644
+ useEffect21(() => {
10645
+ if (isControlled || multiple) return;
10646
+ if (strictSelection && !strictValidationRef.current.valid) {
10647
+ if (Object.is(value, lastSeededValue.current)) return;
10648
+ lastSeededValue.current = value;
10649
+ const sv = strictValidationRef.current;
10650
+ setInternalInput(sv.displayText);
10651
+ return;
10652
+ }
10653
+ lastSeededValue.current = value;
10654
+ setInternalInput(value != null ? getOptionLabel(value) : "");
10655
+ }, [value, multiple, getOptionLabel, isControlled, strictSelection]);
10656
+ const activeInput = isControlled ? inputValueProp : internalInput;
10608
10657
  const displayOptions = useMemo3(() => {
10609
10658
  let base = flatOptionsList;
10610
10659
  if (searchResults.length) {
@@ -10612,17 +10661,26 @@ function SmartSelect({
10612
10661
  const serverOnly = searchResults.filter((o) => !localKeys.has(getValue(o)));
10613
10662
  base = [...flatOptionsList, ...serverOnly];
10614
10663
  }
10615
- if (!multiple && value != null) {
10616
- const key = getValue(value);
10617
- if (!base.some((o) => getValue(o) === key)) base = [value, ...base];
10618
- }
10619
- if (multiple && Array.isArray(value) && value.length > 0) {
10664
+ if (!multiple) {
10665
+ if (strictSelection) {
10666
+ const injectValue = strictValidation.valid ? strictValidation.canonicalValue : null;
10667
+ if (injectValue != null) {
10668
+ const key = getValue(injectValue);
10669
+ if (!base.some((o) => getValue(o) === key)) base = [injectValue, ...base];
10670
+ }
10671
+ } else {
10672
+ if (value != null) {
10673
+ const key = getValue(value);
10674
+ if (!base.some((o) => getValue(o) === key)) base = [value, ...base];
10675
+ }
10676
+ }
10677
+ } else if (Array.isArray(value) && value.length > 0) {
10620
10678
  const baseKeys = new Set(base.map((o) => getValue(o)));
10621
10679
  const missing = value.filter((v) => !baseKeys.has(getValue(v)));
10622
10680
  if (missing.length > 0) base = [...base, ...missing];
10623
10681
  }
10624
10682
  return base;
10625
- }, [flatOptionsList, searchResults, getValue, value, multiple]);
10683
+ }, [flatOptionsList, searchResults, getValue, value, multiple, strictSelection, strictValidation]);
10626
10684
  const depthMap = useMemo3(() => {
10627
10685
  const map = /* @__PURE__ */ new Map();
10628
10686
  flatItems.forEach(({ option, depth }) => map.set(getValue(option), depth));
@@ -10636,13 +10694,31 @@ function SmartSelect({
10636
10694
  if (multiple) {
10637
10695
  return new Set((Array.isArray(value) ? value : []).map((v) => getValue(v)));
10638
10696
  }
10697
+ if (strictSelection) {
10698
+ const sv = strictValidation;
10699
+ if (!strictValidation.valid || sv.canonicalValue == null) return /* @__PURE__ */ new Set();
10700
+ return /* @__PURE__ */ new Set([getValue(sv.canonicalValue)]);
10701
+ }
10639
10702
  return value != null ? /* @__PURE__ */ new Set([getValue(value)]) : /* @__PURE__ */ new Set();
10640
- }, [multiple, value, getValue]);
10703
+ }, [multiple, value, getValue, strictSelection, strictValidation]);
10704
+ let autocompleteValue;
10705
+ let autocompleteInputValue;
10706
+ if (strictSelection && !multiple) {
10707
+ const sv = strictValidation;
10708
+ if (!strictValidation.valid) {
10709
+ autocompleteValue = null;
10710
+ autocompleteInputValue = isControlled ? inputValueProp : internalInput;
10711
+ } else {
10712
+ autocompleteValue = sv.canonicalValue ?? null;
10713
+ autocompleteInputValue = isControlled ? inputValueProp : activeInput;
10714
+ }
10715
+ } else {
10716
+ autocompleteValue = value ?? (multiple ? [] : null);
10717
+ autocompleteInputValue = multiple ? isControlled ? inputValueProp : void 0 : activeInput;
10718
+ }
10641
10719
  const handleInputChange = useCallback12((_, val, reason) => {
10642
10720
  const resolvedReason = reason ?? "input";
10643
- if (!isControlled) {
10644
- setInternalInput(val);
10645
- }
10721
+ if (!isControlled) setInternalInput(val);
10646
10722
  onInputChange?.(val, resolvedReason);
10647
10723
  if (!onSearchChange) return;
10648
10724
  if (debounceTimer.current) clearTimeout(debounceTimer.current);
@@ -10664,9 +10740,7 @@ function SmartSelect({
10664
10740
  if (debounceMs <= 0) {
10665
10741
  onSearchChange(val, needed);
10666
10742
  } else {
10667
- debounceTimer.current = setTimeout(() => {
10668
- onSearchChange(val, needed);
10669
- }, debounceMs);
10743
+ debounceTimer.current = setTimeout(() => onSearchChange(val, needed), debounceMs);
10670
10744
  }
10671
10745
  }, [isControlled, onInputChange, onSearchChange, debounceMs, searchThreshold, flatOptionsList, getOptionLabel, getOptionSubLabel]);
10672
10746
  const handleChange = useCallback12((_, newValue) => {
@@ -10746,6 +10820,14 @@ function SmartSelect({
10746
10820
  }, [depthMap, getValue, getOptionLabel, getOptionSubLabel, selectedKeys]);
10747
10821
  const computedFilterOptions = useCallback12((opts, inputVal) => {
10748
10822
  if (filterOptionsProp) return filterOptionsProp(opts, inputVal);
10823
+ if (strictSelection && !strictValidation.valid) {
10824
+ const sv = strictValidation;
10825
+ if (!inputVal || inputVal === sv.displayText) return opts;
10826
+ const q2 = inputVal.toLowerCase();
10827
+ return opts.filter(
10828
+ (opt) => getOptionLabel(opt).toLowerCase().includes(q2) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q2)
10829
+ ).slice(0, searchThreshold);
10830
+ }
10749
10831
  if (multiple) {
10750
10832
  const selected = opts.filter((o) => selectedKeys.has(getValue(o)));
10751
10833
  const unselected = opts.filter((o) => !selectedKeys.has(getValue(o)));
@@ -10756,11 +10838,12 @@ function SmartSelect({
10756
10838
  ).slice(0, searchThreshold);
10757
10839
  return [...selected, ...filteredUnselected];
10758
10840
  }
10759
- if (value != null) {
10760
- const selectedKey = getValue(value);
10761
- const selectedLabel = getOptionLabel(value);
10841
+ const effectiveVal = strictSelection ? strictValidation.canonicalValue : value;
10842
+ if (effectiveVal != null) {
10843
+ const selectedKey = getValue(effectiveVal);
10844
+ const selectedLabel = getOptionLabel(effectiveVal);
10762
10845
  const inOpts = opts.some((o) => getValue(o) === selectedKey);
10763
- const selectedFallback = inOpts ? [] : [value];
10846
+ const selectedFallback = inOpts ? [] : [effectiveVal];
10764
10847
  if (!inputVal || inputVal === selectedLabel) {
10765
10848
  return [
10766
10849
  ...selectedFallback,
@@ -10774,21 +10857,27 @@ function SmartSelect({
10774
10857
  return opts.filter(
10775
10858
  (opt) => getOptionLabel(opt).toLowerCase().includes(q) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q)
10776
10859
  ).slice(0, searchThreshold);
10777
- }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value, searchThreshold]);
10860
+ }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value, searchThreshold, strictSelection, strictValidation]);
10778
10861
  return /* @__PURE__ */ React109.createElement(
10779
10862
  Autocomplete,
10780
10863
  {
10781
10864
  options: displayOptions,
10782
- value: value ?? (multiple ? [] : null),
10865
+ value: autocompleteValue,
10783
10866
  onChange: handleChange,
10784
- inputValue: multiple ? isControlled ? inputValueProp : void 0 : activeInput,
10867
+ inputValue: autocompleteInputValue,
10785
10868
  onInputChange: handleInputChange,
10786
10869
  multiple,
10787
10870
  limitTags,
10788
10871
  loading,
10789
10872
  loadingText: loadingText ?? /* @__PURE__ */ React109.createElement("span", { style: { fontSize: "0.875rem", color: "var(--text-secondary)" } }, "Loading\u2026"),
10790
10873
  getOptionLabel,
10791
- isOptionEqualToValue: (opt, val) => getValue(opt) === getValue(val),
10874
+ isOptionEqualToValue: (opt, val) => {
10875
+ try {
10876
+ return getValue(opt) === getValue(val);
10877
+ } catch {
10878
+ return false;
10879
+ }
10880
+ },
10792
10881
  filterOptions: computedFilterOptions,
10793
10882
  renderOption: renderOptionProp ?? defaultRenderOption,
10794
10883
  label,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rufous/ui",
3
3
  "private": false,
4
- "version": "0.3.40",
4
+ "version": "0.3.41",
5
5
  "type": "module",
6
6
  "description": "Experimental: A lightweight React UI component library (Beta)",
7
7
  "style": "./dist/main.css",