pentatrion-design 0.0.1

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 (208) hide show
  1. package/.storybook/main.ts +33 -0
  2. package/.storybook/manager.ts +8 -0
  3. package/.storybook/preview-head.html +2 -0
  4. package/.storybook/preview.ts +26 -0
  5. package/.storybook/public/bg-map.jpg +0 -0
  6. package/.storybook/public/favicon-32x32.png +0 -0
  7. package/.storybook/public/favicon.svg +28 -0
  8. package/.storybook/public/movies.json +2153 -0
  9. package/.storybook/public/population.json +1178 -0
  10. package/.storybook/public/town-74.geojson +8995 -0
  11. package/.storybook/public/town-74.json +4062 -0
  12. package/.storybook/theme.ts +14 -0
  13. package/Makefile +14 -0
  14. package/README.md +40 -0
  15. package/TODO.md +5 -0
  16. package/dist/index.d.ts +1110 -0
  17. package/dist/index.js +3707 -0
  18. package/dist/redux/index.d.ts +43 -0
  19. package/dist/redux/index.js +583 -0
  20. package/dist/types.d-DlHa_IaW.d.ts +14 -0
  21. package/images/logo-shadow.svg +173 -0
  22. package/images/logo.svg +76 -0
  23. package/package.json +85 -0
  24. package/postcss.config.js +7 -0
  25. package/src/components/autocomplete/Autocomplete.stories.tsx +89 -0
  26. package/src/components/autocomplete/Autocomplete.tsx +316 -0
  27. package/src/components/autocomplete/AutocompleteOption.tsx +35 -0
  28. package/src/components/autocomplete/LazyAutocomplete.tsx +130 -0
  29. package/src/components/autocomplete/SimpleAutocomplete.tsx +74 -0
  30. package/src/components/autocomplete/index.ts +6 -0
  31. package/src/components/autocomplete/useAutocompleteContext.ts +20 -0
  32. package/src/components/autocomplete/util.ts +19 -0
  33. package/src/components/badge/Badge.stories.tsx +72 -0
  34. package/src/components/badge/Badge.tsx +70 -0
  35. package/src/components/button/Button.stories.tsx +189 -0
  36. package/src/components/button/Button.test.tsx +20 -0
  37. package/src/components/button/Button.tsx +200 -0
  38. package/src/components/button/ButtonGroup.stories.tsx +46 -0
  39. package/src/components/button/ButtonGroup.tsx +29 -0
  40. package/src/components/button/index.ts +2 -0
  41. package/src/components/card/Card.stories.tsx +21 -0
  42. package/src/components/card/config.ts +4 -0
  43. package/src/components/card/index.ts +1 -0
  44. package/src/components/code/Code.tsx +15 -0
  45. package/src/components/context-menu/ContextMenu.stories.tsx +22 -0
  46. package/src/components/context-menu/ContextMenu.tsx +175 -0
  47. package/src/components/context-menu/ContextMenuItem.tsx +34 -0
  48. package/src/components/context-menu/index.ts +2 -0
  49. package/src/components/dialog/Dialog.stories.tsx +270 -0
  50. package/src/components/dialog/Dialog.tsx +46 -0
  51. package/src/components/dialog/index.ts +1 -0
  52. package/src/components/dialog/util.ts +29 -0
  53. package/src/components/dropdown-menu/DropdownMenu.stories.tsx +19 -0
  54. package/src/components/dropdown-menu/DropdownMenu.tsx +12 -0
  55. package/src/components/dropdown-menu/DropdownMenuContent.tsx +46 -0
  56. package/src/components/dropdown-menu/DropdownMenuItem.tsx +33 -0
  57. package/src/components/dropdown-menu/DropdownMenuTrigger.tsx +42 -0
  58. package/src/components/dropdown-menu/index.ts +7 -0
  59. package/src/components/dropdown-menu/interface.d.ts +13 -0
  60. package/src/components/dropdown-menu/useDropdownMenu.tsx +115 -0
  61. package/src/components/dropdown-menu/useDropdownMenuContext.ts +16 -0
  62. package/src/components/dropdown-menu-nested/DropdownMenuNested.stories.tsx +33 -0
  63. package/src/components/dropdown-menu-nested/DropdownMenuNested.tsx +19 -0
  64. package/src/components/dropdown-menu-nested/MenuContext.ts +15 -0
  65. package/src/components/dropdown-menu-nested/MenuItem.tsx +43 -0
  66. package/src/components/dropdown-menu-nested/MenuItemWithChildren.tsx +204 -0
  67. package/src/components/dropdown-menu-nested/index.ts +3 -0
  68. package/src/components/flash/Flash.stories.tsx +34 -0
  69. package/src/components/flash/Flash.tsx +24 -0
  70. package/src/components/highlight/Highlight.stories.tsx +128 -0
  71. package/src/components/highlight/Highlight.tsx +29 -0
  72. package/src/components/href/Href.stories.tsx +55 -0
  73. package/src/components/href/Href.tsx +18 -0
  74. package/src/components/input/Checkbox.stories.tsx +52 -0
  75. package/src/components/input/Checkbox.tsx +41 -0
  76. package/src/components/input/Input.stories.tsx +103 -0
  77. package/src/components/input/Input.test.tsx +10 -0
  78. package/src/components/input/Input.tsx +60 -0
  79. package/src/components/input/RadioGroup.stories.tsx +76 -0
  80. package/src/components/input/RadioGroup.tsx +84 -0
  81. package/src/components/input/Toggle.stories.tsx +29 -0
  82. package/src/components/input/Toggle.tsx +35 -0
  83. package/src/components/input/index.ts +4 -0
  84. package/src/components/input-field/InputField.stories.tsx +227 -0
  85. package/src/components/input-field/InputField.tsx +62 -0
  86. package/src/components/loader/Loader.stories.tsx +37 -0
  87. package/src/components/loader/Loader.tsx +54 -0
  88. package/src/components/modal/Components.tsx +58 -0
  89. package/src/components/modal/Modal.stories.tsx +113 -0
  90. package/src/components/modal/Modal.tsx +13 -0
  91. package/src/components/modal/ModalContent.tsx +46 -0
  92. package/src/components/modal/ModalTrigger.tsx +41 -0
  93. package/src/components/modal/index.ts +7 -0
  94. package/src/components/modal/interface.d.ts +10 -0
  95. package/src/components/modal/useModal.ts +48 -0
  96. package/src/components/modal/useModalContext.ts +16 -0
  97. package/src/components/notification/Notification.stories.tsx +51 -0
  98. package/src/components/notification/NotificationsContext.ts +79 -0
  99. package/src/components/notification/NotificationsProvider.tsx +58 -0
  100. package/src/components/notification/index.ts +2 -0
  101. package/src/components/popover/Components.tsx +45 -0
  102. package/src/components/popover/Popover.stories.tsx +46 -0
  103. package/src/components/popover/Popover.test.tsx +21 -0
  104. package/src/components/popover/Popover.tsx +12 -0
  105. package/src/components/popover/PopoverContent.tsx +46 -0
  106. package/src/components/popover/PopoverTrigger.tsx +42 -0
  107. package/src/components/popover/index.ts +7 -0
  108. package/src/components/popover/interface.d.ts +13 -0
  109. package/src/components/popover/usePopover.tsx +80 -0
  110. package/src/components/popover/usePopoverContext.ts +16 -0
  111. package/src/components/portal/Portal.tsx +12 -0
  112. package/src/components/quick-settings/QuickSettings.stories.tsx +182 -0
  113. package/src/components/resize-area/ResizeArea.stories.css +40 -0
  114. package/src/components/resize-area/ResizeArea.stories.tsx +30 -0
  115. package/src/components/resize-area/ResizeArea.tsx +79 -0
  116. package/src/components/scroll/Scroll.stories.tsx +81 -0
  117. package/src/components/scroll/Scroll.tsx +111 -0
  118. package/src/components/select/Select.stories.tsx +248 -0
  119. package/src/components/select/Select.tsx +362 -0
  120. package/src/components/select/SelectOption.tsx +29 -0
  121. package/src/components/select/SelectSelection.tsx +13 -0
  122. package/src/components/select/index.ts +5 -0
  123. package/src/components/select/interface.d.ts +36 -0
  124. package/src/components/select/useSelectContext.ts +19 -0
  125. package/src/components/select/util.ts +12 -0
  126. package/src/components/snack/Snack.stories.tsx +33 -0
  127. package/src/components/snack/Snack.tsx +47 -0
  128. package/src/components/sortable/Sortable.stories.tsx +98 -0
  129. package/src/components/sortable/Sortable.tsx +37 -0
  130. package/src/components/sortable/index.ts +2 -0
  131. package/src/components/sortable/types.d.ts +12 -0
  132. package/src/components/steps/Step.tsx +41 -0
  133. package/src/components/steps/Steps.stories.tsx +141 -0
  134. package/src/components/steps/Steps.tsx +49 -0
  135. package/src/components/steps/index.ts +3 -0
  136. package/src/components/steps/util.ts +3 -0
  137. package/src/components/table/Table.stories.tsx +36 -0
  138. package/src/components/table/Table.tsx +13 -0
  139. package/src/components/table/TableBody.tsx +9 -0
  140. package/src/components/table/TableCell.tsx +16 -0
  141. package/src/components/table/TableFooter.tsx +9 -0
  142. package/src/components/table/TableHeader.tsx +14 -0
  143. package/src/components/table/TableHeaderCell.tsx +14 -0
  144. package/src/components/table/TableRow.tsx +14 -0
  145. package/src/components/table/index.ts +7 -0
  146. package/src/components/tabs/Tabs.stories.tsx +66 -0
  147. package/src/components/tabs/Tabs.tsx +80 -0
  148. package/src/components/tooltip/SimpleTooltip.tsx +17 -0
  149. package/src/components/tooltip/Tooltip.stories.tsx +129 -0
  150. package/src/components/tooltip/Tooltip.test.tsx +18 -0
  151. package/src/components/tooltip/Tooltip.tsx +13 -0
  152. package/src/components/tooltip/TooltipContent.tsx +41 -0
  153. package/src/components/tooltip/TooltipTrigger.tsx +36 -0
  154. package/src/components/tooltip/index.ts +6 -0
  155. package/src/components/tooltip/interface.d.ts +15 -0
  156. package/src/components/tooltip/useTooltip.ts +87 -0
  157. package/src/components/tooltip/useTooltipContext.ts +16 -0
  158. package/src/env.d.ts +27 -0
  159. package/src/hooks/index.ts +13 -0
  160. package/src/hooks/useCombinedRefs.test.tsx +42 -0
  161. package/src/hooks/useCombinedRefs.ts +21 -0
  162. package/src/hooks/useCopyToClipboard.ts +28 -0
  163. package/src/hooks/useDebounce.ts +59 -0
  164. package/src/hooks/useEventCallback.ts +13 -0
  165. package/src/hooks/useEventListener.ts +86 -0
  166. package/src/hooks/useFetch.tsx +21 -0
  167. package/src/hooks/useIsClosing.ts +25 -0
  168. package/src/hooks/useIsMounted.tsx +16 -0
  169. package/src/hooks/useIsomorphicLayoutEffect.ts +4 -0
  170. package/src/hooks/useNotifications.tsx +13 -0
  171. package/src/hooks/useOnClickOutside.ts +35 -0
  172. package/src/hooks/usePrevious.ts +9 -0
  173. package/src/hooks/useRipple.test.tsx +52 -0
  174. package/src/hooks/useRipple.tsx +79 -0
  175. package/src/index.css +14 -0
  176. package/src/index.ts +39 -0
  177. package/src/lib/arrUtil.ts +8 -0
  178. package/src/lib/error.ts +18 -0
  179. package/src/lib/fetch.test.ts +107 -0
  180. package/src/lib/fetch.ts +115 -0
  181. package/src/lib/index.ts +3 -0
  182. package/src/lib/tailwindVariants.ts +35 -0
  183. package/src/redux/index.ts +1 -0
  184. package/src/redux/notification/components/NotificationConsumer.tsx +52 -0
  185. package/src/redux/notification/errorCatcherMiddleware.ts +17 -0
  186. package/src/redux/notification/hooks/useNotification.ts +26 -0
  187. package/src/redux/notification/index.ts +5 -0
  188. package/src/redux/notification/notificationSlice.ts +68 -0
  189. package/src/styles/Animation.stories.tsx +43 -0
  190. package/src/styles/Shadow.stories.tsx +43 -0
  191. package/src/styles/Themes.stories.tsx +31 -0
  192. package/src/styles/Typography.stories.tsx +101 -0
  193. package/src/tailwind/base.css +188 -0
  194. package/src/tailwind/components-input-outline.css +62 -0
  195. package/src/tailwind/components-resize-area.css +112 -0
  196. package/src/tailwind/components-step.css +161 -0
  197. package/src/tailwind/components.css +51 -0
  198. package/src/tailwind/keyframes.ts +69 -0
  199. package/src/tailwind/pentatrionTw.ts +147 -0
  200. package/src/tailwind/utilities-dialog.css +103 -0
  201. package/src/tailwind/utilities.css +3 -0
  202. package/src/types.d.ts +12 -0
  203. package/tailwind.config.js +18 -0
  204. package/tests/README.md +32 -0
  205. package/tests/setup.ts +1 -0
  206. package/tsconfig.json +14 -0
  207. package/tsup.config.ts +23 -0
  208. package/vite.config.ts +24 -0
@@ -0,0 +1,248 @@
1
+ import { Meta } from "@storybook/react";
2
+
3
+ import { useListItem } from "@floating-ui/react";
4
+ import { useState } from "react";
5
+
6
+ import clsx from "clsx";
7
+ import { action } from "@storybook/addon-actions";
8
+
9
+ import { SelectValue } from "./Select";
10
+ import { useSelect, Select, SelectSelectionProps, Option } from ".";
11
+
12
+ const onChangeAction = action("onChange");
13
+
14
+ const meta = {
15
+ title: "Components/Select",
16
+ component: Select,
17
+ } satisfies Meta<typeof Select>;
18
+ export default meta;
19
+
20
+ const departments = [
21
+ { value: "38", label: "Isère" },
22
+ { value: "74", label: "Haute-Savoie" },
23
+ { value: "73", label: "Savoie" },
24
+ ];
25
+
26
+ const townsByDepartment = {
27
+ "38": [
28
+ { value: "grenoble", label: "Grenoble" },
29
+ { value: "meylan", label: "Meylan" },
30
+ { value: "voreppe", label: "Voreppe" },
31
+ ],
32
+ "73": [
33
+ { value: "chambery", label: "Chambéry" },
34
+ { value: "albertville", label: "Albertville" },
35
+ ],
36
+ "74": [
37
+ { value: "annecy", label: "Annecy" },
38
+ { value: "annemasse", label: "Annemasse" },
39
+ { value: "avoriaz", label: "Avoriaz" },
40
+ ],
41
+ };
42
+
43
+ const options = [
44
+ { value: "abbeville", label: "Abbeville" },
45
+ { value: "agde", label: "Agde" },
46
+ { value: "agen", label: "Agen" },
47
+ { value: "aixenprovence", label: "Aix-en-Provence" },
48
+ { value: "ajaccio", label: "Ajaccio" },
49
+ { value: "albi", label: "Albi" },
50
+ // { value: "alencon", label: "Alençon" },
51
+ // { value: "amiens", label: "Amiens" },
52
+ // { value: "angers", label: "Angers" },
53
+ // { value: "angouleme", label: "Angoulême" },
54
+ // { value: "annonay", label: "Annonay" },
55
+ // { value: "antibes", label: "Antibes" },
56
+ // { value: "arcachon", label: "Arcachon" },
57
+ // { value: "arles", label: "Arles" },
58
+ // { value: "arras", label: "Arras" },
59
+ // { value: "asnieres-sur-seine", label: "Asnières-sur-Seine" },
60
+ // { value: "aubagne", label: "Aubagne" },
61
+ // { value: "aubervilliers", label: "Aubervilliers" },
62
+ // { value: "aulnay-sous-bois", label: "Aulnay-sous-Bois" },
63
+ // { value: "avignon", label: "Avignon" },
64
+ // { value: "avranches", label: "Avranches" },
65
+ // { value: "avoriaz", label: "Avoriaz" },
66
+ // { value: "avray", label: "Avray" },
67
+ ];
68
+
69
+ export const Basic = () => {
70
+ const [value, setValue] = useState<SelectValue>(null);
71
+ return (
72
+ <div className="grid gap-2 grid-cols-1">
73
+ <Select
74
+ searchable={false}
75
+ placeholder="Select your town..."
76
+ options={options}
77
+ value={value}
78
+ onChange={(o) => {
79
+ onChangeAction(o);
80
+ setValue(o.target.value);
81
+ }}
82
+ ></Select>
83
+ <Select
84
+ variant="ghost"
85
+ searchable={false}
86
+ placeholder="Select your town..."
87
+ options={options}
88
+ value={value}
89
+ onChange={(o) => {
90
+ onChangeAction(o);
91
+ setValue(o.target.value);
92
+ }}
93
+ ></Select>
94
+ </div>
95
+ );
96
+ };
97
+
98
+ export const NotRequired = () => {
99
+ const [value, setValue] = useState<SelectValue>(null);
100
+ return (
101
+ <Select
102
+ required={false}
103
+ placeholder="Select your town..."
104
+ options={options}
105
+ value={value}
106
+ onChange={(o) => {
107
+ onChangeAction(o);
108
+ setValue(o.target.value);
109
+ }}
110
+ ></Select>
111
+ );
112
+ };
113
+
114
+ export const Searchable = () => {
115
+ const [value, setValue] = useState<SelectValue>(null);
116
+ return (
117
+ <Select
118
+ searchable={true}
119
+ placeholder="Select your town..."
120
+ options={options}
121
+ value={value}
122
+ onChange={(o) => {
123
+ onChangeAction(o);
124
+ setValue(o.target.value);
125
+ }}
126
+ ></Select>
127
+ );
128
+ };
129
+
130
+ // export const Multiple = () => {
131
+ // const [values, setValues] = useState<string[]>([]);
132
+ // return (
133
+ // <Select
134
+ // multiple={true}
135
+ // placeholder="Select your towns..."
136
+ // options={options}
137
+ // value={values}
138
+ // onChange={(options) => {
139
+ // onChangeAction(options);
140
+ // setValues(options.map((o) => o.value));
141
+ // }}
142
+ // ></Select>
143
+ // );
144
+ // };
145
+
146
+ function isDepartment(department: number | string | null): department is "38" | "73" | "74" {
147
+ return department !== null && ["38", "73", "74"].indexOf(department.toString()) !== -1;
148
+ }
149
+
150
+ export const Dynamic = () => {
151
+ const [town, setTown] = useState<SelectValue>(null);
152
+ const [department, setDepartment] = useState<SelectValue>(null);
153
+ return (
154
+ <>
155
+ <div className="flex flex-col gap-2">
156
+ <Select
157
+ required={false}
158
+ placeholder="Select your department..."
159
+ options={departments}
160
+ value={department}
161
+ onChange={(o) => {
162
+ onChangeAction(o);
163
+ setDepartment(o.target.value);
164
+ setTown(null);
165
+ }}
166
+ ></Select>
167
+ {isDepartment(department) && (
168
+ <Select
169
+ required={false}
170
+ placeholder="Select your town..."
171
+ options={townsByDepartment[department] ?? []}
172
+ value={town}
173
+ onChange={(o) => {
174
+ onChangeAction(o);
175
+ setTown(o.target.value);
176
+ }}
177
+ ></Select>
178
+ )}
179
+ </div>
180
+ </>
181
+ );
182
+ };
183
+
184
+ type StarOption = Option & {
185
+ icon: string;
186
+ };
187
+
188
+ const stars: StarOption[] = [
189
+ { value: "empty", label: "Empty", icon: "fe-star-empty" },
190
+ { value: "half", label: "Half", icon: "fe-star-half" },
191
+ { value: "fill", label: "Fill", icon: "fe-star" },
192
+ ];
193
+
194
+ function SelectOptionComponent({ icon, label }: StarOption) {
195
+ const { activeIndex, selectedIndex, getItemProps, handleSelect } = useSelect();
196
+
197
+ const { ref, index } = useListItem({ label });
198
+ const isActive = activeIndex === index;
199
+ const isSelected = selectedIndex === index;
200
+
201
+ return (
202
+ <button
203
+ className={clsx("option", isSelected && "bg-gray-2", isActive && "bg-gray-1")}
204
+ ref={ref}
205
+ role="option"
206
+ aria-selected={isActive && isSelected}
207
+ tabIndex={isActive ? 0 : -1}
208
+ {...getItemProps({
209
+ onClick: () => handleSelect(index),
210
+ })}
211
+ >
212
+ <i className={icon}></i> {label}
213
+ </button>
214
+ );
215
+ }
216
+
217
+ function SelectSelectionComponent({ label, icon }: SelectSelectionProps<StarOption>) {
218
+ return label ? (
219
+ <span>
220
+ <i className={icon}></i>
221
+ </span>
222
+ ) : (
223
+ <span>?</span>
224
+ );
225
+ }
226
+
227
+ export const CustomRenderer = () => {
228
+ const [value, setValue] = useState<SelectValue>("fill");
229
+ return (
230
+ <>
231
+ <Select
232
+ showArrow={false}
233
+ selectionClassName="ml-auto"
234
+ width="37px"
235
+ placement="bottom-end"
236
+ searchable={false}
237
+ options={stars}
238
+ value={value}
239
+ onChange={(o) => {
240
+ onChangeAction(o);
241
+ setValue(o.target.value);
242
+ }}
243
+ selectSelectionComponent={SelectSelectionComponent}
244
+ selectOptionComponent={SelectOptionComponent}
245
+ ></Select>
246
+ </>
247
+ );
248
+ };
@@ -0,0 +1,362 @@
1
+ import {
2
+ ChangeEvent,
3
+ ReactElement,
4
+ ReactNode,
5
+ Ref,
6
+ forwardRef,
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from "react";
13
+ import {
14
+ FloatingFocusManager,
15
+ FloatingList,
16
+ FloatingPortal,
17
+ Placement,
18
+ autoUpdate,
19
+ flip,
20
+ offset,
21
+ size,
22
+ useClick,
23
+ useDismiss,
24
+ useFloating,
25
+ useInteractions,
26
+ useListNavigation,
27
+ useMergeRefs,
28
+ useRole,
29
+ useTypeahead,
30
+ } from "@floating-ui/react";
31
+ import { SelectContext } from "./useSelectContext.ts";
32
+ import SelectSelection, { SelectSelectionProps } from "./SelectSelection.tsx";
33
+
34
+ import type { Option } from "./interface";
35
+ import clsx from "clsx";
36
+ import { Input } from "../input";
37
+ import { Button } from "../button";
38
+ import { useEventCallback } from "~/hooks";
39
+ import { Dialog } from "../dialog";
40
+
41
+ export type SelectValue = number | string | null;
42
+ type ChangeEventLike = {
43
+ target: {
44
+ value: SelectValue;
45
+ };
46
+ };
47
+
48
+ type Props<O extends Option = Option> = {
49
+ disabled?: boolean;
50
+ variant?: "normal" | "ghost";
51
+ showArrow?: boolean;
52
+ selectionClassName?: string;
53
+ width?: number | string;
54
+ floatingMinWidth?: number;
55
+ placement?: Placement;
56
+ options: O[];
57
+ placeholder?: string;
58
+ getSearchableValue?: (matchReg: RegExp, option: O) => string;
59
+ searchable?: boolean;
60
+ required?: boolean;
61
+ selectOptionComponent?: (props: O) => ReactNode;
62
+ selectSelectionComponent?: (props: SelectSelectionProps<O>) => ReactNode;
63
+ value?: SelectValue;
64
+ onChange?: ((e: ChangeEventLike) => void) | null;
65
+ zIndex?: number;
66
+ };
67
+
68
+ function defaultGetSearchableValue(matchReg: RegExp, option: Option) {
69
+ return matchReg.test(option.label.toLowerCase().trim());
70
+ }
71
+
72
+ const Select = forwardRef<HTMLDivElement, Props>(
73
+ (
74
+ {
75
+ variant = "normal",
76
+ disabled = false,
77
+ showArrow = true,
78
+ selectionClassName,
79
+ width = "100%",
80
+ floatingMinWidth = 130,
81
+ placement = "bottom",
82
+ searchable = false,
83
+ required = true,
84
+ value = null,
85
+ onChange = null,
86
+ placeholder = "Select ...",
87
+ getSearchableValue,
88
+ selectSelectionComponent: SelectSelectionCustomComponent,
89
+ selectOptionComponent: SelectOptionCustomComponent,
90
+ options = [],
91
+ zIndex,
92
+ },
93
+ propRef,
94
+ ) => {
95
+ const isControlled = onChange !== null;
96
+
97
+ const onChangeStable = useEventCallback(onChange);
98
+
99
+ const [uncontrolledSelectedIndex, setUncontrolledSelectedIndex] = useState<number | null>(null);
100
+ const [search, setSearch] = useState("");
101
+
102
+ const filteredOptions = useMemo(
103
+ () =>
104
+ options.filter((option) => {
105
+ if (!searchable || search.trim() === "") {
106
+ return true;
107
+ }
108
+ const fn = getSearchableValue ?? defaultGetSearchableValue;
109
+ const matchReg = new RegExp(search.toLowerCase().trim());
110
+ return fn(matchReg, option);
111
+ }),
112
+ [searchable, search, getSearchableValue, options],
113
+ );
114
+
115
+ let selectedIndex: number | null = null;
116
+ if (isControlled) {
117
+ if (value !== null) {
118
+ const pos = filteredOptions.findIndex((o) => o.value === value);
119
+ if (pos !== -1) {
120
+ selectedIndex = pos;
121
+ }
122
+ }
123
+ } else {
124
+ selectedIndex = uncontrolledSelectedIndex;
125
+ }
126
+
127
+ const [isOpen, setIsOpen] = useState(false);
128
+ const [activeIndex, setActiveIndex] = useState<number | null>(null);
129
+ const [searchHasFocus, setSearchHasFocus] = useState(false);
130
+
131
+ const SelectSelectionComponent = SelectSelectionCustomComponent ?? SelectSelection;
132
+
133
+ function handleSearchChange(e: ChangeEvent<HTMLInputElement>) {
134
+ setActiveIndex(null);
135
+ setSearch(e.target.value);
136
+ }
137
+
138
+ const { refs, floatingStyles, context } = useFloating({
139
+ placement,
140
+ open: isOpen,
141
+ onOpenChange: setIsOpen,
142
+ whileElementsMounted: autoUpdate,
143
+ middleware: [
144
+ offset(5),
145
+ flip({ padding: 10 }),
146
+ size({
147
+ apply({ rects, elements, availableHeight }) {
148
+ Object.assign(elements.floating.style, {
149
+ width: `${Math.max(floatingMinWidth, rects.reference.width)}px`,
150
+ });
151
+ const firstChild = elements.floating.firstElementChild as HTMLElement;
152
+ if (firstChild) {
153
+ firstChild.style.maxHeight = `${Math.min(availableHeight, 300)}px`;
154
+ }
155
+ },
156
+ padding: 10,
157
+ }),
158
+ ],
159
+ });
160
+
161
+ const ref = useMergeRefs([refs.setReference, propRef]);
162
+
163
+ const listRef = useRef<Array<HTMLElement | null>>([]);
164
+ const labelsRef = useRef<Array<string | null>>([]);
165
+ const isTypingRef = useRef(false);
166
+
167
+ const click = useClick(context, { event: "mousedown" });
168
+ const dismiss = useDismiss(context);
169
+ const role = useRole(context, { role: "listbox" });
170
+ const listNav = useListNavigation(context, {
171
+ listRef,
172
+ activeIndex,
173
+ focusItemOnHover: searchable ? false : true,
174
+ selectedIndex,
175
+ onNavigate: setActiveIndex,
176
+ loop: true,
177
+ });
178
+
179
+ const typeahead = useTypeahead(context, {
180
+ enabled: !searchHasFocus,
181
+ listRef: labelsRef,
182
+ activeIndex,
183
+ selectedIndex,
184
+ onMatch: isOpen ? setActiveIndex : undefined,
185
+ onTypingChange(isTyping) {
186
+ isTypingRef.current = isTyping;
187
+ },
188
+ });
189
+
190
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
191
+ listNav,
192
+ typeahead,
193
+ click,
194
+ dismiss,
195
+ role,
196
+ ]);
197
+
198
+ useEffect(() => {
199
+ if (SelectOptionCustomComponent) {
200
+ return;
201
+ }
202
+ filteredOptions.map((option, i) => {
203
+ labelsRef.current[i] = option.label;
204
+ });
205
+ }, [search, filteredOptions, SelectOptionCustomComponent]);
206
+
207
+ const handleSelect = useCallback(
208
+ (index: number | null) => {
209
+ setIsOpen(false);
210
+ setSearch("");
211
+ if (isControlled) {
212
+ const event: ChangeEventLike = {
213
+ target: {
214
+ value: index === null ? null : filteredOptions[index].value,
215
+ },
216
+ };
217
+ onChangeStable(event);
218
+ } else {
219
+ setUncontrolledSelectedIndex(index);
220
+ }
221
+ },
222
+ [isControlled, onChangeStable, filteredOptions],
223
+ );
224
+
225
+ const selectContext = useMemo(
226
+ () => ({
227
+ activeIndex,
228
+ selectedIndex,
229
+ getItemProps,
230
+ handleSelect,
231
+ }),
232
+ [activeIndex, selectedIndex, getItemProps, handleSelect],
233
+ );
234
+
235
+ const showCancelButton = !required && selectedIndex !== null;
236
+
237
+ return (
238
+ <div>
239
+ <div
240
+ className={clsx(
241
+ // "selection",
242
+ "rounded-2xl outline outline-1 outline-offset-[-1px] hover:outline-gray-3 flex focus-full:outline-yellow-4 focus-full:outline-2 cursor-pointer",
243
+ variant === "normal" ? "outline-gray-2" : "outline-transparent",
244
+ selectionClassName,
245
+ isOpen && "focus",
246
+ disabled && "disabled",
247
+ )}
248
+ ref={ref}
249
+ tabIndex={0}
250
+ style={{
251
+ width: typeof width === "number" ? `${width}px` : width,
252
+ }}
253
+ {...getReferenceProps()}
254
+ >
255
+ <span className={clsx("h-8 flex-1 flex items-center px-2")}>
256
+ {selectedIndex !== null ? (
257
+ <SelectSelectionComponent {...filteredOptions[selectedIndex]} key={selectedIndex} />
258
+ ) : (
259
+ placeholder
260
+ )}
261
+ </span>
262
+ {showCancelButton && (
263
+ <Button
264
+ withRipple={false}
265
+ icon
266
+ variant="text"
267
+ color="gray"
268
+ onMouseDown={(e) => {
269
+ // we don't want dropdown to open
270
+ e.stopPropagation();
271
+ }}
272
+ onClick={() => {
273
+ handleSelect(null);
274
+ }}
275
+ >
276
+ <i className="fe-cancel"></i>
277
+ </Button>
278
+ )}
279
+ {!showCancelButton && showArrow && (
280
+ <Button color="gray" withRipple={false} icon variant="text" focusable={false}>
281
+ <i className={isOpen ? "fe-angle-up" : "fe-angle-down"}></i>
282
+ </Button>
283
+ )}
284
+ </div>
285
+ <FloatingPortal>
286
+ <SelectContext.Provider value={selectContext}>
287
+ {isOpen && (
288
+ <FloatingFocusManager context={context} modal={false}>
289
+ <div
290
+ className="z-dialog outline-none"
291
+ ref={refs.setFloating}
292
+ style={{
293
+ ...floatingStyles,
294
+ zIndex,
295
+ }}
296
+ {...getFloatingProps()}
297
+ >
298
+ <Dialog
299
+ placement={context.placement}
300
+ className="animate-fade-in-list max-h-80 overflow-auto"
301
+ >
302
+ {searchable && (
303
+ <div className="p-2">
304
+ <Input
305
+ placeholder="Search"
306
+ tabIndex={selectedIndex === null ? 0 : -1}
307
+ value={search}
308
+ onChange={handleSearchChange}
309
+ onFocus={() => setSearchHasFocus(true)}
310
+ onBlur={() => setSearchHasFocus(false)}
311
+ ></Input>
312
+ </div>
313
+ )}
314
+ {SelectOptionCustomComponent ? (
315
+ <FloatingList elementsRef={listRef} labelsRef={labelsRef}>
316
+ {filteredOptions.map((option) => (
317
+ <SelectOptionCustomComponent {...option} key={option.value} />
318
+ ))}
319
+ </FloatingList>
320
+ ) : (
321
+ filteredOptions.map((option, index) => {
322
+ const isActive = activeIndex === index;
323
+ const isSelected = selectedIndex === index;
324
+
325
+ return (
326
+ <button
327
+ key={option.value}
328
+ className={clsx(
329
+ "option",
330
+ isSelected && "bg-gray-2",
331
+ isActive && "bg-gray-1",
332
+ )}
333
+ role="option"
334
+ aria-selected={isActive && isSelected}
335
+ tabIndex={isActive ? 0 : -1}
336
+ ref={(node) => {
337
+ listRef.current[index] = node;
338
+ }}
339
+ {...getItemProps({
340
+ onClick: () => handleSelect(index),
341
+ })}
342
+ >
343
+ {option.label}
344
+ </button>
345
+ );
346
+ })
347
+ )}
348
+ </Dialog>
349
+ </div>
350
+ </FloatingFocusManager>
351
+ )}
352
+ </SelectContext.Provider>
353
+ </FloatingPortal>
354
+ </div>
355
+ );
356
+ },
357
+
358
+ // see : React with Typescript -- Generics while using React.forwardRef
359
+ // https://stackoverflow.com/questions/58469229/react-with-typescript-generics-while-using-react-forwardref
360
+ ) as <O extends Option>(p: Props<O> & { ref?: Ref<HTMLDivElement> }) => ReactElement;
361
+
362
+ export default Select;
@@ -0,0 +1,29 @@
1
+ import { useListItem } from "@floating-ui/react";
2
+ import { useSelect } from ".";
3
+ import clsx from "clsx";
4
+ import { Option } from "./interface";
5
+
6
+ type Props<O extends Option> = O;
7
+
8
+ export default function SelectOption<O extends Option>({ label }: Props<O>) {
9
+ const { activeIndex, selectedIndex, getItemProps, handleSelect } = useSelect();
10
+
11
+ const { ref, index } = useListItem({ label });
12
+ const isActive = activeIndex === index;
13
+ const isSelected = selectedIndex === index;
14
+
15
+ return (
16
+ <button
17
+ className={clsx("option", isSelected && "bg-gray-2", isActive && "bg-gray-1")}
18
+ ref={ref}
19
+ role="option"
20
+ aria-selected={isActive && isSelected}
21
+ tabIndex={isActive ? 0 : -1}
22
+ {...getItemProps({
23
+ onClick: () => handleSelect(index),
24
+ })}
25
+ >
26
+ {label}
27
+ </button>
28
+ );
29
+ }
@@ -0,0 +1,13 @@
1
+ import Badge from "../badge/Badge";
2
+ import type { Option } from "./interface";
3
+
4
+ export type SelectSelectionProps<O extends Option> = O & {
5
+ multiple?: boolean;
6
+ };
7
+
8
+ export default function SelectSelection<O extends Option>({
9
+ label,
10
+ multiple = false,
11
+ }: SelectSelectionProps<O>) {
12
+ return multiple ? <Badge className="ml-1">{label}</Badge> : <span>{label}</span>;
13
+ }
@@ -0,0 +1,5 @@
1
+ export { default as SelectOption } from "./SelectOption";
2
+ export { default as SelectSelection, type SelectSelectionProps } from "./SelectSelection";
3
+ export { default as Select } from "./Select";
4
+ export { default as useSelect, SelectContext } from "./useSelectContext";
5
+ export type * from "./interface";
@@ -0,0 +1,36 @@
1
+ import { BBox, Geometry } from "geojson";
2
+
3
+ export type OptionLike = Option | GeoOption;
4
+
5
+ export type Option = {
6
+ type?: "Option";
7
+ value: string;
8
+ label: string;
9
+ sourceId?: string | number;
10
+ };
11
+
12
+ export type NoDataOption = {
13
+ id: string;
14
+ type: "nodata";
15
+ };
16
+
17
+ /** For compatibility with OptionLike. get original types from pentatrion-geo */
18
+ type GeoOption<G extends Geometry | null = Geometry, T extends string = string> = {
19
+ id: string;
20
+ type: "Feature";
21
+ properties: FeatureProperties<T>;
22
+ geometry: G;
23
+ bbox?: BBox | undefined;
24
+ };
25
+
26
+ type FeatureProperties<T extends string = string> = {
27
+ id: string;
28
+
29
+ /** computed name + short context for input string */
30
+ label: string;
31
+ name: string;
32
+ context: string | null;
33
+ score: number;
34
+ type: T;
35
+ originalProperties?: any;
36
+ };