polpo 0.1.13 → 0.1.16

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 (204) hide show
  1. package/.turbo/cache/eslint/.cache_16wwsjl +1 -0
  2. package/.turbo/cache/eslint/.cache_rpznjo +1 -0
  3. package/.turbo/turbo-build$colon$code.log +49 -0
  4. package/.turbo/turbo-build$colon$css.log +2 -0
  5. package/.turbo/turbo-build.log +43 -0
  6. package/.turbo/turbo-lint.log +2 -0
  7. package/dist/components.cjs +1 -3655
  8. package/dist/components.css +2 -2173
  9. package/dist/components.js +1 -3576
  10. package/dist/helpers.cjs +1 -311
  11. package/dist/helpers.d.cts +4 -1
  12. package/dist/helpers.d.ts +4 -1
  13. package/dist/helpers.js +1 -276
  14. package/dist/hooks.cjs +1 -1065
  15. package/dist/hooks.js +1 -998
  16. package/dist/layouts.cjs +1 -146
  17. package/dist/layouts.css +2 -5
  18. package/dist/layouts.js +1 -119
  19. package/dist/types.cjs +1 -19
  20. package/dist/types.js +0 -1
  21. package/eslint.config.js +4 -0
  22. package/package.json +16 -11
  23. package/postcss.config.js +3 -0
  24. package/src/components/accordion/accordion-item.tsx +123 -0
  25. package/src/components/accordion/accordion.component.tsx +80 -0
  26. package/src/components/accordion/accordion.styles.css +41 -0
  27. package/src/components/accordion/index.ts +2 -0
  28. package/src/components/button/button.component.tsx +50 -0
  29. package/src/components/button/button.styles.css +317 -0
  30. package/src/components/button/index.ts +1 -0
  31. package/src/components/cards/flip-card/flip-card.component.tsx +66 -0
  32. package/src/components/cards/flip-card/flip-card.styles.css +50 -0
  33. package/src/components/cards/flip-card/index.ts +1 -0
  34. package/src/components/cards/hover-card/hover-card.component.tsx +76 -0
  35. package/src/components/cards/hover-card/hover-card.styles.css +13 -0
  36. package/src/components/cards/hover-card/index.ts +1 -0
  37. package/src/components/cards/index.ts +3 -0
  38. package/src/components/cards/slide-card/index.ts +1 -0
  39. package/src/components/cards/slide-card/slide-card.component.tsx +27 -0
  40. package/src/components/component.types.ts +31 -0
  41. package/src/components/cursor/cursor.component.tsx +49 -0
  42. package/src/components/cursor/cursor.styles.css +73 -0
  43. package/src/components/cursor/index.ts +1 -0
  44. package/src/components/form/checkbox/checkbox.component.tsx +95 -0
  45. package/src/components/form/checkbox/checkbox.styles.css +162 -0
  46. package/src/components/form/checkbox/index.ts +1 -0
  47. package/src/components/form/controller/controller.component.tsx +42 -0
  48. package/src/components/form/controller/index.ts +1 -0
  49. package/src/components/form/date-picker/date-picker.component.tsx +66 -0
  50. package/src/components/form/date-picker/index.ts +1 -0
  51. package/src/components/form/field/field.component.tsx +84 -0
  52. package/src/components/form/field/field.styles.css +98 -0
  53. package/src/components/form/field/field.types.ts +34 -0
  54. package/src/components/form/field/index.ts +2 -0
  55. package/src/components/form/form.types.ts +37 -0
  56. package/src/components/form/index.ts +14 -0
  57. package/src/components/form/input/index.ts +1 -0
  58. package/src/components/form/input/input.component.tsx +75 -0
  59. package/src/components/form/input-color/index.ts +1 -0
  60. package/src/components/form/input-color/input-color.component.tsx +170 -0
  61. package/src/components/form/input-color/input-color.styles.css +92 -0
  62. package/src/components/form/input-file/index.ts +1 -0
  63. package/src/components/form/input-file/input-file.component.tsx +221 -0
  64. package/src/components/form/input-file/input-file.styles.css +143 -0
  65. package/src/components/form/input-number/index.ts +1 -0
  66. package/src/components/form/input-number/input-number.component.tsx +144 -0
  67. package/src/components/form/input-password/index.ts +1 -0
  68. package/src/components/form/input-password/input-password.component.tsx +77 -0
  69. package/src/components/form/radio/index.ts +1 -0
  70. package/src/components/form/radio/radio.component.tsx +92 -0
  71. package/src/components/form/radio/radio.styles.css +117 -0
  72. package/src/components/form/select/index.ts +1 -0
  73. package/src/components/form/select/option.tsx +101 -0
  74. package/src/components/form/select/options.tsx +165 -0
  75. package/src/components/form/select/select.component.tsx +317 -0
  76. package/src/components/form/select/select.styles.css +115 -0
  77. package/src/components/form/select/select.types.ts +97 -0
  78. package/src/components/form/slider/index.ts +1 -0
  79. package/src/components/form/slider/slider.component.tsx +117 -0
  80. package/src/components/form/slider/slider.styles.css +94 -0
  81. package/src/components/form/switch/index.ts +1 -0
  82. package/src/components/form/switch/switch.component.tsx +166 -0
  83. package/src/components/form/switch/switch.styles.css +165 -0
  84. package/src/components/form/textarea/index.ts +1 -0
  85. package/src/components/form/textarea/textarea.component.tsx +80 -0
  86. package/src/components/form/textarea/textarea.styles.css +7 -0
  87. package/src/components/image/image.tsx +7 -0
  88. package/src/components/image/index.ts +1 -0
  89. package/src/components/index.ts +15 -0
  90. package/src/components/infinity-scroll/index.ts +1 -0
  91. package/src/components/infinity-scroll/infinity-scroll.component.tsx +51 -0
  92. package/src/components/infinity-scroll/infinity-scroll.styles.css +25 -0
  93. package/src/components/line/index.ts +1 -0
  94. package/src/components/line/line.component.tsx +77 -0
  95. package/src/components/line/line.styles.css +61 -0
  96. package/src/components/modals/action-modal/action-modal.component.tsx +150 -0
  97. package/src/components/modals/action-modal/action-modal.styles.css +115 -0
  98. package/src/components/modals/action-modal/index.ts +1 -0
  99. package/src/components/modals/aside-modal/aside-modal.component.tsx +63 -0
  100. package/src/components/modals/aside-modal/aside-modal.styles.css +86 -0
  101. package/src/components/modals/aside-modal/index.ts +1 -0
  102. package/src/components/modals/confirmation-modal/confirmation-modal.component.tsx +43 -0
  103. package/src/components/modals/confirmation-modal/confirmation-modal.styles.css +17 -0
  104. package/src/components/modals/confirmation-modal/index.ts +1 -0
  105. package/src/components/modals/index.ts +6 -0
  106. package/src/components/modals/menu/index.ts +1 -0
  107. package/src/components/modals/menu/menu.component.tsx +194 -0
  108. package/src/components/modals/menu/menu.styles.css +101 -0
  109. package/src/components/modals/modal/index.ts +2 -0
  110. package/src/components/modals/modal/modal.backdrop.tsx +67 -0
  111. package/src/components/modals/modal/modal.component.tsx +104 -0
  112. package/src/components/modals/modal/modal.styles.css +70 -0
  113. package/src/components/modals/portal/index.ts +1 -0
  114. package/src/components/modals/portal/portal.component.tsx +19 -0
  115. package/src/components/ripple/index.ts +1 -0
  116. package/src/components/ripple/ripple.component.tsx +70 -0
  117. package/src/components/ripple/ripple.styles.css +33 -0
  118. package/src/components/smart-table/index.ts +2 -0
  119. package/src/components/smart-table/smart-table.column.tsx +62 -0
  120. package/src/components/smart-table/smart-table.component.tsx +116 -0
  121. package/src/components/smart-table/smart-table.helpers.tsx +58 -0
  122. package/src/components/smart-table/smart-table.hooks.ts +27 -0
  123. package/src/components/smart-table/smart-table.row.tsx +28 -0
  124. package/src/components/smart-table/smart-table.styles.css +102 -0
  125. package/src/components/smart-table/smart-table.types.ts +42 -0
  126. package/src/components/tabs/index.ts +1 -0
  127. package/src/components/tabs/tabs-list.tsx +120 -0
  128. package/src/components/tabs/tabs.styles.css +164 -0
  129. package/src/components/tabs/tabs.tsx +119 -0
  130. package/src/components/tag/index.ts +1 -0
  131. package/src/components/tag/tag.component.tsx +34 -0
  132. package/src/components/tag/tag.styles.css +50 -0
  133. package/src/components/tooltips/click-to-copy/click-to-copy.component.tsx +41 -0
  134. package/src/components/tooltips/click-to-copy/index.ts +1 -0
  135. package/src/components/tooltips/index.ts +2 -0
  136. package/src/components/tooltips/tooltip/index.ts +1 -0
  137. package/src/components/tooltips/tooltip/tooltip.component.tsx +64 -0
  138. package/src/components/tooltips/tooltip/tooltip.styles.css +91 -0
  139. package/src/components/typography/index.ts +1 -0
  140. package/src/components/typography/typography.component.tsx +81 -0
  141. package/src/components/typography/typography.constants.ts +53 -0
  142. package/src/components/typography/typography.styles.css +122 -0
  143. package/src/helpers/cn.ts +6 -0
  144. package/src/helpers/format-bytes.ts +11 -0
  145. package/src/helpers/format-dates.ts +47 -0
  146. package/src/helpers/get-modal-position-relative-to-screen.ts +86 -0
  147. package/src/helpers/get-modal-position.ts +211 -0
  148. package/src/helpers/index.ts +6 -0
  149. package/src/helpers/text/index.ts +1 -0
  150. package/src/helpers/text/to-capitalize.ts +17 -0
  151. package/src/hooks/index.ts +30 -0
  152. package/src/hooks/use-async.ts +88 -0
  153. package/src/hooks/use-classnames.ts +13 -0
  154. package/src/hooks/use-click-outside.ts +32 -0
  155. package/src/hooks/use-constant.ts +3 -0
  156. package/src/hooks/use-cookie.ts +124 -0
  157. package/src/hooks/use-debounce-state.ts +13 -0
  158. package/src/hooks/use-debounce.ts +15 -0
  159. package/src/hooks/use-dimensions.ts +19 -0
  160. package/src/hooks/use-dom-container.ts +35 -0
  161. package/src/hooks/use-event-listener.ts +71 -0
  162. package/src/hooks/use-file-reader.ts +69 -0
  163. package/src/hooks/use-geolocation.ts +63 -0
  164. package/src/hooks/use-hover.ts +17 -0
  165. package/src/hooks/use-in-view.ts +18 -0
  166. package/src/hooks/use-input-handlers.ts +52 -0
  167. package/src/hooks/use-intersection-observer.ts +19 -0
  168. package/src/hooks/use-media-query.ts +25 -0
  169. package/src/hooks/use-modal-in-container.ts +85 -0
  170. package/src/hooks/use-modal-transition.ts +64 -0
  171. package/src/hooks/use-modal.ts +21 -0
  172. package/src/hooks/use-mouse-position.ts +64 -0
  173. package/src/hooks/use-online-status.ts +12 -0
  174. package/src/hooks/use-render-count.ts +11 -0
  175. package/src/hooks/use-resize-observer.ts +18 -0
  176. package/src/hooks/use-safe-dispatch.ts +22 -0
  177. package/src/hooks/use-scroll.ts +31 -0
  178. package/src/hooks/use-state-history.ts +22 -0
  179. package/src/hooks/use-toggle-values.ts +14 -0
  180. package/src/hooks/use-toggle.ts +11 -0
  181. package/src/hooks/use-viewport.ts +38 -0
  182. package/src/index.ts +5 -0
  183. package/src/layouts/flex/flex.component.tsx +75 -0
  184. package/src/layouts/flex/index.ts +1 -0
  185. package/src/layouts/grid/grid.component.tsx +89 -0
  186. package/src/layouts/grid/grid.styles.css +5 -0
  187. package/src/layouts/grid/index.ts +1 -0
  188. package/src/layouts/index.ts +2 -0
  189. package/src/types/generics.ts +68 -0
  190. package/src/types/index.ts +1 -0
  191. package/tsconfig.json +10 -0
  192. package/tsup.config.cjs +36 -0
  193. package/dist/components.cjs.map +0 -1
  194. package/dist/components.css.map +0 -1
  195. package/dist/components.js.map +0 -1
  196. package/dist/helpers.cjs.map +0 -1
  197. package/dist/helpers.js.map +0 -1
  198. package/dist/hooks.cjs.map +0 -1
  199. package/dist/hooks.js.map +0 -1
  200. package/dist/layouts.cjs.map +0 -1
  201. package/dist/layouts.css.map +0 -1
  202. package/dist/layouts.js.map +0 -1
  203. package/dist/types.cjs.map +0 -1
  204. package/dist/types.js.map +0 -1
@@ -0,0 +1,165 @@
1
+ import React, { RefObject, useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import { useClassNames, useEventListener, useMediaQuery, useResizeObserver } from '../../../hooks';
4
+
5
+ import { OptionsProps } from './select.types';
6
+
7
+ type UseDynamicHeight = {
8
+ height: number;
9
+ minHeight: number;
10
+ containerRef: RefObject<HTMLElement | null>;
11
+ offset: number;
12
+ windowOffset: number;
13
+ };
14
+
15
+ const useDynamicHeight = ({
16
+ height,
17
+ minHeight: defaultMinHeight,
18
+ containerRef,
19
+ offset,
20
+ windowOffset,
21
+ }: UseDynamicHeight) => {
22
+ const [h, setH] = useState<string | undefined>(undefined);
23
+
24
+ const getMaxHeight = useCallback(() => {
25
+ const containerBottom = containerRef.current?.getBoundingClientRect().bottom ?? 0;
26
+
27
+ const heightBottom = window.innerHeight - containerBottom - windowOffset - offset - 5;
28
+ const heightTop = window.innerHeight - windowOffset * 3;
29
+ const heightCalculated = heightBottom < defaultMinHeight ? heightTop : heightBottom;
30
+ const maxHeight = Math.min(heightCalculated, height);
31
+ const minHeight = Math.max(maxHeight, defaultMinHeight);
32
+
33
+ setH(`${Math.round(minHeight)}px`);
34
+ }, [containerRef, windowOffset, offset, defaultMinHeight, height]);
35
+
36
+ useEffect(() => {
37
+ getMaxHeight();
38
+ }, [getMaxHeight]);
39
+
40
+ useResizeObserver(containerRef, getMaxHeight);
41
+ useEventListener('resize', getMaxHeight);
42
+
43
+ return { h };
44
+ };
45
+
46
+ export const Options = ({
47
+ children,
48
+ searchQueryValue,
49
+ searchQueryPlaceholder = 'Search option',
50
+ searchQueryClassName = '',
51
+ searchQueryStyle = {},
52
+ onSearchQuery,
53
+ containerRef,
54
+ height = 300,
55
+ }: OptionsProps) => {
56
+ const optionsGroupContainerRef = useRef<HTMLDivElement>(null);
57
+ const optionsGroupRef = useRef<HTMLUListElement>(null);
58
+ const [internalSearchQuery, setInternalSearchQuery] = useState('');
59
+ const searchInputRef = useRef<HTMLInputElement>(null);
60
+ const isMobile = useMediaQuery('(max-width: 480px)');
61
+
62
+ useEventListener('keydown', e => {
63
+ if (['ArrowDown', 'ArrowUp'].includes(e.code)) {
64
+ e.preventDefault();
65
+ const focusedItem = document.activeElement;
66
+ const isListItem = focusedItem?.tagName === 'LI';
67
+
68
+ if (isListItem) {
69
+ switch (e.code) {
70
+ case 'ArrowDown':
71
+ (focusedItem?.nextSibling as HTMLElement)?.focus();
72
+
73
+ break;
74
+ case 'ArrowUp':
75
+ (focusedItem?.previousElementSibling as HTMLElement)?.focus();
76
+
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ });
82
+
83
+ const handleSearchQuery = useCallback(
84
+ (e: React.ChangeEvent<HTMLInputElement>) => {
85
+ const { value } = e.target;
86
+
87
+ if (onSearchQuery) onSearchQuery(value);
88
+
89
+ setInternalSearchQuery(value);
90
+ },
91
+ [onSearchQuery],
92
+ );
93
+
94
+ useEffect(() => {
95
+ if (searchInputRef.current) {
96
+ searchInputRef.current.focus();
97
+ }
98
+ }, []);
99
+
100
+ const { h } = useDynamicHeight({
101
+ height,
102
+ minHeight: 150,
103
+ containerRef,
104
+ offset: 5,
105
+ windowOffset: 10,
106
+ });
107
+
108
+ const [shouldUseAuto, setShouldUseAuto] = useState(false);
109
+
110
+ const calculateHeight = useCallback(() => {
111
+ const optionsContainerHeight =
112
+ Math.round(optionsGroupContainerRef.current?.getBoundingClientRect().height ?? 0) + 2;
113
+ const optionsHeight = Math.round(optionsGroupRef.current?.scrollHeight ?? 0);
114
+
115
+ setShouldUseAuto(optionsContainerHeight >= optionsHeight);
116
+ }, []);
117
+
118
+ useEffect(calculateHeight, [calculateHeight]);
119
+ useResizeObserver(optionsGroupContainerRef, calculateHeight);
120
+ useResizeObserver(optionsGroupRef, calculateHeight);
121
+ useEventListener('resize', calculateHeight);
122
+
123
+ const selectOptionsHeader = useClassNames({
124
+ 'select-options-header': true,
125
+ [searchQueryClassName]: Boolean(searchQueryClassName),
126
+ });
127
+
128
+ return (
129
+ <section
130
+ className={`select-options ${onSearchQuery ? 'with-search-query' : ''}`}
131
+ style={
132
+ isMobile
133
+ ? {
134
+ maxHeight: window.innerHeight - 100,
135
+ width: window.innerWidth - 100,
136
+ }
137
+ : {
138
+ height: shouldUseAuto ? 'auto' : h,
139
+ maxHeight: height,
140
+ width: containerRef.current?.offsetWidth ?? 'auto',
141
+ }
142
+ }
143
+ >
144
+ {onSearchQuery && (
145
+ <section className={selectOptionsHeader} style={searchQueryStyle}>
146
+ <input
147
+ name='query'
148
+ className='input-search'
149
+ value={searchQueryValue ?? internalSearchQuery}
150
+ onChange={handleSearchQuery}
151
+ placeholder={searchQueryPlaceholder}
152
+ onClick={e => e.stopPropagation()}
153
+ ref={searchInputRef}
154
+ autoFocus
155
+ />
156
+ </section>
157
+ )}
158
+ <section className='select-options-list-container' ref={optionsGroupContainerRef}>
159
+ <ul className='select-options-list' ref={optionsGroupRef}>
160
+ {children}
161
+ </ul>
162
+ </section>
163
+ </section>
164
+ );
165
+ };
@@ -0,0 +1,317 @@
1
+ import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
2
+ import { ImCross } from 'react-icons/im';
3
+ import { PiCaretDown } from 'react-icons/pi';
4
+
5
+ import { useMediaQuery } from '../../../hooks';
6
+ import { InfinityScroll } from '../../infinity-scroll';
7
+ import { Menu } from '../../modals';
8
+ import { Typography } from '../../typography';
9
+ import { Controller } from '../controller';
10
+ import { Field } from '../field';
11
+ import { ControllerGeneratorProps } from '../form.types';
12
+
13
+ import { Option } from './option';
14
+ import { Options } from './options';
15
+ import './select.styles.css';
16
+ import {
17
+ ControllerGeneratorSelectProps,
18
+ MultiSelectProps,
19
+ MultiValue,
20
+ OptionComponentProps,
21
+ SelectItem,
22
+ SingleSelectProps,
23
+ SingleValue,
24
+ UnControlledSelectProps,
25
+ SelectContextValue,
26
+ ValueComponentProps,
27
+ } from './select.types';
28
+
29
+ const SelectContext = createContext<SelectContextValue<unknown> | null>(null);
30
+
31
+ export const useSelectContext = <T extends SelectItem>(): SelectContextValue<T> => {
32
+ const context = useContext(SelectContext as React.Context<SelectContextValue<T> | null>);
33
+
34
+ if (!context) {
35
+ throw new Error('useSelectContext must be used within a Select component');
36
+ }
37
+
38
+ return context;
39
+ };
40
+
41
+ type OptionLabelProps = {
42
+ children: React.ReactNode;
43
+ };
44
+
45
+ const OptionLabel = ({ children }: OptionLabelProps) => {
46
+ const labelComponent = useMemo(() => {
47
+ if (typeof children === 'string') {
48
+ return (
49
+ <Typography noPadding variant='label' nowrap>
50
+ {children}
51
+ </Typography>
52
+ );
53
+ }
54
+
55
+ return children;
56
+ }, [children]);
57
+
58
+ return <Menu.GroupLabel>{labelComponent}</Menu.GroupLabel>;
59
+ };
60
+
61
+ const DefaultOption = <T extends SelectItem>({ value }: OptionComponentProps<T>) => {
62
+ return (
63
+ <Typography variant='label' nowrap>
64
+ {typeof value === 'string' || typeof value === 'number' ? value : JSON.stringify(value)}
65
+ </Typography>
66
+ );
67
+ };
68
+
69
+ const DefaultValue = <T extends SelectItem>({ value, multiselect }: ValueComponentProps<T>) => {
70
+ if (multiselect) {
71
+ return (
72
+ <Typography noPadding nowrap variant='label'>
73
+ {`${value.length} item${value.length === 1 ? '' : 's'} selected`}
74
+ </Typography>
75
+ );
76
+ }
77
+
78
+ if (value === null) {
79
+ return null;
80
+ }
81
+
82
+ return (
83
+ <Typography noPadding variant='label' nowrap>
84
+ {typeof value === 'string' || typeof value === 'number' ? value : JSON.stringify(value)}
85
+ </Typography>
86
+ );
87
+ };
88
+
89
+ export const Select = <T extends SelectItem>({
90
+ // Select props
91
+ options,
92
+ isEqualComparator,
93
+ searchQueryValue,
94
+ searchQueryPlaceholder,
95
+ onSearchQuery,
96
+ loadMore = () => null,
97
+ isLoading = false,
98
+ hasNextPage = false,
99
+ emptyMessage = 'No options to select',
100
+ multiselect,
101
+ optionComponent: OptionComponent = DefaultOption,
102
+ valueComponent: ValueComponent = DefaultValue,
103
+ maxOptions,
104
+ children,
105
+ // Shared props
106
+ name,
107
+ value,
108
+ setValue,
109
+ onBlur,
110
+ onFocus,
111
+ className = '',
112
+ style = {},
113
+ showClearOption = false,
114
+ height,
115
+ searchQueryStyle,
116
+ searchQueryClassName,
117
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
118
+ autoFocus = false,
119
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
120
+ readOnly = false,
121
+ disabled = false,
122
+ placeholder = '',
123
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
124
+ autoComplete = 'off',
125
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
126
+ isDirty = false,
127
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
128
+ isTouched = false,
129
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
130
+ invalid = false,
131
+ error,
132
+ // Field props
133
+ ...fieldProps
134
+ }: UnControlledSelectProps<T>) => {
135
+ const modalRef = useRef<HTMLElement>(null);
136
+ const isMobile = useMediaQuery('(max-width: 480px)');
137
+ const id = useMemo(() => crypto.randomUUID(), []);
138
+ const containerRef = useRef<HTMLElement>(null);
139
+ const [isOpen, setIsOpen] = useState(false);
140
+
141
+ const openSelect = useCallback(
142
+ (open: boolean) => {
143
+ setIsOpen(open && !disabled);
144
+ },
145
+ [disabled],
146
+ );
147
+
148
+ const clearOption = useCallback(
149
+ (e: React.MouseEvent) => {
150
+ e.stopPropagation();
151
+
152
+ if (multiselect) {
153
+ setValue([]);
154
+ } else {
155
+ setValue(null);
156
+ }
157
+ },
158
+ [multiselect, setValue],
159
+ );
160
+
161
+ const valueNonEmpty = useMemo(() => {
162
+ if (multiselect) {
163
+ return value.length > 0;
164
+ } else {
165
+ return Boolean(value);
166
+ }
167
+ }, [multiselect, value]);
168
+
169
+ const renderedOptions = useMemo<React.ReactNode>(() => {
170
+ if (!children)
171
+ return options.map((value, key) => (
172
+ <Select.Option value={value} key={key}>
173
+ <OptionComponent value={value} />
174
+ </Select.Option>
175
+ ));
176
+
177
+ return children;
178
+ }, [OptionComponent, children, options]);
179
+
180
+ return (
181
+ <SelectContext.Provider
182
+ value={
183
+ {
184
+ selectedValue: value,
185
+ setValue,
186
+ multiselect: multiselect ?? false,
187
+ isEqualComparator,
188
+ maxOptions: maxOptions ?? null,
189
+ setIsOpen,
190
+ } as SelectContextValue<unknown>
191
+ }
192
+ >
193
+ <Field id={id} error={error} isFocus={isOpen} ref={containerRef} {...fieldProps}>
194
+ <section
195
+ id={name}
196
+ style={style}
197
+ onBlur={onBlur}
198
+ className={`select ${disabled ? 'disabled' : ''} ${className}`}
199
+ >
200
+ <section
201
+ id={id}
202
+ className={`select-container ${valueNonEmpty && showClearOption ? 'three-columns' : ''}`}
203
+ onClick={() => openSelect(true)}
204
+ >
205
+ <button
206
+ type='button'
207
+ className={`input-button ${(Array.isArray(value) ? value.length > 0 : value) ? '' : 'placeholder'}`}
208
+ aria-haspopup='listbox'
209
+ aria-expanded={isOpen}
210
+ onFocus={e => {
211
+ openSelect(true);
212
+
213
+ if (onFocus) onFocus(e);
214
+ }}
215
+ >
216
+ {valueNonEmpty ? (
217
+ <ValueComponent {...(multiselect ? { value, multiselect } : { value: value })} />
218
+ ) : (
219
+ <Typography variant='label' noPadding nowrap>
220
+ {placeholder}
221
+ </Typography>
222
+ )}
223
+ </button>
224
+ {valueNonEmpty && showClearOption && (
225
+ <section className='icon-close' onClick={clearOption}>
226
+ <ImCross />
227
+ </section>
228
+ )}
229
+ <PiCaretDown className={`select-caret-icon ${isOpen && 'is-select-open'}`} />
230
+ </section>
231
+ <Menu
232
+ id='form-select'
233
+ isOpen={isOpen}
234
+ onClose={() => openSelect(false)}
235
+ backdrop={isMobile ? 'blur' : 'transparent'}
236
+ opacity={isMobile ? 0.8 : 0.4}
237
+ position={isMobile ? 'center' : 'bottom'}
238
+ offset={5}
239
+ modalRef={modalRef}
240
+ windowOffset={10}
241
+ transitionDuration={200}
242
+ containerRef={isMobile ? undefined : containerRef}
243
+ contentClassName='select-menu-content'
244
+ className='select-options-menu'
245
+ >
246
+ <Options
247
+ containerRef={containerRef}
248
+ onSearchQuery={onSearchQuery}
249
+ searchQueryValue={searchQueryValue}
250
+ searchQueryPlaceholder={searchQueryPlaceholder}
251
+ searchQueryClassName={searchQueryClassName}
252
+ searchQueryStyle={searchQueryStyle}
253
+ height={height}
254
+ >
255
+ <InfinityScroll
256
+ isLoading={isLoading}
257
+ hasNextPage={hasNextPage}
258
+ loadMore={loadMore}
259
+ emptyMessage={emptyMessage}
260
+ >
261
+ {renderedOptions}
262
+ </InfinityScroll>
263
+ </Options>
264
+ </Menu>
265
+ </section>
266
+ </Field>
267
+ </SelectContext.Provider>
268
+ );
269
+ };
270
+
271
+ const MultiSelectController = <T extends SelectItem>({
272
+ rules,
273
+ ...props
274
+ }: ControllerGeneratorProps<MultiSelectProps<T>, MultiValue<T>>) => {
275
+ return (
276
+ <Controller<MultiSelectProps<T>, MultiValue<T>>
277
+ Component={Select}
278
+ defaultValue={[]}
279
+ inputProps={{
280
+ ...props,
281
+ multiselect: true,
282
+ }}
283
+ rules={rules}
284
+ />
285
+ );
286
+ };
287
+
288
+ const SingleSelectController = <T extends SelectItem>({
289
+ rules,
290
+ ...props
291
+ }: ControllerGeneratorProps<SingleSelectProps<T>, SingleValue<T>>) => {
292
+ return (
293
+ <Controller<SingleSelectProps<T>, SingleValue<T>>
294
+ Component={Select}
295
+ defaultValue={null}
296
+ inputProps={{
297
+ ...props,
298
+ multiselect: false,
299
+ }}
300
+ rules={rules}
301
+ />
302
+ );
303
+ };
304
+
305
+ const SelectController = <T extends SelectItem>(props: ControllerGeneratorSelectProps<T>) => {
306
+ const { multiselect } = props;
307
+
308
+ if (multiselect) {
309
+ return <MultiSelectController<T> {...props} multiselect={true} />;
310
+ }
311
+
312
+ return <SingleSelectController<T> {...props} multiselect={false} />;
313
+ };
314
+
315
+ Select.Controller = SelectController;
316
+ Select.Option = Option;
317
+ Select.OptionLabel = OptionLabel;
@@ -0,0 +1,115 @@
1
+ @reference "polpo-tailwind-config/styles";
2
+
3
+ .select {
4
+ cursor: pointer;
5
+
6
+ .icon-close {
7
+ display: grid;
8
+ place-content: center;
9
+ align-self: center;
10
+ border-radius: 100%;
11
+ cursor: pointer;
12
+ font-size: 0.6em;
13
+ padding: 0.5em;
14
+
15
+ &:hover {
16
+ background: hsl(from var(--color-foreground) h s l / 33%);
17
+ }
18
+ }
19
+
20
+ .select-container {
21
+ display: grid;
22
+ grid-template-columns: 1fr auto;
23
+ gap: 0.5em;
24
+ align-items: center;
25
+ grid-auto-rows: auto;
26
+ position: relative;
27
+ cursor: pointer;
28
+ user-select: none;
29
+
30
+ &.three-columns {
31
+ grid-template-columns: 1fr auto auto;
32
+ }
33
+ }
34
+
35
+ .input-button {
36
+ appearance: none;
37
+ -webkit-appearance: none;
38
+ -moz-appearance: none;
39
+ white-space: nowrap;
40
+ text-overflow: ellipsis;
41
+ overflow: hidden;
42
+ display: grid;
43
+ min-width: 0;
44
+ width: 100%;
45
+ border: 0;
46
+ margin: 0;
47
+ border-radius: 0;
48
+ background: transparent;
49
+ color: currentColor;
50
+ text-align: left;
51
+ -webkit-tap-highlight-color: transparent;
52
+ padding: 0;
53
+ grid-template-columns: 1fr auto;
54
+
55
+ &.placeholder {
56
+ color: #919191;
57
+ }
58
+ }
59
+
60
+ .select-caret-icon {
61
+ transition: transform 200ms ease;
62
+
63
+ &.is-select-open {
64
+ transform: rotate(180deg);
65
+ }
66
+ }
67
+ }
68
+
69
+ .select-options-menu {
70
+ .select-menu-content {
71
+ gap: 0;
72
+ display: block;
73
+ height: auto;
74
+ overflow: initial;
75
+ }
76
+ }
77
+
78
+ .select-options {
79
+ gap: 0;
80
+ display: grid;
81
+ grid-template-rows: 1fr;
82
+ height: 100%;
83
+ overflow: auto;
84
+
85
+ &.with-search-query {
86
+ grid-template-rows: auto 1fr;
87
+ }
88
+
89
+ .select-options-list-container {
90
+ height: 100%;
91
+ overflow: auto;
92
+ }
93
+
94
+ .select-options-list {
95
+ display: grid;
96
+ gap: 0.2em;
97
+ padding: 0;
98
+ margin: 0;
99
+ list-style: none;
100
+ align-content: start;
101
+ }
102
+ }
103
+
104
+ .select-options-header {
105
+ background: var(--color-background);
106
+ padding: 2px 5px;
107
+ border-bottom: 1px solid var(--color-border);
108
+
109
+ .input-search {
110
+ outline: none;
111
+ padding: 0.8em 5px;
112
+ @apply text-label;
113
+ width: 100%;
114
+ }
115
+ }
@@ -0,0 +1,97 @@
1
+ import React from 'react';
2
+
3
+ import { InputFieldProps } from '../field';
4
+ import { ControllerGeneratorProps, UnControlledComponentProps } from '../form.types';
5
+
6
+ export type SelectItem = string | number | Record<string, unknown> | unknown;
7
+
8
+ export type MultiValue<T extends SelectItem> = Array<T>;
9
+
10
+ export type SingleValue<T extends SelectItem> = T | null;
11
+
12
+ // SELECT CONTEXT
13
+
14
+ export type SharedSelectContextValue<T extends SelectItem> = {
15
+ isEqualComparator?: (a: T, b: T) => boolean;
16
+ maxOptions: number | null;
17
+ setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
18
+ };
19
+
20
+ export type MultiSelectContextValue<T extends SelectItem> = SharedSelectContextValue<T> & {
21
+ selectedValue: MultiValue<T>;
22
+ setValue: (value: MultiValue<T>) => void;
23
+ multiselect: true;
24
+ };
25
+
26
+ export type SingleSelectContextValue<T extends SelectItem> = SharedSelectContextValue<T> & {
27
+ selectedValue: SingleValue<T>;
28
+ setValue: (value: SingleValue<T>) => void;
29
+ multiselect: false;
30
+ };
31
+
32
+ export type SelectContextValue<T extends SelectItem> = SingleSelectContextValue<T> | MultiSelectContextValue<T>;
33
+
34
+ // SELECT
35
+
36
+ export type OptionComponentProps<T extends SelectItem> = {
37
+ value: T;
38
+ };
39
+
40
+ export type ValueComponentProps<T extends SelectItem> =
41
+ | {
42
+ value: MultiValue<T>;
43
+ multiselect: true;
44
+ }
45
+ | {
46
+ value: SingleValue<T>;
47
+ multiselect?: false;
48
+ };
49
+
50
+ export type SharedSelectProps<T extends SelectItem> = InputFieldProps<{
51
+ options: Array<T>;
52
+ isEqualComparator?: (a: T, b: T) => boolean;
53
+ searchQueryValue?: string;
54
+ searchQueryPlaceholder?: string;
55
+ onSearchQuery?: (value: string) => void;
56
+ loadMore?: () => void;
57
+ isLoading?: boolean;
58
+ hasNextPage?: boolean;
59
+ emptyMessage?: string;
60
+ maxOptions?: number;
61
+ optionComponent?: React.FC<OptionComponentProps<T>>;
62
+ valueComponent?: React.FC<ValueComponentProps<T>>;
63
+ showClearOption?: boolean;
64
+ height?: number;
65
+ searchQueryClassName?: string;
66
+ searchQueryStyle?: React.CSSProperties;
67
+ children?: React.ReactNode;
68
+ }>;
69
+
70
+ export type MultiSelectProps<T extends SelectItem> = SharedSelectProps<T> & {
71
+ multiselect: true;
72
+ };
73
+
74
+ export type SingleSelectProps<T extends SelectItem> = SharedSelectProps<T> & {
75
+ multiselect?: false;
76
+ };
77
+
78
+ export type UnControlledSelectProps<T extends SelectItem> =
79
+ | UnControlledComponentProps<MultiSelectProps<T>, MultiValue<T>>
80
+ | UnControlledComponentProps<SingleSelectProps<T>, SingleValue<T>>;
81
+
82
+ export type ControllerGeneratorSelectProps<T extends SelectItem> =
83
+ | ControllerGeneratorProps<MultiSelectProps<T>, MultiValue<T>>
84
+ | ControllerGeneratorProps<SingleSelectProps<T>, SingleValue<T>>;
85
+
86
+ // SELECT OPTIONS
87
+
88
+ export type OptionsProps = {
89
+ onSearchQuery?: (value: string) => void;
90
+ searchQueryValue?: string;
91
+ searchQueryPlaceholder?: string;
92
+ searchQueryClassName?: string;
93
+ searchQueryStyle?: React.CSSProperties;
94
+ containerRef: React.RefObject<HTMLElement | null>;
95
+ height?: number;
96
+ children?: React.ReactNode;
97
+ };
@@ -0,0 +1 @@
1
+ export * from './slider.component';