@rufous/ui 0.3.20 → 0.3.21

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
@@ -4773,6 +4773,7 @@ function DataGrid({
4773
4773
  const menuRef = (0, import_react23.useRef)(null);
4774
4774
  const [showManageColumns, setShowManageColumns] = (0, import_react23.useState)(false);
4775
4775
  const [showAdvancedFilter, setShowAdvancedFilter] = (0, import_react23.useState)(false);
4776
+ const [focusFilterIdx, setFocusFilterIdx] = (0, import_react23.useState)(-1);
4776
4777
  const filterableColumnsProp = initialColumnsProp.filter((c) => c.filterable !== false);
4777
4778
  const initialFilterCol = String(filterableColumnsProp[0]?.field || filterableColumnsProp[0]?.key || "");
4778
4779
  const [advancedFilters, setAdvancedFilters] = (0, import_react23.useState)([
@@ -4821,9 +4822,13 @@ function DataGrid({
4821
4822
  return next;
4822
4823
  });
4823
4824
  }, [initialColumnsProp]);
4825
+ const onFiltersChangeRef = (0, import_react23.useRef)(onFiltersChange);
4824
4826
  (0, import_react23.useEffect)(() => {
4825
- onFiltersChange?.(advancedFilters);
4826
- }, [advancedFilters, onFiltersChange]);
4827
+ onFiltersChangeRef.current = onFiltersChange;
4828
+ });
4829
+ (0, import_react23.useEffect)(() => {
4830
+ onFiltersChangeRef.current?.(advancedFilters);
4831
+ }, [advancedFilters]);
4827
4832
  const handleSort = (fieldKey, dir) => {
4828
4833
  if (dir !== void 0) {
4829
4834
  setSortField(fieldKey);
@@ -5092,6 +5097,7 @@ function DataGrid({
5092
5097
  if (!firstCol) return prev;
5093
5098
  return [{ column: String(firstCol.field), operator: getDefaultOperator(firstCol.type), value: "", logic: "AND" }];
5094
5099
  });
5100
+ setFocusFilterIdx(-1);
5095
5101
  setShowAdvancedFilter(false);
5096
5102
  };
5097
5103
  const activeMenuCol = activeMenu ? resolvedColumns.find((c) => String(c.field) === activeMenu) : null;
@@ -5143,7 +5149,18 @@ function DataGrid({
5143
5149
  },
5144
5150
  col.headerName,
5145
5151
  col.sortable !== false && /* @__PURE__ */ import_react23.default.createElement("span", { className: `dg-sort-icon${isSorted ? " dg-sort-icon--active" : ""}` }, isSorted && sortDirection === "asc" && /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.ChevronUp, { size: 14 }), isSorted && sortDirection === "desc" && /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.ChevronDown, { size: 14 }), !isSorted && /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.ChevronsUpDown, { size: 14 }))
5146
- ), /* @__PURE__ */ import_react23.default.createElement("div", { className: `dg-th-actions${isFiltered ? " dg-th-actions--filtered" : ""}` }, isFiltered && /* @__PURE__ */ import_react23.default.createElement("button", { className: "dg-th-filter-btn", onClick: () => setShowAdvancedFilter(true) }, /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.Filter, { size: 11 })), !col.disableColumnMenu && /* @__PURE__ */ import_react23.default.createElement(
5152
+ ), /* @__PURE__ */ import_react23.default.createElement("div", { className: `dg-th-actions${isFiltered ? " dg-th-actions--filtered" : ""}` }, isFiltered && /* @__PURE__ */ import_react23.default.createElement(
5153
+ "button",
5154
+ {
5155
+ className: "dg-th-filter-btn",
5156
+ onClick: () => {
5157
+ const idx2 = advancedFilters.findIndex((f) => f.column === colField);
5158
+ setFocusFilterIdx(idx2);
5159
+ setShowAdvancedFilter(true);
5160
+ }
5161
+ },
5162
+ /* @__PURE__ */ import_react23.default.createElement(import_lucide_react2.Filter, { size: 11 })
5163
+ ), !col.disableColumnMenu && /* @__PURE__ */ import_react23.default.createElement(
5147
5164
  "button",
5148
5165
  {
5149
5166
  className: "dg-th-menu-btn",
@@ -5372,7 +5389,7 @@ function DataGrid({
5372
5389
  className: "dg-filter-input",
5373
5390
  placeholder: "Value\u2026",
5374
5391
  value: f.value,
5375
- autoFocus: idx === advancedFilters.length - 1,
5392
+ autoFocus: focusFilterIdx >= 0 ? idx === focusFilterIdx : idx === advancedFilters.length - 1,
5376
5393
  onChange: (e) => setAdvancedFilters((p) => p.map((fi, i) => i === idx ? { ...fi, value: e.target.value } : fi))
5377
5394
  }
5378
5395
  ));
@@ -9550,7 +9567,9 @@ function SmartSelect({
9550
9567
  value,
9551
9568
  onChange,
9552
9569
  onSearchChange,
9570
+ searchResults = [],
9553
9571
  debounceMs = 300,
9572
+ searchThreshold = 10,
9554
9573
  getOptionLabel,
9555
9574
  getOptionValue,
9556
9575
  getOptionSubLabel,
@@ -9588,14 +9607,20 @@ function SmartSelect({
9588
9607
  return flattenTree(options, getOptionChildren);
9589
9608
  }, [options, getOptionChildren]);
9590
9609
  const flatOptionsList = (0, import_react51.useMemo)(() => flatItems.map((f) => f.option), [flatItems]);
9610
+ const displayOptions = (0, import_react51.useMemo)(() => {
9611
+ if (!searchResults.length) return flatOptionsList;
9612
+ const localKeys = new Set(flatOptionsList.map((o) => getValue(o)));
9613
+ const serverOnly = searchResults.filter((o) => !localKeys.has(getValue(o)));
9614
+ return [...flatOptionsList, ...serverOnly];
9615
+ }, [flatOptionsList, searchResults, getValue]);
9591
9616
  const depthMap = (0, import_react51.useMemo)(() => {
9592
9617
  const map = /* @__PURE__ */ new Map();
9593
9618
  flatItems.forEach(({ option, depth }) => map.set(getValue(option), depth));
9594
9619
  return map;
9595
9620
  }, [flatItems, getValue]);
9596
9621
  const lookup = (0, import_react51.useMemo)(
9597
- () => buildLookup(options, getOptionChildren, getValue),
9598
- [options, getOptionChildren, getValue]
9622
+ () => buildLookup(displayOptions, getOptionChildren, getValue),
9623
+ [displayOptions, getOptionChildren, getValue]
9599
9624
  );
9600
9625
  const selectedKeys = (0, import_react51.useMemo)(() => {
9601
9626
  if (multiple) {
@@ -9607,21 +9632,27 @@ function SmartSelect({
9607
9632
  if (!onSearchChange) return;
9608
9633
  if (debounceTimer.current) clearTimeout(debounceTimer.current);
9609
9634
  if (!inputValue) {
9610
- onSearchChange("");
9635
+ onSearchChange("", 0);
9611
9636
  return;
9612
9637
  }
9613
- const hasLocalMatch = flatOptionsList.some(
9614
- (opt) => getOptionLabel(opt).toLowerCase().includes(inputValue.toLowerCase()) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(inputValue.toLowerCase())
9615
- );
9616
- if (hasLocalMatch) return;
9638
+ const q = inputValue.toLowerCase();
9639
+ let localCount = 0;
9640
+ for (const opt of flatOptionsList) {
9641
+ if (getOptionLabel(opt).toLowerCase().includes(q) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q)) {
9642
+ localCount++;
9643
+ if (localCount >= searchThreshold) break;
9644
+ }
9645
+ }
9646
+ if (localCount >= searchThreshold) return;
9647
+ const needed = searchThreshold - localCount;
9617
9648
  if (debounceMs <= 0) {
9618
- onSearchChange(inputValue);
9649
+ onSearchChange(inputValue, needed);
9619
9650
  } else {
9620
9651
  debounceTimer.current = setTimeout(() => {
9621
- onSearchChange(inputValue);
9652
+ onSearchChange(inputValue, needed);
9622
9653
  }, debounceMs);
9623
9654
  }
9624
- }, [onSearchChange, debounceMs, flatOptionsList, getOptionLabel, getOptionSubLabel]);
9655
+ }, [onSearchChange, debounceMs, searchThreshold, flatOptionsList, getOptionLabel, getOptionSubLabel]);
9625
9656
  const handleChange = (0, import_react51.useCallback)((_, newValue) => {
9626
9657
  if (!multiple || !allowChildNodesSelection || !getOptionChildren) {
9627
9658
  onChange?.(newValue);
@@ -9708,10 +9739,12 @@ function SmartSelect({
9708
9739
  if (multiple) {
9709
9740
  const selected = opts.filter((o) => selectedKeys.has(getValue(o)));
9710
9741
  const unselected = opts.filter((o) => !selectedKeys.has(getValue(o)));
9711
- const filteredRest = inputValue ? unselected.filter(
9712
- (opt) => getOptionLabel(opt).toLowerCase().includes(inputValue.toLowerCase()) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(inputValue.toLowerCase())
9713
- ) : unselected;
9714
- return [...selected, ...filteredRest];
9742
+ if (!inputValue) return [...selected, ...unselected];
9743
+ const q2 = inputValue.toLowerCase();
9744
+ const filteredUnselected = unselected.filter(
9745
+ (opt) => getOptionLabel(opt).toLowerCase().includes(q2) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q2)
9746
+ ).slice(0, searchThreshold);
9747
+ return [...selected, ...filteredUnselected];
9715
9748
  }
9716
9749
  if (value != null) {
9717
9750
  const selectedLabel = getOptionLabel(value);
@@ -9727,12 +9760,12 @@ function SmartSelect({
9727
9760
  const q = inputValue.toLowerCase();
9728
9761
  return opts.filter(
9729
9762
  (opt) => getOptionLabel(opt).toLowerCase().includes(q) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q)
9730
- );
9731
- }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value]);
9763
+ ).slice(0, searchThreshold);
9764
+ }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value, searchThreshold]);
9732
9765
  return /* @__PURE__ */ import_react51.default.createElement(
9733
9766
  Autocomplete,
9734
9767
  {
9735
- options: flatOptionsList,
9768
+ options: displayOptions,
9736
9769
  value: value ?? (multiple ? [] : null),
9737
9770
  onChange: handleChange,
9738
9771
  onInputChange: handleInputChange,
package/dist/main.d.cts CHANGED
@@ -1909,15 +1909,28 @@ interface SmartSelectProps<T = any> {
1909
1909
  /** Called when selection changes */
1910
1910
  onChange?: (value: T | T[] | null) => void;
1911
1911
  /**
1912
- * Called when the typed text finds no local match —
1913
- * use this to trigger an API / server search.
1912
+ * Called when local matches fall below `searchThreshold`.
1913
+ * Receives the current query and how many more records are needed to fill the threshold.
1914
+ * Use this to trigger an API / server search and update `searchResults`.
1914
1915
  */
1915
- onSearchChange?: (query: string) => void;
1916
+ onSearchChange?: (query: string, needed: number) => void;
1917
+ /**
1918
+ * Results returned by the API for the current query.
1919
+ * Merged with local matches (duplicates excluded) to fill up to `searchThreshold`.
1920
+ * Reset this to [] when the query is cleared.
1921
+ */
1922
+ searchResults?: T[];
1916
1923
  /**
1917
1924
  * Debounce delay in ms before `onSearchChange` fires.
1918
1925
  * Defaults to 300 ms. Pass 0 to disable debouncing.
1919
1926
  */
1920
1927
  debounceMs?: number;
1928
+ /**
1929
+ * Max results to show when a query is active.
1930
+ * If local matches are fewer than this, `onSearchChange` fires with how many more are needed.
1931
+ * Defaults to 10. Pass 0 to always call the API.
1932
+ */
1933
+ searchThreshold?: number;
1921
1934
  /** Primary display label for an option (required) */
1922
1935
  getOptionLabel: (option: T) => string;
1923
1936
  /** Unique key for an option — defaults to the label string */
@@ -1971,7 +1984,7 @@ interface SmartSelectProps<T = any> {
1971
1984
  style?: CSSProperties;
1972
1985
  sx?: SxProp;
1973
1986
  }
1974
- declare function SmartSelect<T = any>({ options, value, onChange, onSearchChange, debounceMs, 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;
1987
+ declare function SmartSelect<T = any>({ options, value, onChange, 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;
1975
1988
 
1976
1989
  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' | '|';
1977
1990
  type EditorVariant = 'default' | 'basic';
package/dist/main.d.ts CHANGED
@@ -1909,15 +1909,28 @@ interface SmartSelectProps<T = any> {
1909
1909
  /** Called when selection changes */
1910
1910
  onChange?: (value: T | T[] | null) => void;
1911
1911
  /**
1912
- * Called when the typed text finds no local match —
1913
- * use this to trigger an API / server search.
1912
+ * Called when local matches fall below `searchThreshold`.
1913
+ * Receives the current query and how many more records are needed to fill the threshold.
1914
+ * Use this to trigger an API / server search and update `searchResults`.
1914
1915
  */
1915
- onSearchChange?: (query: string) => void;
1916
+ onSearchChange?: (query: string, needed: number) => void;
1917
+ /**
1918
+ * Results returned by the API for the current query.
1919
+ * Merged with local matches (duplicates excluded) to fill up to `searchThreshold`.
1920
+ * Reset this to [] when the query is cleared.
1921
+ */
1922
+ searchResults?: T[];
1916
1923
  /**
1917
1924
  * Debounce delay in ms before `onSearchChange` fires.
1918
1925
  * Defaults to 300 ms. Pass 0 to disable debouncing.
1919
1926
  */
1920
1927
  debounceMs?: number;
1928
+ /**
1929
+ * Max results to show when a query is active.
1930
+ * If local matches are fewer than this, `onSearchChange` fires with how many more are needed.
1931
+ * Defaults to 10. Pass 0 to always call the API.
1932
+ */
1933
+ searchThreshold?: number;
1921
1934
  /** Primary display label for an option (required) */
1922
1935
  getOptionLabel: (option: T) => string;
1923
1936
  /** Unique key for an option — defaults to the label string */
@@ -1971,7 +1984,7 @@ interface SmartSelectProps<T = any> {
1971
1984
  style?: CSSProperties;
1972
1985
  sx?: SxProp;
1973
1986
  }
1974
- declare function SmartSelect<T = any>({ options, value, onChange, onSearchChange, debounceMs, 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;
1987
+ declare function SmartSelect<T = any>({ options, value, onChange, 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;
1975
1988
 
1976
1989
  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' | '|';
1977
1990
  type EditorVariant = 'default' | 'basic';
package/dist/main.js CHANGED
@@ -4628,6 +4628,7 @@ function DataGrid({
4628
4628
  const menuRef = useRef10(null);
4629
4629
  const [showManageColumns, setShowManageColumns] = useState9(false);
4630
4630
  const [showAdvancedFilter, setShowAdvancedFilter] = useState9(false);
4631
+ const [focusFilterIdx, setFocusFilterIdx] = useState9(-1);
4631
4632
  const filterableColumnsProp = initialColumnsProp.filter((c) => c.filterable !== false);
4632
4633
  const initialFilterCol = String(filterableColumnsProp[0]?.field || filterableColumnsProp[0]?.key || "");
4633
4634
  const [advancedFilters, setAdvancedFilters] = useState9([
@@ -4676,9 +4677,13 @@ function DataGrid({
4676
4677
  return next;
4677
4678
  });
4678
4679
  }, [initialColumnsProp]);
4680
+ const onFiltersChangeRef = useRef10(onFiltersChange);
4679
4681
  useEffect9(() => {
4680
- onFiltersChange?.(advancedFilters);
4681
- }, [advancedFilters, onFiltersChange]);
4682
+ onFiltersChangeRef.current = onFiltersChange;
4683
+ });
4684
+ useEffect9(() => {
4685
+ onFiltersChangeRef.current?.(advancedFilters);
4686
+ }, [advancedFilters]);
4682
4687
  const handleSort = (fieldKey, dir) => {
4683
4688
  if (dir !== void 0) {
4684
4689
  setSortField(fieldKey);
@@ -4947,6 +4952,7 @@ function DataGrid({
4947
4952
  if (!firstCol) return prev;
4948
4953
  return [{ column: String(firstCol.field), operator: getDefaultOperator(firstCol.type), value: "", logic: "AND" }];
4949
4954
  });
4955
+ setFocusFilterIdx(-1);
4950
4956
  setShowAdvancedFilter(false);
4951
4957
  };
4952
4958
  const activeMenuCol = activeMenu ? resolvedColumns.find((c) => String(c.field) === activeMenu) : null;
@@ -4998,7 +5004,18 @@ function DataGrid({
4998
5004
  },
4999
5005
  col.headerName,
5000
5006
  col.sortable !== false && /* @__PURE__ */ React75.createElement("span", { className: `dg-sort-icon${isSorted ? " dg-sort-icon--active" : ""}` }, isSorted && sortDirection === "asc" && /* @__PURE__ */ React75.createElement(ChevronUp, { size: 14 }), isSorted && sortDirection === "desc" && /* @__PURE__ */ React75.createElement(ChevronDown, { size: 14 }), !isSorted && /* @__PURE__ */ React75.createElement(ChevronsUpDown, { size: 14 }))
5001
- ), /* @__PURE__ */ React75.createElement("div", { className: `dg-th-actions${isFiltered ? " dg-th-actions--filtered" : ""}` }, isFiltered && /* @__PURE__ */ React75.createElement("button", { className: "dg-th-filter-btn", onClick: () => setShowAdvancedFilter(true) }, /* @__PURE__ */ React75.createElement(Filter, { size: 11 })), !col.disableColumnMenu && /* @__PURE__ */ React75.createElement(
5007
+ ), /* @__PURE__ */ React75.createElement("div", { className: `dg-th-actions${isFiltered ? " dg-th-actions--filtered" : ""}` }, isFiltered && /* @__PURE__ */ React75.createElement(
5008
+ "button",
5009
+ {
5010
+ className: "dg-th-filter-btn",
5011
+ onClick: () => {
5012
+ const idx2 = advancedFilters.findIndex((f) => f.column === colField);
5013
+ setFocusFilterIdx(idx2);
5014
+ setShowAdvancedFilter(true);
5015
+ }
5016
+ },
5017
+ /* @__PURE__ */ React75.createElement(Filter, { size: 11 })
5018
+ ), !col.disableColumnMenu && /* @__PURE__ */ React75.createElement(
5002
5019
  "button",
5003
5020
  {
5004
5021
  className: "dg-th-menu-btn",
@@ -5227,7 +5244,7 @@ function DataGrid({
5227
5244
  className: "dg-filter-input",
5228
5245
  placeholder: "Value\u2026",
5229
5246
  value: f.value,
5230
- autoFocus: idx === advancedFilters.length - 1,
5247
+ autoFocus: focusFilterIdx >= 0 ? idx === focusFilterIdx : idx === advancedFilters.length - 1,
5231
5248
  onChange: (e) => setAdvancedFilters((p) => p.map((fi, i) => i === idx ? { ...fi, value: e.target.value } : fi))
5232
5249
  }
5233
5250
  ));
@@ -9474,7 +9491,9 @@ function SmartSelect({
9474
9491
  value,
9475
9492
  onChange,
9476
9493
  onSearchChange,
9494
+ searchResults = [],
9477
9495
  debounceMs = 300,
9496
+ searchThreshold = 10,
9478
9497
  getOptionLabel,
9479
9498
  getOptionValue,
9480
9499
  getOptionSubLabel,
@@ -9512,14 +9531,20 @@ function SmartSelect({
9512
9531
  return flattenTree(options, getOptionChildren);
9513
9532
  }, [options, getOptionChildren]);
9514
9533
  const flatOptionsList = useMemo3(() => flatItems.map((f) => f.option), [flatItems]);
9534
+ const displayOptions = useMemo3(() => {
9535
+ if (!searchResults.length) return flatOptionsList;
9536
+ const localKeys = new Set(flatOptionsList.map((o) => getValue(o)));
9537
+ const serverOnly = searchResults.filter((o) => !localKeys.has(getValue(o)));
9538
+ return [...flatOptionsList, ...serverOnly];
9539
+ }, [flatOptionsList, searchResults, getValue]);
9515
9540
  const depthMap = useMemo3(() => {
9516
9541
  const map = /* @__PURE__ */ new Map();
9517
9542
  flatItems.forEach(({ option, depth }) => map.set(getValue(option), depth));
9518
9543
  return map;
9519
9544
  }, [flatItems, getValue]);
9520
9545
  const lookup = useMemo3(
9521
- () => buildLookup(options, getOptionChildren, getValue),
9522
- [options, getOptionChildren, getValue]
9546
+ () => buildLookup(displayOptions, getOptionChildren, getValue),
9547
+ [displayOptions, getOptionChildren, getValue]
9523
9548
  );
9524
9549
  const selectedKeys = useMemo3(() => {
9525
9550
  if (multiple) {
@@ -9531,21 +9556,27 @@ function SmartSelect({
9531
9556
  if (!onSearchChange) return;
9532
9557
  if (debounceTimer.current) clearTimeout(debounceTimer.current);
9533
9558
  if (!inputValue) {
9534
- onSearchChange("");
9559
+ onSearchChange("", 0);
9535
9560
  return;
9536
9561
  }
9537
- const hasLocalMatch = flatOptionsList.some(
9538
- (opt) => getOptionLabel(opt).toLowerCase().includes(inputValue.toLowerCase()) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(inputValue.toLowerCase())
9539
- );
9540
- if (hasLocalMatch) return;
9562
+ const q = inputValue.toLowerCase();
9563
+ let localCount = 0;
9564
+ for (const opt of flatOptionsList) {
9565
+ if (getOptionLabel(opt).toLowerCase().includes(q) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q)) {
9566
+ localCount++;
9567
+ if (localCount >= searchThreshold) break;
9568
+ }
9569
+ }
9570
+ if (localCount >= searchThreshold) return;
9571
+ const needed = searchThreshold - localCount;
9541
9572
  if (debounceMs <= 0) {
9542
- onSearchChange(inputValue);
9573
+ onSearchChange(inputValue, needed);
9543
9574
  } else {
9544
9575
  debounceTimer.current = setTimeout(() => {
9545
- onSearchChange(inputValue);
9576
+ onSearchChange(inputValue, needed);
9546
9577
  }, debounceMs);
9547
9578
  }
9548
- }, [onSearchChange, debounceMs, flatOptionsList, getOptionLabel, getOptionSubLabel]);
9579
+ }, [onSearchChange, debounceMs, searchThreshold, flatOptionsList, getOptionLabel, getOptionSubLabel]);
9549
9580
  const handleChange = useCallback11((_, newValue) => {
9550
9581
  if (!multiple || !allowChildNodesSelection || !getOptionChildren) {
9551
9582
  onChange?.(newValue);
@@ -9632,10 +9663,12 @@ function SmartSelect({
9632
9663
  if (multiple) {
9633
9664
  const selected = opts.filter((o) => selectedKeys.has(getValue(o)));
9634
9665
  const unselected = opts.filter((o) => !selectedKeys.has(getValue(o)));
9635
- const filteredRest = inputValue ? unselected.filter(
9636
- (opt) => getOptionLabel(opt).toLowerCase().includes(inputValue.toLowerCase()) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(inputValue.toLowerCase())
9637
- ) : unselected;
9638
- return [...selected, ...filteredRest];
9666
+ if (!inputValue) return [...selected, ...unselected];
9667
+ const q2 = inputValue.toLowerCase();
9668
+ const filteredUnselected = unselected.filter(
9669
+ (opt) => getOptionLabel(opt).toLowerCase().includes(q2) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q2)
9670
+ ).slice(0, searchThreshold);
9671
+ return [...selected, ...filteredUnselected];
9639
9672
  }
9640
9673
  if (value != null) {
9641
9674
  const selectedLabel = getOptionLabel(value);
@@ -9651,12 +9684,12 @@ function SmartSelect({
9651
9684
  const q = inputValue.toLowerCase();
9652
9685
  return opts.filter(
9653
9686
  (opt) => getOptionLabel(opt).toLowerCase().includes(q) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q)
9654
- );
9655
- }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value]);
9687
+ ).slice(0, searchThreshold);
9688
+ }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value, searchThreshold]);
9656
9689
  return /* @__PURE__ */ React108.createElement(
9657
9690
  Autocomplete,
9658
9691
  {
9659
- options: flatOptionsList,
9692
+ options: displayOptions,
9660
9693
  value: value ?? (multiple ? [] : null),
9661
9694
  onChange: handleChange,
9662
9695
  onInputChange: handleInputChange,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rufous/ui",
3
3
  "private": false,
4
- "version": "0.3.20",
4
+ "version": "0.3.21",
5
5
  "type": "module",
6
6
  "description": "Experimental: A lightweight React UI component library (Beta)",
7
7
  "style": "./dist/main.css",