pixelize-design-library 2.4.2-beta.46 → 2.4.2-beta.48

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +76 -3
  2. package/COMPONENT-GOTCHAS.md +76 -7
  3. package/dist/Components/Common/chipTriggerSize.d.ts +10 -0
  4. package/dist/Components/Common/chipTriggerSize.js +15 -0
  5. package/dist/Components/Dropdown/DropDown.js +10 -8
  6. package/dist/Components/SearchSelect/SearchSelect.js +66 -20
  7. package/dist/Components/SearchSelect/SearchSelectProps.d.ts +5 -1
  8. package/dist/Components/SearchSelect/searchSelectSize.d.ts +0 -5
  9. package/dist/Components/SearchSelect/searchSelectSize.js +0 -12
  10. package/dist/Components/Select/SelectNative.d.ts +2 -0
  11. package/dist/Components/Select/SelectPopup.js +104 -43
  12. package/dist/Components/Select/SelectProps.d.ts +4 -0
  13. package/dist/Components/Select/optionRowInk.d.ts +4 -0
  14. package/dist/Components/Select/optionRowInk.js +12 -1
  15. package/dist/Components/SelectSearch/SelectSearch.js +8 -4
  16. package/dist/Components/SelectV2/SelectV2.d.ts +2 -9
  17. package/dist/Components/SelectV2/SelectV2.js +2 -10
  18. package/dist/Components/Switch/Switch.d.ts +1 -1
  19. package/dist/Components/Switch/Switch.js +4 -2
  20. package/dist/Components/Table/Table.js +6 -7
  21. package/dist/Components/Table/TableProps.d.ts +9 -1
  22. package/dist/Components/Table/components/Pagination.js +8 -5
  23. package/dist/Components/Table/components/TableBody.d.ts +2 -1
  24. package/dist/Components/Table/components/TableBody.js +82 -15
  25. package/dist/Components/Table/components/TableHeader.d.ts +1 -1
  26. package/dist/Components/Table/components/TableHeader.js +8 -9
  27. package/dist/Components/Table/components/rowMetrics.d.ts +0 -1
  28. package/dist/Components/Table/components/rowMetrics.js +1 -2
  29. package/dist/Components/Table/filters/CompactSelect.js +6 -3
  30. package/dist/Components/Table/hooks/useTable.d.ts +5 -1
  31. package/dist/Components/Table/hooks/useTable.js +43 -7
  32. package/dist/Theme/chakra/Menu.styles.d.ts +6 -0
  33. package/dist/Theme/chakra/Menu.styles.js +8 -0
  34. package/dist/Theme/chakra/componentStyles.d.ts +6 -0
  35. package/dist/Theme/tokens/builders/outlineRung.d.ts +2 -0
  36. package/dist/Theme/tokens/builders/outlineRung.js +14 -1
  37. package/dist/Theme/tokens/builders/rowStates.js +3 -4
  38. package/dist/Theme/tokens/primitives/optionRowSizes.d.ts +20 -0
  39. package/dist/Theme/tokens/primitives/optionRowSizes.js +29 -0
  40. package/dist/esm/Components/Common/chipTriggerSize.js +11 -0
  41. package/dist/esm/Components/Dropdown/DropDown.js +10 -8
  42. package/dist/esm/Components/SearchSelect/SearchSelect.js +66 -20
  43. package/dist/esm/Components/SearchSelect/searchSelectSize.js +0 -12
  44. package/dist/esm/Components/Select/SelectPopup.js +105 -44
  45. package/dist/esm/Components/Select/optionRowInk.js +9 -0
  46. package/dist/esm/Components/SelectSearch/SelectSearch.js +9 -5
  47. package/dist/esm/Components/SelectV2/SelectV2.js +2 -9
  48. package/dist/esm/Components/Switch/Switch.js +4 -2
  49. package/dist/esm/Components/Table/Table.js +6 -7
  50. package/dist/esm/Components/Table/components/Pagination.js +8 -5
  51. package/dist/esm/Components/Table/components/TableBody.js +81 -16
  52. package/dist/esm/Components/Table/components/TableHeader.js +8 -9
  53. package/dist/esm/Components/Table/components/rowMetrics.js +0 -1
  54. package/dist/esm/Components/Table/filters/CompactSelect.js +7 -4
  55. package/dist/esm/Components/Table/hooks/useTable.js +44 -8
  56. package/dist/esm/Theme/chakra/Menu.styles.js +8 -0
  57. package/dist/esm/Theme/tokens/builders/outlineRung.js +12 -0
  58. package/dist/esm/Theme/tokens/builders/rowStates.js +4 -5
  59. package/dist/esm/Theme/tokens/primitives/optionRowSizes.js +25 -0
  60. package/dist/esm/index.js +2 -0
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.js +9 -4
  63. package/package.json +1 -1
@@ -2,14 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.listRowBackgrounds = void 0;
4
4
  const outlineRung_1 = require("./outlineRung");
5
- // NOTE: rungs stay neutral/pale — seeding from primary[500] instead once painted SelectV2's hover solid brand fill.
6
5
  const listRowBackgrounds = (colors) => {
7
- // Dark aliases backgroundColor.tertiary to the panel, leaving the selected row at 1.025:1.
8
- const selected = (0, outlineRung_1.liftToContrast)(colors.backgroundColor.tertiary, colors.surfaceRaised, outlineRung_1.MIN_PERCEPTIBLE_STEP);
6
+ // Raw tertiary reaches 1.29:1 and reads as pressed; dark aliases it to the panel at 1.025:1.
7
+ const selected = (0, outlineRung_1.tintStep)(colors.backgroundColor.tertiary, colors.surfaceRaised, outlineRung_1.MIN_PERCEPTIBLE_STEP);
9
8
  return {
10
9
  active: (0, outlineRung_1.resolveHoverTint)(colors.gray[50], colors.surfaceRaised),
11
10
  selected,
12
- selectedActive: (0, outlineRung_1.resolveHoverTint)(selected, selected),
11
+ selectedActive: (0, outlineRung_1.tintStep)(selected, selected, outlineRung_1.STACKED_STATE_STEP),
13
12
  };
14
13
  };
15
14
  exports.listRowBackgrounds = listRowBackgrounds;
@@ -0,0 +1,20 @@
1
+ import type { ControlSize } from "./controlSizes";
2
+ export type OptionRowSpec = {
3
+ minH: string;
4
+ px: string;
5
+ py: string;
6
+ gap: string;
7
+ /** Inset so the row's rounded fill stops short of the popup edge. */
8
+ mx: string;
9
+ fontSize: string;
10
+ descriptionFontSize: string;
11
+ iconSize: number;
12
+ /** Avatar token for rows that lead with one — an `sm` avatar alone outgrows an `sm` row. */
13
+ avatarSize: "2xs" | "xs" | "sm";
14
+ /** Fixed row height the virtualiser scrolls by; must match a single-line row. */
15
+ virtualRowHeightPx: number;
16
+ };
17
+ /** One density for every option row — Select, SearchSelect, SelectSearch, table filters and Chakra's MenuItem. */
18
+ export declare const OPTION_ROW_SIZES: Readonly<Record<ControlSize, OptionRowSpec>>;
19
+ export declare const OPTION_ROW_RADIUS = "md";
20
+ export declare const optionRowSpec: (size: ControlSize) => OptionRowSpec;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.optionRowSpec = exports.OPTION_ROW_RADIUS = exports.OPTION_ROW_SIZES = void 0;
4
+ /** One density for every option row — Select, SearchSelect, SelectSearch, table filters and Chakra's MenuItem. */
5
+ exports.OPTION_ROW_SIZES = {
6
+ xs: {
7
+ minH: "1.5rem", px: "0.5rem", py: "0.125rem", gap: "0.375rem", mx: "0.25rem",
8
+ fontSize: "0.75rem", descriptionFontSize: "0.6875rem", iconSize: 12,
9
+ avatarSize: "2xs", virtualRowHeightPx: 24,
10
+ },
11
+ sm: {
12
+ minH: "1.875rem", px: "0.625rem", py: "0.25rem", gap: "0.5rem", mx: "0.375rem",
13
+ fontSize: "0.8125rem", descriptionFontSize: "0.75rem", iconSize: 14,
14
+ avatarSize: "xs", virtualRowHeightPx: 30,
15
+ },
16
+ md: {
17
+ minH: "2.125rem", px: "0.75rem", py: "0.3125rem", gap: "0.5rem", mx: "0.375rem",
18
+ fontSize: "0.875rem", descriptionFontSize: "0.75rem", iconSize: 16,
19
+ avatarSize: "xs", virtualRowHeightPx: 34,
20
+ },
21
+ lg: {
22
+ minH: "2.5rem", px: "0.875rem", py: "0.375rem", gap: "0.625rem", mx: "0.5rem",
23
+ fontSize: "1rem", descriptionFontSize: "0.8125rem", iconSize: 18,
24
+ avatarSize: "sm", virtualRowHeightPx: 40,
25
+ },
26
+ };
27
+ exports.OPTION_ROW_RADIUS = "md";
28
+ const optionRowSpec = (size) => exports.OPTION_ROW_SIZES[size];
29
+ exports.optionRowSpec = optionRowSpec;
@@ -0,0 +1,11 @@
1
+ import { CONTROL_SIZES } from "../../Theme/tokens/primitives/controlSizes.js";
2
+ const CHIP_TAG_HEIGHT_PX = { xs: 20, sm: 24 };
3
+ const CONTROL_BORDER_PX = 2;
4
+ const controlHeightPx = (size) => parseFloat(CONTROL_SIZES[size].height) * 16;
5
+ const chipRowPaddingY = (size, chipSize) => `${(controlHeightPx(size) - CHIP_TAG_HEIGHT_PX[chipSize] - CONTROL_BORDER_PX) / 2 / 16}rem`;
6
+ /** Sizes a select trigger so a chip row lands on the same height as a plain one. */
7
+ export const chipTriggerSizeStyles = (size, hasChips, chipSize = "sm") => {
8
+ const spec = CONTROL_SIZES[size];
9
+ const base = { minH: spec.height, fontSize: spec.fontSize, px: spec.paddingX };
10
+ return hasChips ? { ...base, py: chipRowPaddingY(size, chipSize) } : { ...base, h: spec.height };
11
+ };
@@ -9,16 +9,17 @@ import { useExclusiveOverlay } from "../../Hooks/useExclusiveOverlay.js";
9
9
  import { ChevronDown } from "lucide-react";
10
10
  import { rafThrottle } from "../../Utils/rafThrottle.js";
11
11
  import { CONTROL_SIZES } from "../../Theme/tokens/primitives/controlSizes.js";
12
+ import { optionRowSpec, OPTION_ROW_RADIUS } from "../../Theme/tokens/primitives/optionRowSizes.js";
12
13
  import { transitionFor } from "../../Theme/tokens/primitives/motion.js";
13
14
  import { reducedMotionSx } from "../../Theme/chakra/motion.js";
14
15
  import { focusVisibleStyles } from "../../Theme/chakra/focusRing.styles.js";
15
16
  const MotionBox = motion(forwardRef((props, ref) => (_jsx(Box, { ref: ref, ...props }))));
16
17
  const SIZE_MAP = {
17
- xs: { btn: "xs", fontSize: CONTROL_SIZES.xs.fontSize, py: 1, px: 2.5, img: "1rem", chevron: CONTROL_SIZES.xs.iconSize, minW: "8rem", gap: 1.5 },
18
- sm: { btn: "sm", fontSize: CONTROL_SIZES.sm.fontSize, py: 1.5, px: 3, img: "1rem", chevron: CONTROL_SIZES.sm.iconSize, minW: "9rem", gap: 2 },
19
- md: { btn: "md", fontSize: CONTROL_SIZES.md.fontSize, py: 2, px: 3, img: "1.125rem", chevron: CONTROL_SIZES.md.iconSize, minW: "11rem", gap: 2 },
20
- lg: { btn: "lg", fontSize: CONTROL_SIZES.lg.fontSize, py: 2.5, px: 3.5, img: "1.25rem", chevron: CONTROL_SIZES.lg.iconSize, minW: "12rem", gap: 2.5 },
21
- xl: { btn: "lg", fontSize: "1.0625rem", py: 3, px: 4, img: "1.4rem", chevron: 20, minW: "13rem", gap: 3 },
18
+ xs: { btn: "xs", img: "1rem", chevron: CONTROL_SIZES.xs.iconSize, minW: "8rem", gap: 1.5, rowSize: "xs" },
19
+ sm: { btn: "sm", img: "1rem", chevron: CONTROL_SIZES.sm.iconSize, minW: "9rem", gap: 2, rowSize: "sm" },
20
+ md: { btn: "md", img: "1.125rem", chevron: CONTROL_SIZES.md.iconSize, minW: "11rem", gap: 2, rowSize: "md" },
21
+ lg: { btn: "lg", img: "1.25rem", chevron: CONTROL_SIZES.lg.iconSize, minW: "12rem", gap: 2.5, rowSize: "lg" },
22
+ xl: { btn: "lg", img: "1.4rem", chevron: 20, minW: "13rem", gap: 3, rowSize: "lg" },
22
23
  };
23
24
  const TRIGGER_TRANSITION = transitionFor(["background-color", "border-color", "color", "box-shadow"]);
24
25
  const ROW_TRANSITION = transitionFor(["background-color", "color"]);
@@ -35,6 +36,7 @@ const Dropdown = forwardRef(({ dropDownButtonStyle, ButtonText, options, handleO
35
36
  const menuRef = useRef(null);
36
37
  const [menuPos, setMenuPos] = useState({ top: 0, left: 0, width: 0 });
37
38
  const s = SIZE_MAP[(_a = size !== null && size !== void 0 ? size : optionsSize) !== null && _a !== void 0 ? _a : "sm"];
39
+ const rowSpec = optionRowSpec(s.rowSize);
38
40
  const prefersReducedMotion = useReducedMotion();
39
41
  const menuId = useId();
40
42
  // Roving-tabindex keyboard nav for the menu (ARIA APG menu-button pattern):
@@ -230,7 +232,7 @@ const Dropdown = forwardRef(({ dropDownButtonStyle, ButtonText, options, handleO
230
232
  const Chevron = isVisibleIconShow ? (_jsx(Box, { as: "span", display: "inline-flex", transition: CHEVRON_TRANSITION, transform: isOpen ? "rotate(180deg)" : "rotate(0deg)", sx: reducedMotionSx(), children: _jsx(ChevronDown, { size: s.chevron }) })) : undefined;
231
233
  const dropDownToggleOption = () => {
232
234
  const ButtonToggle = (_jsx(Button, { ref: buttonTriggerRef, onClick: handleToggle, onKeyDown: handleTriggerKeyDown, size: s.btn, colorScheme: "primary", borderRadius: "lg", fontWeight: 600, "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-controls": menuId, "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : (ButtonText ? undefined : "Open menu"), rightIcon: Chevron, sx: { transition: TRIGGER_TRANSITION, ...reducedMotionSx(), ...dropDownButtonStyle }, ...buttonProps, children: _jsxs(Box, { as: "span", display: "inline-flex", alignItems: "center", gap: DropdownIcon && ButtonText ? s.gap : 0, children: [DropdownIcon, ButtonText] }) }));
233
- const TextToggle = (_jsxs(Flex, { ref: textTriggerRef, onClick: handleToggle, onKeyDown: handleTriggerKeyDown, align: "center", gap: 1.5, cursor: "pointer", fontSize: s.fontSize, fontWeight: 500, color: theme.colors.text[700], borderRadius: "md", transition: ROW_TRANSITION, _hover: { color: theme.colors.accentText }, sx: { ...focusVisibleStyles(theme.colors), ...reducedMotionSx() }, style: dropDownButtonStyle, role: "button", tabIndex: 0, "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-controls": menuId, "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : (text ? undefined : "Open menu"), children: [DropdownIcon, text, Chevron] }));
235
+ const TextToggle = (_jsxs(Flex, { ref: textTriggerRef, onClick: handleToggle, onKeyDown: handleTriggerKeyDown, align: "center", gap: 1.5, cursor: "pointer", fontSize: rowSpec.fontSize, fontWeight: 500, color: theme.colors.text[700], borderRadius: "md", transition: ROW_TRANSITION, _hover: { color: theme.colors.accentText }, sx: { ...focusVisibleStyles(theme.colors), ...reducedMotionSx() }, style: dropDownButtonStyle, role: "button", tabIndex: 0, "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-controls": menuId, "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : (text ? undefined : "Open menu"), children: [DropdownIcon, text, Chevron] }));
234
236
  return dropdownType === "button" ? ButtonToggle : TextToggle;
235
237
  };
236
238
  const DropdownContentRender = () => {
@@ -240,7 +242,7 @@ const Dropdown = forwardRef(({ dropDownButtonStyle, ButtonText, options, handleO
240
242
  }, role: "menuitem", tabIndex: index === activeIndex ? 0 : -1, onClick: () => {
241
243
  handleListItemClick(option.id, option.label);
242
244
  focusTrigger();
243
- }, onKeyDown: (e) => handleItemKeyDown(e, option.id, option.label), mx: 1.5, px: s.px, py: s.py, borderRadius: "md", display: "flex", alignItems: "center", gap: 2, cursor: "pointer", color: theme.colors.text[700], transition: ROW_TRANSITION, _hover: {
245
+ }, onKeyDown: (e) => handleItemKeyDown(e, option.id, option.label), mx: rowSpec.mx, px: rowSpec.px, py: rowSpec.py, minH: rowSpec.minH, borderRadius: OPTION_ROW_RADIUS, display: "flex", alignItems: "center", gap: rowSpec.gap, fontSize: rowSpec.fontSize, cursor: "pointer", color: theme.colors.text[700], transition: ROW_TRANSITION, _hover: {
244
246
  bg: hoverTint,
245
247
  color: theme.colors.accentText,
246
248
  }, _focus: {
@@ -253,7 +255,7 @@ const Dropdown = forwardRef(({ dropDownButtonStyle, ButtonText, options, handleO
253
255
  boxShadow: `inset 0 0 0 2px ${theme.colors.focusRing.ring}`,
254
256
  }, borderBottom: divider
255
257
  ? `0.063rem solid ${theme.colors.boxborder[200]}`
256
- : undefined, sx: { "&:last-child": { borderBottom: "none" }, ...reducedMotionSx() }, style: ItemStyle, fontWeight: 500, children: [option.image && (_jsx(Box, { as: option.image, boxSize: s.img, flexShrink: 0, color: theme.colors.textMuted, style: ImageStyle })), _jsx(Box, { as: "span", fontSize: s.fontSize, isTruncated: true, style: LabelStyle, children: option.label })] }, option.id))) }));
258
+ : undefined, sx: { "&:last-child": { borderBottom: "none" }, ...reducedMotionSx() }, style: ItemStyle, fontWeight: 500, children: [option.image && (_jsx(Box, { as: option.image, boxSize: s.img, flexShrink: 0, color: theme.colors.textMuted, style: ImageStyle })), _jsx(Box, { as: "span", fontSize: rowSpec.fontSize, isTruncated: true, style: LabelStyle, children: option.label })] }, option.id))) }));
257
259
  };
258
260
  return (_jsxs(Box, { ref: dropdownRef, position: "relative", display: "inline-block", children: [dropDownToggleOption(), isOpen && (_jsx(Portal, { children: _jsx(MotionBox, { ref: setMenuRef, position: "fixed", top: `${menuPos.top}px`, left: `${menuPos.left}px`, minW: `${menuPos.width}px`, zIndex: 1500, initial: prefersReducedMotion ? false : { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -6 }, style: headStyle, children: DropdownContentRender() }) }))] }));
259
261
  });
@@ -6,6 +6,8 @@ import { TextLabel } from "../Common/FormLabel.js";
6
6
  import Tag from "../Tag/Tag.js";
7
7
  import { useCustomTheme } from "../../Theme/useCustomTheme.js";
8
8
  import { listRowBackgrounds } from "../../Theme/tokens/builders/rowStates.js";
9
+ import { optionRowSpec } from "../../Theme/tokens/primitives/optionRowSizes.js";
10
+ import { optionRowInk, optionRowWeight } from "../Select/optionRowInk.js";
9
11
  import { useExclusiveOverlay } from "../../Hooks/useExclusiveOverlay.js";
10
12
  import FieldMessage from "../Common/FieldMessage.js";
11
13
  import { useFieldIds } from "../Common/useFieldIds.js";
@@ -37,6 +39,7 @@ const normalizeMultiValue = (val) => {
37
39
  const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOptionLoading = false, isLoading = false, loadingText = "Loading...", placeholder = "Select", value, searchQuery = "", isMultiple = false, isRequired = false, error = false, errorMessage, helperText, width = "100%", height, size = "sm", isDisabled = false, chip, information, isInformation, informationMessage, rightIcon, addNew, isSelectAll = false, pagination, insideSelect, isRemoveAllIcon, "aria-label": ariaLabel, }) => {
38
40
  var _a, _b, _c, _d, _e;
39
41
  const s = getSearchSelectSizeStyles(size);
42
+ const spec = optionRowSpec(size);
40
43
  const emitChange = onChange !== null && onChange !== void 0 ? onChange : onSelect;
41
44
  const insideSelectBoxHeight = height !== null && height !== void 0 ? height : s.insideSelectDefaultHeight;
42
45
  const [inputValue, setInputValue] = useState(searchQuery);
@@ -50,6 +53,9 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
50
53
  const dropdownMenuRef = useRef(null);
51
54
  const [dropdownPos, setDropdownPos] = useState({ top: 0, left: 0, width: 0 });
52
55
  const scrollTimeoutRef = useRef(null);
56
+ const optionsLengthRef = useRef(options.length);
57
+ const lastPageStartRef = useRef(null);
58
+ const optionsListRef = useRef(null);
53
59
  const [focused, setFocused] = useState(false);
54
60
  const [customSelectOpen, setCustomSelectOpen] = useState(false);
55
61
  const customSelectRef = useRef(null);
@@ -100,10 +106,22 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
100
106
  return () => clearTimeout(delayDebounce);
101
107
  }, [inputValue]);
102
108
  useEffect(() => {
103
- if (pagination === null || pagination === void 0 ? void 0 : pagination.limit) {
104
- setHasMore(options.length % pagination.limit === 0);
109
+ optionsLengthRef.current = options.length;
110
+ }, [options.length]);
111
+ useEffect(() => {
112
+ if ((pagination === null || pagination === void 0 ? void 0 : pagination.hasMore) !== undefined) {
113
+ setHasMore(pagination.hasMore);
114
+ return;
115
+ }
116
+ if ((pagination === null || pagination === void 0 ? void 0 : pagination.totalRecords) !== undefined) {
117
+ setHasMore(options.length < pagination.totalRecords);
118
+ return;
105
119
  }
106
- }, [options.length, pagination === null || pagination === void 0 ? void 0 : pagination.limit]);
120
+ if (!(pagination === null || pagination === void 0 ? void 0 : pagination.onScroll))
121
+ return;
122
+ const limit = pagination.limit || 10;
123
+ setHasMore(options.length % limit === 0);
124
+ }, [options.length, pagination === null || pagination === void 0 ? void 0 : pagination.hasMore, pagination === null || pagination === void 0 ? void 0 : pagination.totalRecords, pagination === null || pagination === void 0 ? void 0 : pagination.limit, pagination === null || pagination === void 0 ? void 0 : pagination.onScroll]);
107
125
  const handleSelect = (option) => {
108
126
  var _a;
109
127
  if (isMultiple && insideSelect) {
@@ -283,6 +301,13 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
283
301
  }
284
302
  setActiveIndex((prev) => (prev >= 0 && prev < filteredOptions.length ? prev : -1));
285
303
  }, [isOpen, filteredOptions.length]);
304
+ useEffect(() => {
305
+ var _a, _b;
306
+ if (!isOpen || activeIndex < 0 || !optionsListRef.current)
307
+ return;
308
+ (_b = (_a = optionsListRef.current
309
+ .querySelector(`[data-index="${activeIndex}"]`)) === null || _a === void 0 ? void 0 : _a.scrollIntoView) === null || _b === void 0 ? void 0 : _b.call(_a, { block: "nearest" });
310
+ }, [isOpen, activeIndex]);
286
311
  // NOTE: only restores focus when a keyboard close actually unmounted Input and dropped focus to body.
287
312
  useEffect(() => {
288
313
  var _a, _b, _c, _d;
@@ -313,11 +338,30 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
313
338
  clearTimeout(scrollTimeoutRef.current);
314
339
  scrollTimeoutRef.current = setTimeout(() => {
315
340
  var _a;
316
- const startIndex = options.length + 1;
341
+ scrollTimeoutRef.current = null;
342
+ const startIndex = optionsLengthRef.current + 1;
343
+ if (lastPageStartRef.current === startIndex) {
344
+ setHasMore(false);
345
+ return;
346
+ }
347
+ lastPageStartRef.current = startIndex;
317
348
  (_a = pagination.onScroll) === null || _a === void 0 ? void 0 : _a.call(pagination, startIndex, pagination.limit || 10, inputValue);
318
349
  }, 200);
319
350
  }
320
- }, [pagination, options.length, inputValue, hasMore]);
351
+ }, [pagination, inputValue, hasMore]);
352
+ useEffect(() => {
353
+ if (isOpen)
354
+ return;
355
+ if (scrollTimeoutRef.current) {
356
+ clearTimeout(scrollTimeoutRef.current);
357
+ scrollTimeoutRef.current = null;
358
+ }
359
+ lastPageStartRef.current = null;
360
+ }, [isOpen]);
361
+ useEffect(() => () => {
362
+ if (scrollTimeoutRef.current)
363
+ clearTimeout(scrollTimeoutRef.current);
364
+ }, []);
321
365
  useOutsideClick({
322
366
  ref: customSelectRef,
323
367
  handler: (e) => {
@@ -456,16 +500,14 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
456
500
  }, children: [_jsxs(Box, { minW: "4.5rem", cursor: "pointer", display: "flex", justifyContent: "space-between", alignItems: "center", gap: 1, width: insideSelect.width, height: insideSelectBoxHeight, pl: "0.5rem", pr: "0.375rem", children: [insideLabel ? (_jsx(SelectTruncatedLabel, { label: insideLabel, maxWidth: insideSelect.width || "6rem", fontSize: s.fieldFontSize })) : (_jsx(Text, { fontSize: s.fieldFontSize, fontWeight: 500, color: theme.colors.textMuted, isTruncated: true, maxW: insideSelect.width || "6rem", children: insidePlaceholder })), _jsx(Box, { as: "span", display: "inline-flex", flexShrink: 0, color: customSelectOpen ? theme.colors.accentText : theme.colors.textMuted, transition: CHEVRON_TRANSITION, transform: customSelectOpen ? "rotate(180deg)" : "rotate(0deg)", children: _jsx(ChevronDown, { size: s.chevronSize }) })] }), customSelectOpen && (_jsx(Portal, { children: _jsx(Box, { ref: setInsideSelectMenuRef, id: insideSelectPortalId, position: "fixed", top: `${customSelectPos.top}px`, left: `${customSelectPos.left}px`, zIndex: 1510, bg: theme.colors.surfaceRaised, border: "0.063rem solid", borderColor: theme.colors.boxborder[200], borderRadius: "xl", boxShadow: theme.shadows.lg, minW: `${customSelectPos.width}px`, maxW: "20rem", py: 1, overflow: "hidden", children: (_c = insideSelect.option) === null || _c === void 0 ? void 0 : _c.map((item) => {
457
501
  var _a;
458
502
  const selected = ((_a = insideSelect.value) === null || _a === void 0 ? void 0 : _a.id) === item.id;
459
- return (_jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, minH: s.optionRowMinH, py: s.optionRowPy, px: "0.75rem", fontSize: s.dropdownTextFontSize, fontWeight: selected ? 600 : 500, color: selected
460
- ? theme.colors.accentText
461
- : theme.colors.text[700], bg: selected ? rowBackgrounds.selected : "transparent", _hover: {
503
+ return (_jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "space-between", gap: spec.gap, minH: spec.minH, py: spec.py, px: spec.px, fontSize: spec.fontSize, fontWeight: optionRowWeight(selected), color: optionRowInk(theme.colors, selected), bg: selected ? rowBackgrounds.selected : "transparent", _hover: {
462
504
  bg: selected
463
505
  ? rowBackgrounds.selectedActive
464
506
  : rowBackgrounds.active,
465
507
  }, cursor: "pointer", onClick: (e) => {
466
508
  e.stopPropagation();
467
509
  handleCustomSelect(item);
468
- }, children: [_jsx(Text, { isTruncated: true, children: item.label }), selected && (_jsx(Check, { size: s.chevronSize - 2, color: theme.colors.accentText }))] }, item.id));
510
+ }, children: [_jsx(Text, { isTruncated: true, children: item.label }), selected && (_jsx(Check, { size: spec.iconSize, color: theme.colors.accentText }))] }, item.id));
469
511
  }) }) }))] }));
470
512
  })(), isMultiple
471
513
  ? selectedValues
@@ -497,16 +539,20 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
497
539
  else {
498
540
  openMenu();
499
541
  }
500
- }, "aria-label": "toggle-dropdown", display: "flex", alignItems: "center", borderRadius: "sm", color: theme.colors.textMuted, cursor: isDisabled ? "not-allowed" : "pointer", sx: focusVisibleStyles(theme.colors), children: isOpen ? (_jsx(ChevronUp, { size: s.chevronSize })) : (_jsx(ChevronDown, { size: s.chevronSize })) }))] })] }) }), isOpen && (_jsx(Portal, { children: _jsxs(Box, { ref: setDropdownMenuRef, id: listboxId, role: "listbox", "aria-multiselectable": isMultiple || undefined, position: "fixed", top: `${dropdownPos.top}px`, left: `${dropdownPos.left}px`, width: `${dropdownPos.width}px`, zIndex: 1500, maxH: "20rem", borderWidth: 1, borderColor: theme.colors.boxborder[200], borderRadius: "xl", overflow: "hidden", bg: theme.colors.surfaceRaised, boxShadow: theme.shadows.lg, display: "flex", flexDirection: "column", children: [isMultiple && isSelectAll && (_jsxs(Box, { py: s.optionRowPy, px: 3, cursor: "pointer", onClick: handleSelectAll, display: "flex", alignItems: "center", gap: 2, color: theme.colors.text[700], borderBottom: `0.063rem solid ${theme.colors.boxborder[200]}`, children: [_jsx(Checkbox, { isChecked: allFilteredSelected, sx: { pointerEvents: "none" }, size: s.checkboxSize }), _jsx(Text, { fontSize: s.dropdownTextFontSize, children: allFilteredSelected ? "Unselect All" : "Select All" })] })), _jsx(Box, { flex: "1", overflowY: "auto", onScroll: handleScroll, maxH: "15rem", borderBottom: "0.063rem solid", borderColor: theme.colors.boxborder[200], children: isOptionLoading && filteredOptions.length === 0 ? (_jsxs(Box, { py: s.optionRowPy, px: 3, display: "flex", alignItems: "center", gap: 2, minH: s.optionRowMinH, color: theme.colors.textMuted, children: [_jsx(Spinner, { size: s.spinnerSize }), _jsx(Text, { fontSize: s.dropdownTextFontSize, children: loadingText })] })) : !isOptionLoading && filteredOptions.length === 0 ? (_jsx(Text, { p: 3, fontSize: s.dropdownTextFontSize, color: theme.colors.textMuted, children: "No results found" })) : (_jsxs(_Fragment, { children: [filteredOptions.map((option, index) => (_jsxs(Box, { id: getOptionId(index), role: "option", "aria-selected": isSelected(option.id), py: s.optionRowPy, px: 3, minH: s.optionRowMinH, cursor: "pointer", bg: isSelected(option.id)
501
- ? index === activeIndex
502
- ? rowBackgrounds.selectedActive
503
- : rowBackgrounds.selected
504
- : index === activeIndex
505
- ? rowBackgrounds.active
506
- : "transparent", _hover: {
507
- bg: isSelected(option.id)
508
- ? rowBackgrounds.selectedActive
509
- : rowBackgrounds.active,
510
- }, onClick: () => handleSelect(option), onMouseEnter: () => setActiveIndex(index), display: "flex", alignItems: "center", gap: 2, color: isSelected(option.id) ? theme.colors.accentText : theme.colors.text[700], fontWeight: isSelected(option.id) ? 600 : 400, borderBottom: `0.063rem solid ${theme.colors.boxborder[200]}`, children: [isMultiple && (_jsx(Checkbox, { isChecked: isSelected(option.id), sx: { pointerEvents: "none" }, size: s.checkboxSize })), option.view ? (_jsx(_Fragment, { children: option.view })) : (_jsx(Text, { fontSize: s.dropdownTextFontSize, children: option.label }))] }, option.id))), ((pagination === null || pagination === void 0 ? void 0 : pagination.scrollLoading) || (isOptionLoading && filteredOptions.length > 0)) && (_jsxs(Box, { py: s.optionRowPy, px: 3, display: "flex", alignItems: "center", justifyContent: "center", gap: 2, minH: s.optionRowMinH, color: theme.colors.textMuted, children: [_jsx(Spinner, { size: s.spinnerSize }), _jsx(Text, { fontSize: s.dropdownTextFontSize, children: loadingText })] }))] })) }), (addNew === null || addNew === void 0 ? void 0 : addNew.enabled) && (_jsxs(Box, { py: s.optionRowPy, px: 3, borderTop: "0.063rem solid", borderColor: theme.colors.boxborder[200], bg: theme.colors.surfaceRaised, display: "flex", alignItems: "center", gap: 2, color: theme.colors.accentText, fontWeight: 500, minH: s.optionRowMinH, cursor: "pointer", onClick: addNew.onClick, _hover: { bg: rowBackgrounds.active }, children: [addNew.icon || _jsx(Plus, { size: s.addNewIconSize }), _jsx(Text, { fontSize: s.dropdownTextFontSize, children: addNew.text || "Add New" })] }))] }) })), _jsx(FieldMessage, { error: error, errorMessage: errorMessage, helperText: helperText, helperId: ids.helperId, errorId: ids.errorId })] }));
542
+ }, "aria-label": "toggle-dropdown", display: "flex", alignItems: "center", borderRadius: "sm", color: theme.colors.textMuted, cursor: isDisabled ? "not-allowed" : "pointer", sx: focusVisibleStyles(theme.colors), children: isOpen ? (_jsx(ChevronUp, { size: s.chevronSize })) : (_jsx(ChevronDown, { size: s.chevronSize })) }))] })] }) }), isOpen && (_jsx(Portal, { children: _jsxs(Box, { ref: setDropdownMenuRef, id: listboxId, role: "listbox", "aria-multiselectable": isMultiple || undefined, position: "fixed", top: `${dropdownPos.top}px`, left: `${dropdownPos.left}px`, width: `${dropdownPos.width}px`, zIndex: 1500, maxH: "20rem", borderWidth: 1, borderColor: theme.colors.boxborder[200], borderRadius: "xl", overflow: "hidden", bg: theme.colors.surfaceRaised, boxShadow: theme.shadows.lg, display: "flex", flexDirection: "column", children: [isMultiple && isSelectAll && (_jsxs(Box, { py: spec.py, px: spec.px, minH: spec.minH, cursor: "pointer", onClick: handleSelectAll, display: "flex", alignItems: "center", gap: spec.gap, color: theme.colors.text[700], borderBottom: `0.063rem solid ${theme.colors.boxborder[200]}`, children: [_jsx(Checkbox, { isChecked: allFilteredSelected, sx: { pointerEvents: "none" }, size: s.checkboxSize }), _jsx(Text, { fontSize: spec.fontSize, children: allFilteredSelected ? "Unselect All" : "Select All" })] })), _jsx(Box, { ref: optionsListRef, flex: "1", overflowY: "auto", onScroll: handleScroll, maxH: "15rem", borderBottom: "0.063rem solid", borderColor: theme.colors.boxborder[200], children: isOptionLoading && filteredOptions.length === 0 ? (_jsxs(Box, { py: spec.py, px: spec.px, display: "flex", alignItems: "center", gap: spec.gap, minH: spec.minH, color: theme.colors.textMuted, children: [_jsx(Spinner, { size: s.spinnerSize }), _jsx(Text, { fontSize: spec.fontSize, children: loadingText })] })) : !isOptionLoading && filteredOptions.length === 0 ? (_jsx(Text, { p: 3, fontSize: spec.fontSize, color: theme.colors.textMuted, children: "No results found" })) : (_jsxs(_Fragment, { children: [filteredOptions.map((option, index) => {
543
+ const selected = isSelected(option.id);
544
+ const singleSelected = selected && !isMultiple;
545
+ return (_jsxs(Box, { id: getOptionId(index), "data-index": index, role: "option", "aria-selected": selected, py: spec.py, px: spec.px, minH: spec.minH, cursor: "pointer", bg: selected
546
+ ? index === activeIndex
547
+ ? rowBackgrounds.selectedActive
548
+ : rowBackgrounds.selected
549
+ : index === activeIndex
550
+ ? rowBackgrounds.active
551
+ : "transparent", _hover: {
552
+ bg: selected ? rowBackgrounds.selectedActive : rowBackgrounds.active,
553
+ }, onClick: () => handleSelect(option), onMouseEnter: () => setActiveIndex(index), display: "flex", alignItems: "center", gap: spec.gap, color: optionRowInk(theme.colors, singleSelected),
554
+ // `view` is consumer JSX with no size of its own; it inherited the 16px body font.
555
+ fontSize: spec.fontSize, fontWeight: optionRowWeight(singleSelected), borderBottom: `0.063rem solid ${theme.colors.boxborder[200]}`, children: [isMultiple && (_jsx(Checkbox, { isChecked: selected, sx: { pointerEvents: "none" }, size: s.checkboxSize })), option.view ? (_jsx(_Fragment, { children: option.view })) : (_jsx(Text, { isTruncated: true, children: option.label })), singleSelected && (_jsx(Box, { as: Check, size: spec.iconSize, color: theme.colors.accentText, flexShrink: 0 }))] }, option.id));
556
+ }), ((pagination === null || pagination === void 0 ? void 0 : pagination.scrollLoading) || (isOptionLoading && filteredOptions.length > 0)) && (_jsxs(Box, { py: spec.py, px: spec.px, display: "flex", alignItems: "center", justifyContent: "center", gap: spec.gap, minH: spec.minH, color: theme.colors.textMuted, children: [_jsx(Spinner, { size: s.spinnerSize }), _jsx(Text, { fontSize: spec.fontSize, children: loadingText })] }))] })) }), (addNew === null || addNew === void 0 ? void 0 : addNew.enabled) && (_jsxs(Box, { py: spec.py, px: spec.px, borderTop: "0.063rem solid", borderColor: theme.colors.boxborder[200], bg: theme.colors.surfaceRaised, display: "flex", alignItems: "center", gap: spec.gap, color: theme.colors.accentText, fontWeight: 500, minH: spec.minH, cursor: "pointer", onClick: addNew.onClick, _hover: { bg: rowBackgrounds.active }, children: [addNew.icon || _jsx(Plus, { size: s.addNewIconSize }), _jsx(Text, { fontSize: spec.fontSize, children: addNew.text || "Add New" })] }))] }) })), _jsx(FieldMessage, { error: error, errorMessage: errorMessage, helperText: helperText, helperId: ids.helperId, errorId: ids.errorId })] }));
511
557
  };
512
558
  export default SearchSelect;
@@ -18,9 +18,6 @@ const SIZE_STYLES = {
18
18
  clearIconSize: 10,
19
19
  addNewIconSize: 14,
20
20
  insideSelectMarginLeft: "-0.5rem",
21
- dropdownTextFontSize: "xs",
22
- optionRowMinH: "1.5rem",
23
- optionRowPy: 1.5,
24
21
  checkboxSize: "sm",
25
22
  },
26
23
  sm: {
@@ -39,9 +36,6 @@ const SIZE_STYLES = {
39
36
  clearIconSize: 12,
40
37
  addNewIconSize: 16,
41
38
  insideSelectMarginLeft: "-0.625rem",
42
- dropdownTextFontSize: "sm",
43
- optionRowMinH: "2rem",
44
- optionRowPy: 2,
45
39
  checkboxSize: "sm",
46
40
  },
47
41
  md: {
@@ -60,9 +54,6 @@ const SIZE_STYLES = {
60
54
  clearIconSize: 12,
61
55
  addNewIconSize: 16,
62
56
  insideSelectMarginLeft: "-0.75rem",
63
- dropdownTextFontSize: "sm",
64
- optionRowMinH: "2.5rem",
65
- optionRowPy: 2,
66
57
  checkboxSize: "md",
67
58
  },
68
59
  lg: {
@@ -81,9 +72,6 @@ const SIZE_STYLES = {
81
72
  clearIconSize: 14,
82
73
  addNewIconSize: 18,
83
74
  insideSelectMarginLeft: "-0.875rem",
84
- dropdownTextFontSize: "md",
85
- optionRowMinH: "2.75rem",
86
- optionRowPy: 2.5,
87
75
  checkboxSize: "lg",
88
76
  },
89
77
  };