@pushwoosh/dumb-components 1.1.140 → 1.1.141

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 (45) hide show
  1. package/Combobox/Combobox.d.ts +4 -0
  2. package/Combobox/Combobox.js +254 -0
  3. package/Combobox/index.d.ts +2 -0
  4. package/Combobox/index.js +1 -0
  5. package/Combobox/types.d.ts +38 -0
  6. package/Combobox/types.js +1 -0
  7. package/ComboboxMulti/ComboboxMulti.d.ts +3 -0
  8. package/ComboboxMulti/ComboboxMulti.js +194 -0
  9. package/ComboboxMulti/index.d.ts +2 -0
  10. package/ComboboxMulti/index.js +1 -0
  11. package/ComboboxMulti/types.d.ts +32 -0
  12. package/ComboboxMulti/types.js +1 -0
  13. package/hooks/useLayer.js +41 -14
  14. package/hooks/useOnClickOutside.js +6 -5
  15. package/index.d.ts +2 -0
  16. package/index.js +2 -0
  17. package/package.json +2 -2
  18. package/shared/dropdownField/FieldInput.d.ts +5 -0
  19. package/shared/dropdownField/FieldInput.js +43 -0
  20. package/shared/dropdownField/FieldShell.d.ts +30 -0
  21. package/shared/dropdownField/FieldShell.js +99 -0
  22. package/shared/dropdownField/SuggestionsDropdown.d.ts +16 -0
  23. package/shared/dropdownField/SuggestionsDropdown.js +68 -0
  24. package/shared/dropdownField/constants.d.ts +6 -0
  25. package/shared/dropdownField/constants.js +6 -0
  26. package/shared/dropdownField/index.d.ts +12 -0
  27. package/shared/dropdownField/index.js +11 -0
  28. package/shared/dropdownField/shared.d.ts +4 -0
  29. package/shared/dropdownField/shared.js +18 -0
  30. package/shared/dropdownField/styles.d.ts +24 -0
  31. package/shared/dropdownField/styles.js +81 -0
  32. package/shared/dropdownField/types.d.ts +27 -0
  33. package/shared/dropdownField/types.js +1 -0
  34. package/shared/dropdownField/useActiveIndex.d.ts +2 -0
  35. package/shared/dropdownField/useActiveIndex.js +12 -0
  36. package/shared/dropdownField/useAsyncItems.d.ts +11 -0
  37. package/shared/dropdownField/useAsyncItems.js +46 -0
  38. package/shared/dropdownField/useDebouncedValue.d.ts +1 -0
  39. package/shared/dropdownField/useDebouncedValue.js +13 -0
  40. package/shared/dropdownField/useDropdownItems.d.ts +16 -0
  41. package/shared/dropdownField/useDropdownItems.js +36 -0
  42. package/shared/dropdownField/useDropdownPosition.d.ts +10 -0
  43. package/shared/dropdownField/useDropdownPosition.js +40 -0
  44. package/shared/dropdownField/useListNavigation.d.ts +16 -0
  45. package/shared/dropdownField/useListNavigation.js +45 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/dumb-components",
3
- "version": "1.1.140",
3
+ "version": "1.1.141",
4
4
  "description": "React components to build Pushwoosh products",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -47,7 +47,7 @@
47
47
  "@codemirror/lang-liquid": "^6.2.2",
48
48
  "@floating-ui/react-dom": "^2.1.2",
49
49
  "@lezer/highlight": "^1.2.1",
50
- "@pushwoosh/kit-constants": "^1.6.14",
50
+ "@pushwoosh/kit-constants": "^1.7.0",
51
51
  "@pushwoosh/kit-helpers": "^4.11.0",
52
52
  "@pushwoosh/kit-icons": "^2.1.16",
53
53
  "@pushwoosh/kit-typography": "^1.7.5",
@@ -0,0 +1,5 @@
1
+ import { type InputHTMLAttributes } from 'react';
2
+ export declare const FieldInput: import("react").ForwardRefExoticComponent<InputHTMLAttributes<HTMLInputElement> & {
3
+ value: string;
4
+ autosize?: boolean;
5
+ } & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useLayoutEffect, useRef, useState } from 'react';
3
+ import { AutosizeMirror, Input } from './styles';
4
+ export const FieldInput = forwardRef(({
5
+ autosize,
6
+ value,
7
+ placeholder,
8
+ ...rest
9
+ }, ref) => {
10
+ const mirrorRef = useRef(null);
11
+ const [width, setWidth] = useState(0);
12
+ useLayoutEffect(() => {
13
+ if (!autosize) return;
14
+ const el = mirrorRef.current;
15
+ if (el) setWidth(el.offsetWidth);
16
+ }, [autosize, value, placeholder]);
17
+ if (!autosize) {
18
+ return _jsx(Input, {
19
+ ref: ref,
20
+ value: value,
21
+ placeholder: placeholder,
22
+ ...rest
23
+ });
24
+ }
25
+ return _jsxs(_Fragment, {
26
+ children: [_jsx(Input, {
27
+ ref: ref,
28
+ value: value,
29
+ placeholder: placeholder,
30
+ style: {
31
+ flex: 'none',
32
+ minWidth: 0,
33
+ width: Math.max(width + 2, 8)
34
+ },
35
+ ...rest
36
+ }), _jsx(AutosizeMirror, {
37
+ ref: mirrorRef,
38
+ "aria-hidden": true,
39
+ children: value || placeholder || ''
40
+ })]
41
+ });
42
+ });
43
+ FieldInput.displayName = 'FieldInput';
@@ -0,0 +1,30 @@
1
+ import { type MouseEvent, type ReactElement, type ReactNode } from 'react';
2
+ import { type DropdownItemDescriptor } from './types';
3
+ type FieldShellProps = {
4
+ children: ReactNode;
5
+ isOpen?: boolean;
6
+ dropdownOpen?: boolean;
7
+ isErrored?: boolean;
8
+ disabled?: boolean;
9
+ loading?: boolean;
10
+ width?: string;
11
+ minWidth?: string;
12
+ autosize?: boolean;
13
+ tightLeft?: boolean;
14
+ onShellMouseDown?: (event: MouseEvent<HTMLDivElement>) => void;
15
+ onClose?: () => void;
16
+ showClear?: boolean;
17
+ onClear?: () => void;
18
+ clearLabel?: string;
19
+ showChevron?: boolean;
20
+ onToggle?: () => void;
21
+ dropdownItems: DropdownItemDescriptor[];
22
+ emptyText?: ReactNode;
23
+ createLabel?: ReactNode;
24
+ createActive?: boolean;
25
+ onCreate?: () => void;
26
+ listboxId?: string;
27
+ createId?: string;
28
+ };
29
+ export declare function FieldShell({ children, isOpen, dropdownOpen, isErrored, disabled, loading, width, minWidth, autosize, tightLeft, onShellMouseDown, onClose, showClear, onClear, clearLabel, showChevron, onToggle, dropdownItems, emptyText, createLabel, createActive, onCreate, listboxId, createId, }: FieldShellProps): ReactElement;
30
+ export {};
@@ -0,0 +1,99 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { ChevronIcon, CloseIcon } from '@pushwoosh/kit-icons';
3
+ import { IndicatorButton, Indicators, Shell, ShellInner } from './styles';
4
+ import { SuggestionsDropdown } from './SuggestionsDropdown';
5
+ import { useDropdownPosition } from './useDropdownPosition';
6
+ import { useLayer } from '../../hooks';
7
+ export function FieldShell({
8
+ children,
9
+ isOpen,
10
+ dropdownOpen,
11
+ isErrored,
12
+ disabled,
13
+ loading,
14
+ width,
15
+ minWidth,
16
+ autosize,
17
+ tightLeft,
18
+ onShellMouseDown,
19
+ onClose,
20
+ showClear,
21
+ onClear,
22
+ clearLabel = 'Clear',
23
+ showChevron,
24
+ onToggle,
25
+ dropdownItems,
26
+ emptyText,
27
+ createLabel,
28
+ createActive,
29
+ onCreate,
30
+ listboxId,
31
+ createId
32
+ }) {
33
+ const {
34
+ setReference,
35
+ setFloating,
36
+ floatingStyles
37
+ } = useDropdownPosition({
38
+ isOpen: dropdownOpen,
39
+ onClickOutside: onClose
40
+ });
41
+ const {
42
+ zIndex
43
+ } = useLayer({
44
+ isOpen: !!dropdownOpen,
45
+ onClose
46
+ });
47
+ return _jsxs(_Fragment, {
48
+ children: [_jsxs(Shell, {
49
+ ref: setReference,
50
+ "$isFocused": isOpen,
51
+ "$isErrored": isErrored,
52
+ "$isDisabled": disabled,
53
+ "$isLoading": loading,
54
+ "$width": width,
55
+ "$minWidth": minWidth,
56
+ "$autosize": autosize,
57
+ "$tightLeft": tightLeft,
58
+ onMouseDown: onShellMouseDown,
59
+ children: [_jsx(ShellInner, {
60
+ children: children
61
+ }), _jsxs(Indicators, {
62
+ children: [showClear && !disabled && _jsx(IndicatorButton, {
63
+ onMouseDown: e => {
64
+ e.preventDefault();
65
+ e.stopPropagation();
66
+ onClear === null || onClear === void 0 || onClear();
67
+ },
68
+ "aria-label": clearLabel,
69
+ children: _jsx(CloseIcon, {
70
+ size: "small"
71
+ })
72
+ }), showChevron && _jsx(IndicatorButton, {
73
+ tabIndex: -1,
74
+ "aria-label": isOpen ? 'Close' : 'Open',
75
+ onMouseDown: e => {
76
+ e.preventDefault();
77
+ e.stopPropagation();
78
+ onToggle === null || onToggle === void 0 || onToggle();
79
+ },
80
+ children: _jsx(ChevronIcon, {
81
+ direction: isOpen ? 'up' : 'down',
82
+ size: "small"
83
+ })
84
+ })]
85
+ })]
86
+ }), dropdownOpen && _jsx(SuggestionsDropdown, {
87
+ floatingRef: setFloating,
88
+ floatingStyles: floatingStyles,
89
+ items: dropdownItems,
90
+ emptyText: emptyText,
91
+ createLabel: createLabel,
92
+ createActive: createActive,
93
+ onCreate: onCreate,
94
+ listboxId: listboxId,
95
+ createId: createId,
96
+ zIndex: zIndex
97
+ })]
98
+ });
99
+ }
@@ -0,0 +1,16 @@
1
+ import { type CSSProperties, type ReactElement, type ReactNode, type Ref } from 'react';
2
+ import { type DropdownItemDescriptor } from './types';
3
+ type SuggestionsDropdownProps = {
4
+ floatingRef: Ref<HTMLDivElement>;
5
+ floatingStyles: CSSProperties;
6
+ items: DropdownItemDescriptor[];
7
+ emptyText?: ReactNode;
8
+ createLabel?: ReactNode;
9
+ createActive?: boolean;
10
+ onCreate?: () => void;
11
+ listboxId?: string;
12
+ createId?: string;
13
+ zIndex?: number;
14
+ };
15
+ export declare function SuggestionsDropdown({ floatingRef, floatingStyles, items, emptyText, createLabel, createActive, onCreate, listboxId, createId, zIndex, }: SuggestionsDropdownProps): ReactElement | null;
16
+ export {};
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { CreateItemLabel, Dropdown, DropdownEmpty, DropdownItem } from './styles';
5
+ const CREATE_KEY = '__create__';
6
+ export function SuggestionsDropdown({
7
+ floatingRef,
8
+ floatingStyles,
9
+ items,
10
+ emptyText = 'No options',
11
+ createLabel,
12
+ createActive,
13
+ onCreate,
14
+ listboxId,
15
+ createId,
16
+ zIndex
17
+ }) {
18
+ const activeItemRef = useRef(null);
19
+ const activeItem = items.find(item => item.active);
20
+ const activeKey = createActive ? CREATE_KEY : (activeItem === null || activeItem === void 0 ? void 0 : activeItem.key) ?? null;
21
+ useEffect(() => {
22
+ var _activeItemRef$curren;
23
+ if (activeKey === null) return;
24
+ (_activeItemRef$curren = activeItemRef.current) === null || _activeItemRef$curren === void 0 || _activeItemRef$curren.scrollIntoView({
25
+ block: 'nearest'
26
+ });
27
+ }, [activeKey]);
28
+ if (typeof document === 'undefined') return null;
29
+ const isEmpty = items.length === 0 && !createLabel;
30
+ return createPortal(_jsxs(Dropdown, {
31
+ ref: floatingRef,
32
+ style: {
33
+ ...floatingStyles,
34
+ zIndex
35
+ },
36
+ id: listboxId,
37
+ role: "listbox",
38
+ children: [createLabel && _jsx(DropdownItem, {
39
+ ref: createActive ? activeItemRef : undefined,
40
+ id: createId,
41
+ role: "option",
42
+ "aria-selected": false,
43
+ "$active": createActive,
44
+ onMouseDown: e => {
45
+ e.preventDefault();
46
+ onCreate === null || onCreate === void 0 || onCreate();
47
+ },
48
+ children: _jsx(CreateItemLabel, {
49
+ children: createLabel
50
+ })
51
+ }), items.map(item => _jsx(DropdownItem, {
52
+ ref: item.active ? activeItemRef : undefined,
53
+ id: item.id,
54
+ role: "option",
55
+ "aria-selected": !!item.selected,
56
+ "$active": item.active,
57
+ "$selected": item.selected,
58
+ onMouseDown: e => {
59
+ var _item$onSelect;
60
+ e.preventDefault();
61
+ (_item$onSelect = item.onSelect) === null || _item$onSelect === void 0 || _item$onSelect.call(item);
62
+ },
63
+ children: item.label
64
+ }, item.key)), isEmpty && _jsx(DropdownEmpty, {
65
+ children: emptyText
66
+ })]
67
+ }), document.body);
68
+ }
@@ -0,0 +1,6 @@
1
+ import { Color } from '@pushwoosh/kit-constants';
2
+ export declare const LOADING_BORDER_COLORS: readonly [Color.PRIMARY, Color.BRIGHT, Color.BRAND_GREEN, Color.EXCEPTIONAL];
3
+ export declare const LOADING_BORDER_ANIMATION_MS = 3000;
4
+ export declare const DROPDOWN_MAX_HEIGHT = 280;
5
+ export declare const DROPDOWN_MAX_WIDTH = 320;
6
+ export declare const DEFAULT_DEBOUNCE_MS = 250;
@@ -0,0 +1,6 @@
1
+ import { Color } from '@pushwoosh/kit-constants';
2
+ export const LOADING_BORDER_COLORS = [Color.PRIMARY, Color.BRIGHT, Color.BRAND_GREEN, Color.EXCEPTIONAL];
3
+ export const LOADING_BORDER_ANIMATION_MS = 3000;
4
+ export const DROPDOWN_MAX_HEIGHT = 280;
5
+ export const DROPDOWN_MAX_WIDTH = 320;
6
+ export const DEFAULT_DEBOUNCE_MS = 250;
@@ -0,0 +1,12 @@
1
+ export { SuggestionsDropdown } from './SuggestionsDropdown';
2
+ export { FieldShell } from './FieldShell';
3
+ export { FieldInput } from './FieldInput';
4
+ export { useDropdownPosition } from './useDropdownPosition';
5
+ export { useDebouncedValue } from './useDebouncedValue';
6
+ export { useAsyncItems } from './useAsyncItems';
7
+ export { useDropdownItems } from './useDropdownItems';
8
+ export { useActiveIndex } from './useActiveIndex';
9
+ export { useListNavigation } from './useListNavigation';
10
+ export { defaultGetKey, defaultLabelFilter, useShellMouseDown } from './shared';
11
+ export { DEFAULT_DEBOUNCE_MS } from './constants';
12
+ export type { ItemState, DropdownItemDescriptor, GetKeyProp, ItemsSource, } from './types';
@@ -0,0 +1,11 @@
1
+ export { SuggestionsDropdown } from './SuggestionsDropdown';
2
+ export { FieldShell } from './FieldShell';
3
+ export { FieldInput } from './FieldInput';
4
+ export { useDropdownPosition } from './useDropdownPosition';
5
+ export { useDebouncedValue } from './useDebouncedValue';
6
+ export { useAsyncItems } from './useAsyncItems';
7
+ export { useDropdownItems } from './useDropdownItems';
8
+ export { useActiveIndex } from './useActiveIndex';
9
+ export { useListNavigation } from './useListNavigation';
10
+ export { defaultGetKey, defaultLabelFilter, useShellMouseDown } from './shared';
11
+ export { DEFAULT_DEBOUNCE_MS } from './constants';
@@ -0,0 +1,4 @@
1
+ import { type MouseEvent, type RefObject } from 'react';
2
+ export declare function defaultGetKey<T>(item: T): string | number;
3
+ export declare function defaultLabelFilter<T>(item: T, query: string, getLabel: (item: T) => string): boolean;
4
+ export declare function useShellMouseDown(inputRef: RefObject<HTMLInputElement>, isOpen: boolean, open: () => void): (event: MouseEvent<HTMLDivElement>) => void;
@@ -0,0 +1,18 @@
1
+ import { useCallback } from 'react';
2
+ export function defaultGetKey(item) {
3
+ return item;
4
+ }
5
+ export function defaultLabelFilter(item, query, getLabel) {
6
+ return getLabel(item).toLowerCase().includes(query.toLowerCase());
7
+ }
8
+ export function useShellMouseDown(inputRef, isOpen, open) {
9
+ return useCallback(event => {
10
+ var _inputRef$current;
11
+ const target = event.target;
12
+ if (target instanceof HTMLInputElement) return;
13
+ if (target.closest('button')) return;
14
+ event.preventDefault();
15
+ (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
16
+ if (!isOpen) open();
17
+ }, [inputRef, isOpen, open]);
18
+ }
@@ -0,0 +1,24 @@
1
+ export declare const Shell: import("styled-components").StyledComponent<"div", any, {
2
+ $isFocused?: boolean;
3
+ $isErrored?: boolean;
4
+ $isDisabled?: boolean;
5
+ $isLoading?: boolean;
6
+ $width?: string;
7
+ $minWidth?: string;
8
+ $autosize?: boolean;
9
+ $tightLeft?: boolean;
10
+ }, never>;
11
+ export declare const ShellInner: import("styled-components").StyledComponent<"div", any, {}, never>;
12
+ export declare const Input: import("styled-components").StyledComponent<"input", any, {}, never>;
13
+ export declare const AutosizeMirror: import("styled-components").StyledComponent<"span", any, {}, never>;
14
+ export declare const Indicators: import("styled-components").StyledComponent<"div", any, {}, never>;
15
+ export declare const IndicatorButton: import("styled-components").StyledComponent<"button", any, {
16
+ type: "button";
17
+ }, "type">;
18
+ export declare const Dropdown: import("styled-components").StyledComponent<"div", any, {}, never>;
19
+ export declare const DropdownItem: import("styled-components").StyledComponent<"div", any, {
20
+ $active?: boolean;
21
+ $selected?: boolean;
22
+ }, never>;
23
+ export declare const DropdownEmpty: import("styled-components").StyledComponent<"div", any, {}, never>;
24
+ export declare const CreateItemLabel: import("styled-components").StyledComponent<"span", any, {}, never>;
@@ -0,0 +1,81 @@
1
+ import styled, { css, keyframes } from 'styled-components';
2
+ import { Color, FontSize, LineHeight, Shadow, ShapeRadius, UnitSize } from '@pushwoosh/kit-constants';
3
+ import { DROPDOWN_MAX_HEIGHT, LOADING_BORDER_ANIMATION_MS, LOADING_BORDER_COLORS } from './constants';
4
+ const textBase = css(["font-size:", ";line-height:", ";color:", ";"], FontSize.REGULAR, LineHeight.REGULAR, Color.MAIN);
5
+ const rotate = keyframes(["from{transform:translate(-50%,-50%) rotate(0);}to{transform:translate(-50%,-50%) rotate(1turn);}"]);
6
+ const [c1, c2, c3, c4] = LOADING_BORDER_COLORS;
7
+ const loadingBorder = css(["&::before{content:'';position:absolute;z-index:0;top:50%;left:50%;width:200%;aspect-ratio:1;background-repeat:no-repeat;background-size:50% 50%,50% 50%,50% 50%,50% 50%;background-position:0 0,100% 0,100% 100%,0 100%;background-image:linear-gradient(", ",", "),linear-gradient(", ",", "),linear-gradient(", ",", "),linear-gradient(", ",", ");animation:", " ", "ms linear infinite;}&::after{content:'';position:absolute;z-index:1;inset:1px;background:", ";border-radius:calc(", " - 1px);}"], c1, c1, c2, c2, c3, c3, c4, c4, rotate, LOADING_BORDER_ANIMATION_MS, Color.CLEAR, ShapeRadius.CONTROL);
8
+ export const Shell = styled.div.withConfig({
9
+ displayName: "Shell",
10
+ componentId: "sc-1pvwhhw-0"
11
+ })(["position:relative;display:", ";align-items:center;gap:4px;width:", ";max-width:100%;min-width:", ";min-height:", ";padding:0 4px 0 ", ";", ";background-color:", ";border:1px solid ", ";border-radius:", ";box-sizing:border-box;cursor:", ";overflow:hidden;", ""], ({
12
+ $autosize
13
+ }) => $autosize ? 'inline-flex' : 'flex', ({
14
+ $autosize,
15
+ $width
16
+ }) => {
17
+ if ($width) return $width;
18
+ return $autosize ? 'fit-content' : '100%';
19
+ }, ({
20
+ $minWidth
21
+ }) => $minWidth || '0', UnitSize.FIELD_HEIGHT, ({
22
+ $tightLeft
23
+ }) => $tightLeft ? '6px' : '12px', textBase, ({
24
+ $isDisabled
25
+ }) => $isDisabled ? Color.FROZEN : Color.CLEAR, ({
26
+ $isErrored,
27
+ $isFocused
28
+ }) => {
29
+ if ($isErrored) return Color.DANGER;
30
+ if ($isFocused) return Color.BRIGHT;
31
+ return Color.FORM;
32
+ }, ShapeRadius.CONTROL, ({
33
+ $isDisabled
34
+ }) => $isDisabled ? 'not-allowed' : 'text', ({
35
+ $isLoading
36
+ }) => $isLoading && css(["border-color:transparent;", ";"], loadingBorder));
37
+ export const ShellInner = styled.div.withConfig({
38
+ displayName: "ShellInner",
39
+ componentId: "sc-1pvwhhw-1"
40
+ })(["position:relative;z-index:2;display:flex;align-items:center;gap:4px;flex:1;min-width:0;flex-wrap:wrap;padding:3px 0;"]);
41
+ export const Input = styled.input.withConfig({
42
+ displayName: "Input",
43
+ componentId: "sc-1pvwhhw-2"
44
+ })(["flex:1;min-width:30px;padding:0;margin:0;border:none;outline:none;background:transparent;font-family:inherit;", ";&::placeholder{color:", ";}&:disabled{cursor:not-allowed;}"], textBase, Color.PHANTOM);
45
+ export const AutosizeMirror = styled.span.withConfig({
46
+ displayName: "AutosizeMirror",
47
+ componentId: "sc-1pvwhhw-3"
48
+ })(["position:absolute;top:0;left:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none;white-space:pre;", ";"], textBase);
49
+ export const Indicators = styled.div.withConfig({
50
+ displayName: "Indicators",
51
+ componentId: "sc-1pvwhhw-4"
52
+ })(["position:relative;z-index:2;display:flex;align-items:center;gap:2px;flex-shrink:0;"]);
53
+ export const IndicatorButton = styled.button.attrs({
54
+ type: 'button'
55
+ }).withConfig({
56
+ displayName: "IndicatorButton",
57
+ componentId: "sc-1pvwhhw-5"
58
+ })(["display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:transparent;color:", ";cursor:pointer;border-radius:", ";transition:color 0.15s ease,background-color 0.15s ease;&:hover{color:", ";background-color:", ";}"], Color.PHANTOM, ShapeRadius.CONTROL, Color.MAIN, Color.FROZEN);
59
+ export const Dropdown = styled.div.withConfig({
60
+ displayName: "Dropdown",
61
+ componentId: "sc-1pvwhhw-6"
62
+ })(["max-height:", "px;overflow-y:auto;background:", ";border:1px solid ", ";border-radius:", ";box-shadow:", ";padding:4px 0;box-sizing:border-box;"], DROPDOWN_MAX_HEIGHT, Color.CLEAR, Color.FORM, ShapeRadius.CONTROL, Shadow.REGULAR);
63
+ export const DropdownItem = styled.div.withConfig({
64
+ displayName: "DropdownItem",
65
+ componentId: "sc-1pvwhhw-7"
66
+ })(["display:flex;align-items:center;gap:6px;padding:6px 12px;overflow-wrap:anywhere;", ";& > *{min-width:0;}background:", ";cursor:pointer;&:hover{background:", ";}"], textBase, ({
67
+ $active,
68
+ $selected
69
+ }) => {
70
+ if ($active) return Color.ROW_HOVER;
71
+ if ($selected) return Color.BRIGHT_LIGHT;
72
+ return 'transparent';
73
+ }, Color.ROW_HOVER);
74
+ export const DropdownEmpty = styled.div.withConfig({
75
+ displayName: "DropdownEmpty",
76
+ componentId: "sc-1pvwhhw-8"
77
+ })(["padding:6px 12px;", ";color:", ";"], textBase, Color.PHANTOM);
78
+ export const CreateItemLabel = styled.span.withConfig({
79
+ displayName: "CreateItemLabel",
80
+ componentId: "sc-1pvwhhw-9"
81
+ })(["color:", ";"], Color.PRIMARY);
@@ -0,0 +1,27 @@
1
+ import type { ReactNode } from 'react';
2
+ export type ItemState = {
3
+ selected: boolean;
4
+ active: boolean;
5
+ };
6
+ export type GetKeyProp<T> = [T] extends [string | number] ? {
7
+ getKey?: (item: T) => string | number;
8
+ } : {
9
+ getKey: (item: T) => string | number;
10
+ };
11
+ export type ItemsSource<T> = {
12
+ items: T[];
13
+ loadItems?: never;
14
+ debounceMs?: never;
15
+ } | {
16
+ items?: never;
17
+ loadItems: (query: string) => Promise<T[]>;
18
+ debounceMs?: number;
19
+ };
20
+ export type DropdownItemDescriptor = {
21
+ key: string | number;
22
+ id?: string;
23
+ label: ReactNode;
24
+ active?: boolean;
25
+ selected?: boolean;
26
+ onSelect?: () => void;
27
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { type Dispatch, type SetStateAction } from 'react';
2
+ export declare function useActiveIndex(total: number, resetIndex: number): [number, Dispatch<SetStateAction<number>>];
@@ -0,0 +1,12 @@
1
+ import { useEffect, useState } from 'react';
2
+ export function useActiveIndex(total, resetIndex) {
3
+ const [activeIndex, setActiveIndex] = useState(resetIndex);
4
+ useEffect(() => {
5
+ setActiveIndex(prev => {
6
+ if (total === 0) return -1;
7
+ if (prev < 0 || prev >= total) return resetIndex;
8
+ return prev;
9
+ });
10
+ }, [total, resetIndex]);
11
+ return [activeIndex, setActiveIndex];
12
+ }
@@ -0,0 +1,11 @@
1
+ type Args<T> = {
2
+ loadItems?: (query: string) => Promise<T[]>;
3
+ query: string;
4
+ isOpen: boolean;
5
+ debounceMs?: number;
6
+ };
7
+ export declare function useAsyncItems<T>({ loadItems, query, isOpen, debounceMs, }: Args<T>): {
8
+ items: T[];
9
+ loading: boolean;
10
+ };
11
+ export {};
@@ -0,0 +1,46 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { DEFAULT_DEBOUNCE_MS } from './constants';
3
+ import { useDebouncedValue } from './useDebouncedValue';
4
+ export function useAsyncItems({
5
+ loadItems,
6
+ query,
7
+ isOpen,
8
+ debounceMs = DEFAULT_DEBOUNCE_MS
9
+ }) {
10
+ const [items, setItems] = useState([]);
11
+ const [loading, setLoading] = useState(false);
12
+ const debouncedQuery = useDebouncedValue(query, debounceMs);
13
+ const loadItemsRef = useRef(loadItems);
14
+ loadItemsRef.current = loadItems;
15
+ const lastQueryRef = useRef(null);
16
+ useEffect(() => {
17
+ const loader = loadItemsRef.current;
18
+ if (!loader || !isOpen) {
19
+ lastQueryRef.current = null;
20
+ return undefined;
21
+ }
22
+ if (lastQueryRef.current === debouncedQuery) {
23
+ setLoading(false);
24
+ return undefined;
25
+ }
26
+ let cancelled = false;
27
+ setLoading(true);
28
+ Promise.resolve(loader(debouncedQuery)).then(result => {
29
+ if (cancelled) return;
30
+ lastQueryRef.current = debouncedQuery;
31
+ setItems(result);
32
+ setLoading(false);
33
+ }).catch(() => {
34
+ if (cancelled) return;
35
+ setLoading(false);
36
+ });
37
+ return () => {
38
+ cancelled = true;
39
+ setLoading(false);
40
+ };
41
+ }, [debouncedQuery, isOpen]);
42
+ return {
43
+ items,
44
+ loading
45
+ };
46
+ }
@@ -0,0 +1 @@
1
+ export declare function useDebouncedValue<V>(value: V, delay: number): V;
@@ -0,0 +1,13 @@
1
+ import { useEffect, useState } from 'react';
2
+ export function useDebouncedValue(value, delay) {
3
+ const [debounced, setDebounced] = useState(value);
4
+ useEffect(() => {
5
+ if (delay <= 0) {
6
+ setDebounced(value);
7
+ return undefined;
8
+ }
9
+ const id = setTimeout(() => setDebounced(value), delay);
10
+ return () => clearTimeout(id);
11
+ }, [value, delay]);
12
+ return debounced;
13
+ }
@@ -0,0 +1,16 @@
1
+ type Args<T> = {
2
+ items?: T[];
3
+ loadItems?: (query: string) => Promise<T[]>;
4
+ localItems?: T[];
5
+ debounceMs?: number;
6
+ getLabel: (item: T) => string;
7
+ filter?: (item: T, query: string) => boolean;
8
+ query: string;
9
+ isOpen: boolean;
10
+ };
11
+ export declare function useDropdownItems<T>({ items, loadItems, localItems, debounceMs, getLabel, filter, query, isOpen, }: Args<T>): {
12
+ sourceItems: T[];
13
+ filteredItems: T[];
14
+ loading: boolean;
15
+ };
16
+ export {};
@@ -0,0 +1,36 @@
1
+ import { useMemo } from 'react';
2
+ import { DEFAULT_DEBOUNCE_MS } from './constants';
3
+ import { defaultLabelFilter } from './shared';
4
+ import { useAsyncItems } from './useAsyncItems';
5
+ export function useDropdownItems({
6
+ items,
7
+ loadItems,
8
+ localItems,
9
+ debounceMs = DEFAULT_DEBOUNCE_MS,
10
+ getLabel,
11
+ filter,
12
+ query,
13
+ isOpen
14
+ }) {
15
+ const isAsync = !!loadItems;
16
+ const {
17
+ items: loadedItems,
18
+ loading
19
+ } = useAsyncItems({
20
+ loadItems,
21
+ query,
22
+ isOpen,
23
+ debounceMs
24
+ });
25
+ const sourceItems = useMemo(() => isAsync ? loadedItems : [...(items ?? []), ...(localItems ?? [])], [isAsync, loadedItems, items, localItems]);
26
+ const filteredItems = useMemo(() => {
27
+ if (isAsync || !query) return sourceItems;
28
+ const fn = filter ?? ((item, q) => defaultLabelFilter(item, q, getLabel));
29
+ return sourceItems.filter(item => fn(item, query));
30
+ }, [isAsync, sourceItems, query, filter, getLabel]);
31
+ return {
32
+ sourceItems,
33
+ filteredItems,
34
+ loading
35
+ };
36
+ }
@@ -0,0 +1,10 @@
1
+ type Args = {
2
+ isOpen?: boolean;
3
+ onClickOutside?: () => void;
4
+ };
5
+ export declare function useDropdownPosition({ isOpen, onClickOutside }: Args): {
6
+ setReference: (node: import("@floating-ui/react-dom").ReferenceType | null) => void;
7
+ setFloating: (node: HTMLElement | null) => void;
8
+ floatingStyles: import("react").CSSProperties;
9
+ };
10
+ export {};