@rufous/ui 0.3.20 → 0.3.22

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
  ));
@@ -9071,6 +9088,14 @@ function getTopLevelSelected(nodes, selected) {
9071
9088
  }
9072
9089
  return result;
9073
9090
  }
9091
+ function getAllSelected(nodes, selected) {
9092
+ const result = [];
9093
+ for (const node of nodes) {
9094
+ if (selected.has(String(node.id))) result.push(node);
9095
+ if (node.children?.length) result.push(...getAllSelected(node.children, selected));
9096
+ }
9097
+ return result;
9098
+ }
9074
9099
  function filterTree(nodes, query) {
9075
9100
  if (!query) return nodes;
9076
9101
  const q = query.toLowerCase();
@@ -9099,6 +9124,7 @@ function TreeNodeItem({
9099
9124
  selected,
9100
9125
  expanded,
9101
9126
  selectionMode,
9127
+ allowChildSelection,
9102
9128
  onToggleExpand,
9103
9129
  onSelect,
9104
9130
  isFiltering
@@ -9106,7 +9132,7 @@ function TreeNodeItem({
9106
9132
  const nodeId = String(node.id);
9107
9133
  const hasChildren = !!node.children?.length;
9108
9134
  const isExpanded = isFiltering || expanded.has(nodeId);
9109
- const state = getNodeState(node, selected);
9135
+ const state = allowChildSelection ? getNodeState(node, selected) : selected.has(nodeId) ? "checked" : "unchecked";
9110
9136
  return /* @__PURE__ */ import_react49.default.createElement("div", { className: "rf-tsn" }, /* @__PURE__ */ import_react49.default.createElement(
9111
9137
  "div",
9112
9138
  {
@@ -9126,16 +9152,8 @@ function TreeNodeItem({
9126
9152
  },
9127
9153
  isExpanded ? /* @__PURE__ */ import_react49.default.createElement(import_lucide_react3.ChevronDown, { size: 14 }) : /* @__PURE__ */ import_react49.default.createElement(import_lucide_react3.ChevronRight, { size: 14 })
9128
9154
  ) : /* @__PURE__ */ import_react49.default.createElement("span", { className: "rf-tsn__expand-ph" }),
9129
- selectionMode === "multiple" && /* @__PURE__ */ import_react49.default.createElement(
9130
- "span",
9131
- {
9132
- className: `rf-tsn__cb${state === "checked" ? " rf-tsn__cb--checked" : state === "partial" ? " rf-tsn__cb--partial" : ""}`
9133
- },
9134
- state === "checked" && /* @__PURE__ */ import_react49.default.createElement(import_lucide_react3.Check, { size: 10, strokeWidth: 3 }),
9135
- state === "partial" && /* @__PURE__ */ import_react49.default.createElement("span", { className: "rf-tsn__cb-dash" })
9136
- ),
9137
- selectionMode === "single" && /* @__PURE__ */ import_react49.default.createElement("span", { className: `rf-tsn__radio${state === "checked" ? " rf-tsn__radio--checked" : ""}` }),
9138
- /* @__PURE__ */ import_react49.default.createElement("span", { className: "rf-tsn__label" }, node.label)
9155
+ /* @__PURE__ */ import_react49.default.createElement("span", { className: "rf-tsn__label" }, node.label),
9156
+ /* @__PURE__ */ import_react49.default.createElement("span", { className: "rf-tsn__check", "aria-hidden": "true" }, state === "partial" ? /* @__PURE__ */ import_react49.default.createElement(import_lucide_react3.Minus, { size: 13, strokeWidth: 2.5 }) : /* @__PURE__ */ import_react49.default.createElement(import_lucide_react3.Check, { size: 13, strokeWidth: 2.5 }))
9139
9157
  ), hasChildren && isExpanded && /* @__PURE__ */ import_react49.default.createElement("div", null, node.children.map((child) => /* @__PURE__ */ import_react49.default.createElement(
9140
9158
  TreeNodeItem,
9141
9159
  {
@@ -9145,6 +9163,7 @@ function TreeNodeItem({
9145
9163
  selected,
9146
9164
  expanded,
9147
9165
  selectionMode,
9166
+ allowChildSelection,
9148
9167
  onToggleExpand,
9149
9168
  onSelect,
9150
9169
  isFiltering
@@ -9158,6 +9177,7 @@ function TreeSelect({
9158
9177
  onNodeSelect,
9159
9178
  onNodeUnselect,
9160
9179
  selectionMode = "single",
9180
+ allowChildSelection = false,
9161
9181
  placeholder = "Select",
9162
9182
  filter = false,
9163
9183
  filterInputAutoFocus = false,
@@ -9278,15 +9298,25 @@ function TreeSelect({
9278
9298
  const handleSelect = (node) => {
9279
9299
  const nodeId = String(node.id);
9280
9300
  if (isMultiple) {
9281
- const state = getNodeState(node, selectedSet);
9282
- const descendants = collectDescendants(node);
9283
9301
  const newSet = new Set(selectedSet);
9284
- if (state === "checked" || state === "partial") {
9285
- descendants.forEach((id) => newSet.delete(id));
9286
- onNodeUnselect?.({ node });
9302
+ if (allowChildSelection) {
9303
+ const state = getNodeState(node, selectedSet);
9304
+ const descendants = collectDescendants(node);
9305
+ if (state === "checked" || state === "partial") {
9306
+ descendants.forEach((id) => newSet.delete(id));
9307
+ onNodeUnselect?.({ node });
9308
+ } else {
9309
+ descendants.forEach((id) => newSet.add(id));
9310
+ onNodeSelect?.({ node });
9311
+ }
9287
9312
  } else {
9288
- descendants.forEach((id) => newSet.add(id));
9289
- onNodeSelect?.({ node });
9313
+ if (newSet.has(nodeId)) {
9314
+ newSet.delete(nodeId);
9315
+ onNodeUnselect?.({ node });
9316
+ } else {
9317
+ newSet.add(nodeId);
9318
+ onNodeSelect?.({ node });
9319
+ }
9290
9320
  }
9291
9321
  onChange?.({ value: setToRecord(newSet) });
9292
9322
  } else {
@@ -9319,7 +9349,7 @@ function TreeSelect({
9319
9349
  onNodeUnselect?.({ node });
9320
9350
  };
9321
9351
  const filteredTree = filterTree(options, filterQuery);
9322
- const tagNodes = isMultiple ? getTopLevelSelected(options, selectedSet) : value != null ? findNodeById(options, String(value)) ? [findNodeById(options, String(value))] : [] : [];
9352
+ const tagNodes = isMultiple ? allowChildSelection ? getTopLevelSelected(options, selectedSet) : getAllSelected(options, selectedSet) : value != null ? findNodeById(options, String(value)) ? [findNodeById(options, String(value))] : [] : [];
9323
9353
  const hasValue = tagNodes.length > 0;
9324
9354
  const isFloating = open || hasValue || focused;
9325
9355
  const rootClasses = [
@@ -9417,6 +9447,7 @@ function TreeSelect({
9417
9447
  selected: selectedSet,
9418
9448
  expanded: expandedKeys,
9419
9449
  selectionMode,
9450
+ allowChildSelection,
9420
9451
  onToggleExpand: handleToggleExpand,
9421
9452
  onSelect: handleSelect,
9422
9453
  isFiltering: !!filterQuery
@@ -9550,7 +9581,9 @@ function SmartSelect({
9550
9581
  value,
9551
9582
  onChange,
9552
9583
  onSearchChange,
9584
+ searchResults = [],
9553
9585
  debounceMs = 300,
9586
+ searchThreshold = 10,
9554
9587
  getOptionLabel,
9555
9588
  getOptionValue,
9556
9589
  getOptionSubLabel,
@@ -9588,14 +9621,20 @@ function SmartSelect({
9588
9621
  return flattenTree(options, getOptionChildren);
9589
9622
  }, [options, getOptionChildren]);
9590
9623
  const flatOptionsList = (0, import_react51.useMemo)(() => flatItems.map((f) => f.option), [flatItems]);
9624
+ const displayOptions = (0, import_react51.useMemo)(() => {
9625
+ if (!searchResults.length) return flatOptionsList;
9626
+ const localKeys = new Set(flatOptionsList.map((o) => getValue(o)));
9627
+ const serverOnly = searchResults.filter((o) => !localKeys.has(getValue(o)));
9628
+ return [...flatOptionsList, ...serverOnly];
9629
+ }, [flatOptionsList, searchResults, getValue]);
9591
9630
  const depthMap = (0, import_react51.useMemo)(() => {
9592
9631
  const map = /* @__PURE__ */ new Map();
9593
9632
  flatItems.forEach(({ option, depth }) => map.set(getValue(option), depth));
9594
9633
  return map;
9595
9634
  }, [flatItems, getValue]);
9596
9635
  const lookup = (0, import_react51.useMemo)(
9597
- () => buildLookup(options, getOptionChildren, getValue),
9598
- [options, getOptionChildren, getValue]
9636
+ () => buildLookup(displayOptions, getOptionChildren, getValue),
9637
+ [displayOptions, getOptionChildren, getValue]
9599
9638
  );
9600
9639
  const selectedKeys = (0, import_react51.useMemo)(() => {
9601
9640
  if (multiple) {
@@ -9607,21 +9646,27 @@ function SmartSelect({
9607
9646
  if (!onSearchChange) return;
9608
9647
  if (debounceTimer.current) clearTimeout(debounceTimer.current);
9609
9648
  if (!inputValue) {
9610
- onSearchChange("");
9649
+ onSearchChange("", 0);
9611
9650
  return;
9612
9651
  }
9613
- const hasLocalMatch = flatOptionsList.some(
9614
- (opt) => getOptionLabel(opt).toLowerCase().includes(inputValue.toLowerCase()) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(inputValue.toLowerCase())
9615
- );
9616
- if (hasLocalMatch) return;
9652
+ const q = inputValue.toLowerCase();
9653
+ let localCount = 0;
9654
+ for (const opt of flatOptionsList) {
9655
+ if (getOptionLabel(opt).toLowerCase().includes(q) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q)) {
9656
+ localCount++;
9657
+ if (localCount >= searchThreshold) break;
9658
+ }
9659
+ }
9660
+ if (localCount >= searchThreshold) return;
9661
+ const needed = searchThreshold - localCount;
9617
9662
  if (debounceMs <= 0) {
9618
- onSearchChange(inputValue);
9663
+ onSearchChange(inputValue, needed);
9619
9664
  } else {
9620
9665
  debounceTimer.current = setTimeout(() => {
9621
- onSearchChange(inputValue);
9666
+ onSearchChange(inputValue, needed);
9622
9667
  }, debounceMs);
9623
9668
  }
9624
- }, [onSearchChange, debounceMs, flatOptionsList, getOptionLabel, getOptionSubLabel]);
9669
+ }, [onSearchChange, debounceMs, searchThreshold, flatOptionsList, getOptionLabel, getOptionSubLabel]);
9625
9670
  const handleChange = (0, import_react51.useCallback)((_, newValue) => {
9626
9671
  if (!multiple || !allowChildNodesSelection || !getOptionChildren) {
9627
9672
  onChange?.(newValue);
@@ -9673,34 +9718,28 @@ function SmartSelect({
9673
9718
  {
9674
9719
  key,
9675
9720
  ...rest,
9676
- className: muiClass,
9721
+ className: [
9722
+ muiClass,
9723
+ "rf-select__option",
9724
+ isSelected ? "rf-select__option--selected" : ""
9725
+ ].filter(Boolean).join(" "),
9677
9726
  style: {
9678
9727
  ...muiStyle,
9679
- display: "flex",
9680
- alignItems: "center",
9681
- gap: 10,
9682
- padding: "8px 16px",
9683
9728
  paddingLeft: 16 + depth * 20,
9684
- fontSize: "0.9rem",
9685
- color: "rgba(0,0,0,0.87)",
9686
- cursor: "pointer",
9687
- userSelect: "none",
9688
- minHeight: 40,
9689
- boxSizing: "border-box",
9690
- fontWeight: isSelected ? 500 : void 0,
9691
- backgroundColor: isSelected ? "rgba(164,27,6,0.08)" : void 0,
9692
- listStyle: "none"
9729
+ listStyle: "none",
9730
+ alignItems: subLabel ? "flex-start" : void 0
9693
9731
  }
9694
9732
  },
9695
- /* @__PURE__ */ import_react51.default.createElement("span", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ import_react51.default.createElement("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", lineHeight: 1.3 } }, getOptionLabel(option)), subLabel && /* @__PURE__ */ import_react51.default.createElement("span", { style: { fontSize: "0.75rem", color: "var(--text-secondary)", lineHeight: 1.3, marginTop: 1 } }, subLabel)),
9696
- /* @__PURE__ */ import_react51.default.createElement("span", { style: {
9697
- color: "var(--primary-color, #a41b06)",
9698
- flexShrink: 0,
9699
- display: "flex",
9700
- alignItems: "center",
9701
- opacity: isSelected ? 1 : 0,
9702
- transition: "opacity 0.1s"
9703
- } }, /* @__PURE__ */ import_react51.default.createElement(CheckIcon3, null))
9733
+ /* @__PURE__ */ import_react51.default.createElement(
9734
+ "span",
9735
+ {
9736
+ className: "rf-select__option-label",
9737
+ style: subLabel ? { display: "flex", flexDirection: "column", whiteSpace: "normal" } : void 0
9738
+ },
9739
+ /* @__PURE__ */ import_react51.default.createElement("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", lineHeight: 1.3 } }, getOptionLabel(option)),
9740
+ subLabel && /* @__PURE__ */ import_react51.default.createElement("span", { style: { fontSize: "0.75rem", color: "var(--text-secondary)", lineHeight: 1.3, marginTop: 1 } }, subLabel)
9741
+ ),
9742
+ /* @__PURE__ */ import_react51.default.createElement("span", { className: "rf-select__option-check", "aria-hidden": "true" }, /* @__PURE__ */ import_react51.default.createElement(CheckIcon3, null))
9704
9743
  );
9705
9744
  }, [depthMap, getValue, getOptionLabel, getOptionSubLabel, selectedKeys]);
9706
9745
  const computedFilterOptions = (0, import_react51.useCallback)((opts, inputValue) => {
@@ -9708,10 +9747,12 @@ function SmartSelect({
9708
9747
  if (multiple) {
9709
9748
  const selected = opts.filter((o) => selectedKeys.has(getValue(o)));
9710
9749
  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];
9750
+ if (!inputValue) return [...selected, ...unselected];
9751
+ const q2 = inputValue.toLowerCase();
9752
+ const filteredUnselected = unselected.filter(
9753
+ (opt) => getOptionLabel(opt).toLowerCase().includes(q2) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q2)
9754
+ ).slice(0, searchThreshold);
9755
+ return [...selected, ...filteredUnselected];
9715
9756
  }
9716
9757
  if (value != null) {
9717
9758
  const selectedLabel = getOptionLabel(value);
@@ -9727,12 +9768,12 @@ function SmartSelect({
9727
9768
  const q = inputValue.toLowerCase();
9728
9769
  return opts.filter(
9729
9770
  (opt) => getOptionLabel(opt).toLowerCase().includes(q) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q)
9730
- );
9731
- }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value]);
9771
+ ).slice(0, searchThreshold);
9772
+ }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value, searchThreshold]);
9732
9773
  return /* @__PURE__ */ import_react51.default.createElement(
9733
9774
  Autocomplete,
9734
9775
  {
9735
- options: flatOptionsList,
9776
+ options: displayOptions,
9736
9777
  value: value ?? (multiple ? [] : null),
9737
9778
  onChange: handleChange,
9738
9779
  onInputChange: handleInputChange,
package/dist/main.css CHANGED
@@ -1370,6 +1370,12 @@ pre {
1370
1370
  .rf-tsn__row:hover {
1371
1371
  background: rgba(241, 91, 36, 0.06);
1372
1372
  }
1373
+ .rf-tsn__row--active {
1374
+ background: rgba(164, 27, 6, 0.08);
1375
+ }
1376
+ .rf-tsn__row--active:hover {
1377
+ background: rgba(164, 27, 6, 0.12);
1378
+ }
1373
1379
  .rf-tsn__row--active .rf-tsn__label {
1374
1380
  color: rgba(0, 0, 0, 0.87);
1375
1381
  font-weight: 500;
@@ -1397,56 +1403,17 @@ pre {
1397
1403
  flex-shrink: 0;
1398
1404
  display: inline-block;
1399
1405
  }
1400
- .rf-tsn__cb {
1401
- width: 16px;
1402
- height: 16px;
1403
- border: 2px solid var(--border-color);
1404
- border-radius: 4px;
1406
+ .rf-tsn__check {
1407
+ color: var(--primary-color, #a41b06);
1408
+ flex-shrink: 0;
1405
1409
  display: flex;
1406
1410
  align-items: center;
1407
- justify-content: center;
1408
- flex-shrink: 0;
1409
- transition: border-color 0.15s, background 0.15s;
1410
- background: var(--surface-color);
1411
- color: transparent;
1412
- }
1413
- .rf-tsn__cb--checked {
1414
- background: var(--primary-color);
1415
- border-color: var(--primary-color);
1416
- color: #fff;
1417
- }
1418
- .rf-tsn__cb--partial {
1419
- border-color: var(--primary-color);
1420
- }
1421
- .rf-tsn__cb-dash {
1422
- display: block;
1423
- width: 8px;
1424
- height: 2px;
1425
- background: var(--primary-color);
1426
- border-radius: 1px;
1427
- }
1428
- .rf-tsn__radio {
1429
- width: 16px;
1430
- height: 16px;
1431
- border: 2px solid var(--border-color);
1432
- border-radius: 50%;
1433
- flex-shrink: 0;
1434
- transition: border-color 0.15s;
1435
- position: relative;
1436
- }
1437
- .rf-tsn__radio--checked {
1438
- border-color: var(--primary-color);
1411
+ margin-left: auto;
1412
+ opacity: 0;
1413
+ transition: opacity 0.1s;
1439
1414
  }
1440
- .rf-tsn__radio--checked::after {
1441
- content: "";
1442
- position: absolute;
1443
- top: 50%;
1444
- left: 50%;
1445
- transform: translate(-50%, -50%);
1446
- width: 8px;
1447
- height: 8px;
1448
- background: var(--primary-color);
1449
- border-radius: 50%;
1415
+ .rf-tsn__row--active .rf-tsn__check {
1416
+ opacity: 1;
1450
1417
  }
1451
1418
  .rf-tsn__label {
1452
1419
  font-size: 0.875rem;
package/dist/main.d.cts CHANGED
@@ -1830,7 +1830,13 @@ interface TreeSelectProps {
1830
1830
  filterInputAutoFocus?: boolean;
1831
1831
  /** Clear filter text when the dropdown closes */
1832
1832
  resetFilterOnHide?: boolean;
1833
- /** PrimeReact compat — accepted but not used (checkboxes handle multi-select) */
1833
+ /**
1834
+ * When **true**, selecting a parent selects all its descendants and vice-versa (cascade).
1835
+ * When **false** (default), every node is selected/deselected independently — no cascade.
1836
+ * Only relevant in `multiple` mode.
1837
+ */
1838
+ allowChildSelection?: boolean;
1839
+ /** PrimeReact compat — accepted but not used */
1834
1840
  metaKeySelection?: boolean;
1835
1841
  /** Disable the whole control */
1836
1842
  disabled?: boolean;
@@ -1854,7 +1860,7 @@ interface TreeSelectProps {
1854
1860
  className?: string;
1855
1861
  sx?: SxProp;
1856
1862
  }
1857
- declare function TreeSelect({ options, value, onChange, onNodeSelect, onNodeUnselect, selectionMode, placeholder, filter, filterInputAutoFocus, resetFilterOnHide, metaKeySelection: _metaKeySelection, disabled, label, variant, size, error, helperText, fullWidth, clearable, required, style, className, sx, }: TreeSelectProps): React__default.JSX.Element;
1863
+ declare function TreeSelect({ options, value, onChange, onNodeSelect, onNodeUnselect, selectionMode, allowChildSelection, placeholder, filter, filterInputAutoFocus, resetFilterOnHide, metaKeySelection: _metaKeySelection, disabled, label, variant, size, error, helperText, fullWidth, clearable, required, style, className, sx, }: TreeSelectProps): React__default.JSX.Element;
1858
1864
 
1859
1865
  interface UserOption {
1860
1866
  id?: string | number;
@@ -1909,15 +1915,28 @@ interface SmartSelectProps<T = any> {
1909
1915
  /** Called when selection changes */
1910
1916
  onChange?: (value: T | T[] | null) => void;
1911
1917
  /**
1912
- * Called when the typed text finds no local match —
1913
- * use this to trigger an API / server search.
1918
+ * Called when local matches fall below `searchThreshold`.
1919
+ * Receives the current query and how many more records are needed to fill the threshold.
1920
+ * Use this to trigger an API / server search and update `searchResults`.
1914
1921
  */
1915
- onSearchChange?: (query: string) => void;
1922
+ onSearchChange?: (query: string, needed: number) => void;
1923
+ /**
1924
+ * Results returned by the API for the current query.
1925
+ * Merged with local matches (duplicates excluded) to fill up to `searchThreshold`.
1926
+ * Reset this to [] when the query is cleared.
1927
+ */
1928
+ searchResults?: T[];
1916
1929
  /**
1917
1930
  * Debounce delay in ms before `onSearchChange` fires.
1918
1931
  * Defaults to 300 ms. Pass 0 to disable debouncing.
1919
1932
  */
1920
1933
  debounceMs?: number;
1934
+ /**
1935
+ * Max results to show when a query is active.
1936
+ * If local matches are fewer than this, `onSearchChange` fires with how many more are needed.
1937
+ * Defaults to 10. Pass 0 to always call the API.
1938
+ */
1939
+ searchThreshold?: number;
1921
1940
  /** Primary display label for an option (required) */
1922
1941
  getOptionLabel: (option: T) => string;
1923
1942
  /** Unique key for an option — defaults to the label string */
@@ -1971,7 +1990,7 @@ interface SmartSelectProps<T = any> {
1971
1990
  style?: CSSProperties;
1972
1991
  sx?: SxProp;
1973
1992
  }
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;
1993
+ 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
1994
 
1976
1995
  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
1996
  type EditorVariant = 'default' | 'basic';
package/dist/main.d.ts CHANGED
@@ -1830,7 +1830,13 @@ interface TreeSelectProps {
1830
1830
  filterInputAutoFocus?: boolean;
1831
1831
  /** Clear filter text when the dropdown closes */
1832
1832
  resetFilterOnHide?: boolean;
1833
- /** PrimeReact compat — accepted but not used (checkboxes handle multi-select) */
1833
+ /**
1834
+ * When **true**, selecting a parent selects all its descendants and vice-versa (cascade).
1835
+ * When **false** (default), every node is selected/deselected independently — no cascade.
1836
+ * Only relevant in `multiple` mode.
1837
+ */
1838
+ allowChildSelection?: boolean;
1839
+ /** PrimeReact compat — accepted but not used */
1834
1840
  metaKeySelection?: boolean;
1835
1841
  /** Disable the whole control */
1836
1842
  disabled?: boolean;
@@ -1854,7 +1860,7 @@ interface TreeSelectProps {
1854
1860
  className?: string;
1855
1861
  sx?: SxProp;
1856
1862
  }
1857
- declare function TreeSelect({ options, value, onChange, onNodeSelect, onNodeUnselect, selectionMode, placeholder, filter, filterInputAutoFocus, resetFilterOnHide, metaKeySelection: _metaKeySelection, disabled, label, variant, size, error, helperText, fullWidth, clearable, required, style, className, sx, }: TreeSelectProps): React__default.JSX.Element;
1863
+ declare function TreeSelect({ options, value, onChange, onNodeSelect, onNodeUnselect, selectionMode, allowChildSelection, placeholder, filter, filterInputAutoFocus, resetFilterOnHide, metaKeySelection: _metaKeySelection, disabled, label, variant, size, error, helperText, fullWidth, clearable, required, style, className, sx, }: TreeSelectProps): React__default.JSX.Element;
1858
1864
 
1859
1865
  interface UserOption {
1860
1866
  id?: string | number;
@@ -1909,15 +1915,28 @@ interface SmartSelectProps<T = any> {
1909
1915
  /** Called when selection changes */
1910
1916
  onChange?: (value: T | T[] | null) => void;
1911
1917
  /**
1912
- * Called when the typed text finds no local match —
1913
- * use this to trigger an API / server search.
1918
+ * Called when local matches fall below `searchThreshold`.
1919
+ * Receives the current query and how many more records are needed to fill the threshold.
1920
+ * Use this to trigger an API / server search and update `searchResults`.
1914
1921
  */
1915
- onSearchChange?: (query: string) => void;
1922
+ onSearchChange?: (query: string, needed: number) => void;
1923
+ /**
1924
+ * Results returned by the API for the current query.
1925
+ * Merged with local matches (duplicates excluded) to fill up to `searchThreshold`.
1926
+ * Reset this to [] when the query is cleared.
1927
+ */
1928
+ searchResults?: T[];
1916
1929
  /**
1917
1930
  * Debounce delay in ms before `onSearchChange` fires.
1918
1931
  * Defaults to 300 ms. Pass 0 to disable debouncing.
1919
1932
  */
1920
1933
  debounceMs?: number;
1934
+ /**
1935
+ * Max results to show when a query is active.
1936
+ * If local matches are fewer than this, `onSearchChange` fires with how many more are needed.
1937
+ * Defaults to 10. Pass 0 to always call the API.
1938
+ */
1939
+ searchThreshold?: number;
1921
1940
  /** Primary display label for an option (required) */
1922
1941
  getOptionLabel: (option: T) => string;
1923
1942
  /** Unique key for an option — defaults to the label string */
@@ -1971,7 +1990,7 @@ interface SmartSelectProps<T = any> {
1971
1990
  style?: CSSProperties;
1972
1991
  sx?: SxProp;
1973
1992
  }
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;
1993
+ 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
1994
 
1976
1995
  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
1996
  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
  ));
@@ -8961,7 +8978,7 @@ import React106, {
8961
8978
  useCallback as useCallback10
8962
8979
  } from "react";
8963
8980
  import ReactDOM10 from "react-dom";
8964
- import { ChevronDown as ChevronDown2, ChevronRight as ChevronRight2, X as X3, Search as Search2, Check } from "lucide-react";
8981
+ import { ChevronDown as ChevronDown2, ChevronRight as ChevronRight2, X as X3, Search as Search2, Check, Minus } from "lucide-react";
8965
8982
  function collectDescendants(node) {
8966
8983
  const ids = [String(node.id)];
8967
8984
  node.children?.forEach((c) => ids.push(...collectDescendants(c)));
@@ -8995,6 +9012,14 @@ function getTopLevelSelected(nodes, selected) {
8995
9012
  }
8996
9013
  return result;
8997
9014
  }
9015
+ function getAllSelected(nodes, selected) {
9016
+ const result = [];
9017
+ for (const node of nodes) {
9018
+ if (selected.has(String(node.id))) result.push(node);
9019
+ if (node.children?.length) result.push(...getAllSelected(node.children, selected));
9020
+ }
9021
+ return result;
9022
+ }
8998
9023
  function filterTree(nodes, query) {
8999
9024
  if (!query) return nodes;
9000
9025
  const q = query.toLowerCase();
@@ -9023,6 +9048,7 @@ function TreeNodeItem({
9023
9048
  selected,
9024
9049
  expanded,
9025
9050
  selectionMode,
9051
+ allowChildSelection,
9026
9052
  onToggleExpand,
9027
9053
  onSelect,
9028
9054
  isFiltering
@@ -9030,7 +9056,7 @@ function TreeNodeItem({
9030
9056
  const nodeId = String(node.id);
9031
9057
  const hasChildren = !!node.children?.length;
9032
9058
  const isExpanded = isFiltering || expanded.has(nodeId);
9033
- const state = getNodeState(node, selected);
9059
+ const state = allowChildSelection ? getNodeState(node, selected) : selected.has(nodeId) ? "checked" : "unchecked";
9034
9060
  return /* @__PURE__ */ React106.createElement("div", { className: "rf-tsn" }, /* @__PURE__ */ React106.createElement(
9035
9061
  "div",
9036
9062
  {
@@ -9050,16 +9076,8 @@ function TreeNodeItem({
9050
9076
  },
9051
9077
  isExpanded ? /* @__PURE__ */ React106.createElement(ChevronDown2, { size: 14 }) : /* @__PURE__ */ React106.createElement(ChevronRight2, { size: 14 })
9052
9078
  ) : /* @__PURE__ */ React106.createElement("span", { className: "rf-tsn__expand-ph" }),
9053
- selectionMode === "multiple" && /* @__PURE__ */ React106.createElement(
9054
- "span",
9055
- {
9056
- className: `rf-tsn__cb${state === "checked" ? " rf-tsn__cb--checked" : state === "partial" ? " rf-tsn__cb--partial" : ""}`
9057
- },
9058
- state === "checked" && /* @__PURE__ */ React106.createElement(Check, { size: 10, strokeWidth: 3 }),
9059
- state === "partial" && /* @__PURE__ */ React106.createElement("span", { className: "rf-tsn__cb-dash" })
9060
- ),
9061
- selectionMode === "single" && /* @__PURE__ */ React106.createElement("span", { className: `rf-tsn__radio${state === "checked" ? " rf-tsn__radio--checked" : ""}` }),
9062
- /* @__PURE__ */ React106.createElement("span", { className: "rf-tsn__label" }, node.label)
9079
+ /* @__PURE__ */ React106.createElement("span", { className: "rf-tsn__label" }, node.label),
9080
+ /* @__PURE__ */ React106.createElement("span", { className: "rf-tsn__check", "aria-hidden": "true" }, state === "partial" ? /* @__PURE__ */ React106.createElement(Minus, { size: 13, strokeWidth: 2.5 }) : /* @__PURE__ */ React106.createElement(Check, { size: 13, strokeWidth: 2.5 }))
9063
9081
  ), hasChildren && isExpanded && /* @__PURE__ */ React106.createElement("div", null, node.children.map((child) => /* @__PURE__ */ React106.createElement(
9064
9082
  TreeNodeItem,
9065
9083
  {
@@ -9069,6 +9087,7 @@ function TreeNodeItem({
9069
9087
  selected,
9070
9088
  expanded,
9071
9089
  selectionMode,
9090
+ allowChildSelection,
9072
9091
  onToggleExpand,
9073
9092
  onSelect,
9074
9093
  isFiltering
@@ -9082,6 +9101,7 @@ function TreeSelect({
9082
9101
  onNodeSelect,
9083
9102
  onNodeUnselect,
9084
9103
  selectionMode = "single",
9104
+ allowChildSelection = false,
9085
9105
  placeholder = "Select",
9086
9106
  filter = false,
9087
9107
  filterInputAutoFocus = false,
@@ -9202,15 +9222,25 @@ function TreeSelect({
9202
9222
  const handleSelect = (node) => {
9203
9223
  const nodeId = String(node.id);
9204
9224
  if (isMultiple) {
9205
- const state = getNodeState(node, selectedSet);
9206
- const descendants = collectDescendants(node);
9207
9225
  const newSet = new Set(selectedSet);
9208
- if (state === "checked" || state === "partial") {
9209
- descendants.forEach((id) => newSet.delete(id));
9210
- onNodeUnselect?.({ node });
9226
+ if (allowChildSelection) {
9227
+ const state = getNodeState(node, selectedSet);
9228
+ const descendants = collectDescendants(node);
9229
+ if (state === "checked" || state === "partial") {
9230
+ descendants.forEach((id) => newSet.delete(id));
9231
+ onNodeUnselect?.({ node });
9232
+ } else {
9233
+ descendants.forEach((id) => newSet.add(id));
9234
+ onNodeSelect?.({ node });
9235
+ }
9211
9236
  } else {
9212
- descendants.forEach((id) => newSet.add(id));
9213
- onNodeSelect?.({ node });
9237
+ if (newSet.has(nodeId)) {
9238
+ newSet.delete(nodeId);
9239
+ onNodeUnselect?.({ node });
9240
+ } else {
9241
+ newSet.add(nodeId);
9242
+ onNodeSelect?.({ node });
9243
+ }
9214
9244
  }
9215
9245
  onChange?.({ value: setToRecord(newSet) });
9216
9246
  } else {
@@ -9243,7 +9273,7 @@ function TreeSelect({
9243
9273
  onNodeUnselect?.({ node });
9244
9274
  };
9245
9275
  const filteredTree = filterTree(options, filterQuery);
9246
- const tagNodes = isMultiple ? getTopLevelSelected(options, selectedSet) : value != null ? findNodeById(options, String(value)) ? [findNodeById(options, String(value))] : [] : [];
9276
+ const tagNodes = isMultiple ? allowChildSelection ? getTopLevelSelected(options, selectedSet) : getAllSelected(options, selectedSet) : value != null ? findNodeById(options, String(value)) ? [findNodeById(options, String(value))] : [] : [];
9247
9277
  const hasValue = tagNodes.length > 0;
9248
9278
  const isFloating = open || hasValue || focused;
9249
9279
  const rootClasses = [
@@ -9341,6 +9371,7 @@ function TreeSelect({
9341
9371
  selected: selectedSet,
9342
9372
  expanded: expandedKeys,
9343
9373
  selectionMode,
9374
+ allowChildSelection,
9344
9375
  onToggleExpand: handleToggleExpand,
9345
9376
  onSelect: handleSelect,
9346
9377
  isFiltering: !!filterQuery
@@ -9474,7 +9505,9 @@ function SmartSelect({
9474
9505
  value,
9475
9506
  onChange,
9476
9507
  onSearchChange,
9508
+ searchResults = [],
9477
9509
  debounceMs = 300,
9510
+ searchThreshold = 10,
9478
9511
  getOptionLabel,
9479
9512
  getOptionValue,
9480
9513
  getOptionSubLabel,
@@ -9512,14 +9545,20 @@ function SmartSelect({
9512
9545
  return flattenTree(options, getOptionChildren);
9513
9546
  }, [options, getOptionChildren]);
9514
9547
  const flatOptionsList = useMemo3(() => flatItems.map((f) => f.option), [flatItems]);
9548
+ const displayOptions = useMemo3(() => {
9549
+ if (!searchResults.length) return flatOptionsList;
9550
+ const localKeys = new Set(flatOptionsList.map((o) => getValue(o)));
9551
+ const serverOnly = searchResults.filter((o) => !localKeys.has(getValue(o)));
9552
+ return [...flatOptionsList, ...serverOnly];
9553
+ }, [flatOptionsList, searchResults, getValue]);
9515
9554
  const depthMap = useMemo3(() => {
9516
9555
  const map = /* @__PURE__ */ new Map();
9517
9556
  flatItems.forEach(({ option, depth }) => map.set(getValue(option), depth));
9518
9557
  return map;
9519
9558
  }, [flatItems, getValue]);
9520
9559
  const lookup = useMemo3(
9521
- () => buildLookup(options, getOptionChildren, getValue),
9522
- [options, getOptionChildren, getValue]
9560
+ () => buildLookup(displayOptions, getOptionChildren, getValue),
9561
+ [displayOptions, getOptionChildren, getValue]
9523
9562
  );
9524
9563
  const selectedKeys = useMemo3(() => {
9525
9564
  if (multiple) {
@@ -9531,21 +9570,27 @@ function SmartSelect({
9531
9570
  if (!onSearchChange) return;
9532
9571
  if (debounceTimer.current) clearTimeout(debounceTimer.current);
9533
9572
  if (!inputValue) {
9534
- onSearchChange("");
9573
+ onSearchChange("", 0);
9535
9574
  return;
9536
9575
  }
9537
- const hasLocalMatch = flatOptionsList.some(
9538
- (opt) => getOptionLabel(opt).toLowerCase().includes(inputValue.toLowerCase()) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(inputValue.toLowerCase())
9539
- );
9540
- if (hasLocalMatch) return;
9576
+ const q = inputValue.toLowerCase();
9577
+ let localCount = 0;
9578
+ for (const opt of flatOptionsList) {
9579
+ if (getOptionLabel(opt).toLowerCase().includes(q) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q)) {
9580
+ localCount++;
9581
+ if (localCount >= searchThreshold) break;
9582
+ }
9583
+ }
9584
+ if (localCount >= searchThreshold) return;
9585
+ const needed = searchThreshold - localCount;
9541
9586
  if (debounceMs <= 0) {
9542
- onSearchChange(inputValue);
9587
+ onSearchChange(inputValue, needed);
9543
9588
  } else {
9544
9589
  debounceTimer.current = setTimeout(() => {
9545
- onSearchChange(inputValue);
9590
+ onSearchChange(inputValue, needed);
9546
9591
  }, debounceMs);
9547
9592
  }
9548
- }, [onSearchChange, debounceMs, flatOptionsList, getOptionLabel, getOptionSubLabel]);
9593
+ }, [onSearchChange, debounceMs, searchThreshold, flatOptionsList, getOptionLabel, getOptionSubLabel]);
9549
9594
  const handleChange = useCallback11((_, newValue) => {
9550
9595
  if (!multiple || !allowChildNodesSelection || !getOptionChildren) {
9551
9596
  onChange?.(newValue);
@@ -9597,34 +9642,28 @@ function SmartSelect({
9597
9642
  {
9598
9643
  key,
9599
9644
  ...rest,
9600
- className: muiClass,
9645
+ className: [
9646
+ muiClass,
9647
+ "rf-select__option",
9648
+ isSelected ? "rf-select__option--selected" : ""
9649
+ ].filter(Boolean).join(" "),
9601
9650
  style: {
9602
9651
  ...muiStyle,
9603
- display: "flex",
9604
- alignItems: "center",
9605
- gap: 10,
9606
- padding: "8px 16px",
9607
9652
  paddingLeft: 16 + depth * 20,
9608
- fontSize: "0.9rem",
9609
- color: "rgba(0,0,0,0.87)",
9610
- cursor: "pointer",
9611
- userSelect: "none",
9612
- minHeight: 40,
9613
- boxSizing: "border-box",
9614
- fontWeight: isSelected ? 500 : void 0,
9615
- backgroundColor: isSelected ? "rgba(164,27,6,0.08)" : void 0,
9616
- listStyle: "none"
9653
+ listStyle: "none",
9654
+ alignItems: subLabel ? "flex-start" : void 0
9617
9655
  }
9618
9656
  },
9619
- /* @__PURE__ */ React108.createElement("span", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React108.createElement("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", lineHeight: 1.3 } }, getOptionLabel(option)), subLabel && /* @__PURE__ */ React108.createElement("span", { style: { fontSize: "0.75rem", color: "var(--text-secondary)", lineHeight: 1.3, marginTop: 1 } }, subLabel)),
9620
- /* @__PURE__ */ React108.createElement("span", { style: {
9621
- color: "var(--primary-color, #a41b06)",
9622
- flexShrink: 0,
9623
- display: "flex",
9624
- alignItems: "center",
9625
- opacity: isSelected ? 1 : 0,
9626
- transition: "opacity 0.1s"
9627
- } }, /* @__PURE__ */ React108.createElement(CheckIcon3, null))
9657
+ /* @__PURE__ */ React108.createElement(
9658
+ "span",
9659
+ {
9660
+ className: "rf-select__option-label",
9661
+ style: subLabel ? { display: "flex", flexDirection: "column", whiteSpace: "normal" } : void 0
9662
+ },
9663
+ /* @__PURE__ */ React108.createElement("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", lineHeight: 1.3 } }, getOptionLabel(option)),
9664
+ subLabel && /* @__PURE__ */ React108.createElement("span", { style: { fontSize: "0.75rem", color: "var(--text-secondary)", lineHeight: 1.3, marginTop: 1 } }, subLabel)
9665
+ ),
9666
+ /* @__PURE__ */ React108.createElement("span", { className: "rf-select__option-check", "aria-hidden": "true" }, /* @__PURE__ */ React108.createElement(CheckIcon3, null))
9628
9667
  );
9629
9668
  }, [depthMap, getValue, getOptionLabel, getOptionSubLabel, selectedKeys]);
9630
9669
  const computedFilterOptions = useCallback11((opts, inputValue) => {
@@ -9632,10 +9671,12 @@ function SmartSelect({
9632
9671
  if (multiple) {
9633
9672
  const selected = opts.filter((o) => selectedKeys.has(getValue(o)));
9634
9673
  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];
9674
+ if (!inputValue) return [...selected, ...unselected];
9675
+ const q2 = inputValue.toLowerCase();
9676
+ const filteredUnselected = unselected.filter(
9677
+ (opt) => getOptionLabel(opt).toLowerCase().includes(q2) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q2)
9678
+ ).slice(0, searchThreshold);
9679
+ return [...selected, ...filteredUnselected];
9639
9680
  }
9640
9681
  if (value != null) {
9641
9682
  const selectedLabel = getOptionLabel(value);
@@ -9651,12 +9692,12 @@ function SmartSelect({
9651
9692
  const q = inputValue.toLowerCase();
9652
9693
  return opts.filter(
9653
9694
  (opt) => getOptionLabel(opt).toLowerCase().includes(q) || (getOptionSubLabel?.(opt) ?? "").toLowerCase().includes(q)
9654
- );
9655
- }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value]);
9695
+ ).slice(0, searchThreshold);
9696
+ }, [filterOptionsProp, multiple, selectedKeys, getValue, getOptionLabel, getOptionSubLabel, value, searchThreshold]);
9656
9697
  return /* @__PURE__ */ React108.createElement(
9657
9698
  Autocomplete,
9658
9699
  {
9659
- options: flatOptionsList,
9700
+ options: displayOptions,
9660
9701
  value: value ?? (multiple ? [] : null),
9661
9702
  onChange: handleChange,
9662
9703
  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.22",
5
5
  "type": "module",
6
6
  "description": "Experimental: A lightweight React UI component library (Beta)",
7
7
  "style": "./dist/main.css",