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,316 @@
1
+ import {
2
+ ChangeEvent,
3
+ KeyboardEvent,
4
+ useCallback,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ forwardRef,
9
+ ForwardedRef,
10
+ ReactNode,
11
+ } from "react";
12
+ import type { OptionLike, Option } from "../select";
13
+ import AutocompleteOption, { AutocompleteOptionProps } from "./AutocompleteOption.tsx";
14
+ import {
15
+ FloatingFocusManager,
16
+ FloatingList,
17
+ FloatingPortal,
18
+ Placement,
19
+ autoUpdate,
20
+ flip,
21
+ offset,
22
+ size,
23
+ useDismiss,
24
+ useFloating,
25
+ useInteractions,
26
+ useListNavigation,
27
+ useRole,
28
+ } from "@floating-ui/react";
29
+ import { AutocompleteContext } from "./useAutocompleteContext.ts";
30
+ import clsx from "clsx";
31
+ import { useEventCallback } from "~/hooks";
32
+
33
+ import { getOptionLabel, getOptionValue } from "./util.ts";
34
+ import { Button } from "../button";
35
+ import { Dialog } from "../dialog";
36
+ import Loader from "../loader/Loader";
37
+ import { inputConfig } from "../input/Input.tsx";
38
+ import { ThemeColor } from "~/types.d";
39
+
40
+ export interface AutocompleteProps<O extends OptionLike = Option> {
41
+ className?: string;
42
+
43
+ icon?: boolean | ReactNode;
44
+
45
+ color?: ThemeColor;
46
+
47
+ /**
48
+ * render suffix only if selection
49
+ * ex: add contextuel buttons associated to selection
50
+ */
51
+ selectionSuffix?: ReactNode;
52
+ /**
53
+ * render suffix only if selection and searchValue is empty
54
+ */
55
+ noSearchSuffix?: ReactNode;
56
+
57
+ placement?: Placement;
58
+ placeholder?: string;
59
+ /**
60
+ * search string used as value for <input>
61
+ */
62
+ searchValue: string;
63
+ /**
64
+ * change handler when typing in the input
65
+ * or when clicking on an option (the label value is passed)
66
+ */
67
+ onChangeSearchValue: (value: string, selectionLabel: boolean) => void;
68
+
69
+ selection?: O | null;
70
+ /**
71
+ * select handler for a selection from the autocomplete's dropdown
72
+ * the option object is passed when clicking on an option
73
+ * null value is passed when typing a new character in the input search.
74
+ */
75
+ onChangeSelection: (option: O | null) => void;
76
+ options: O[];
77
+
78
+ autocompleteOptionComponent?: (props: AutocompleteOptionProps<O>) => ReactNode;
79
+
80
+ loading?: boolean;
81
+
82
+ clearSearchButton?: boolean;
83
+
84
+ selectOnFocus?: boolean;
85
+ }
86
+
87
+ function Autocomplete<O extends OptionLike = Option>(
88
+ {
89
+ className,
90
+ icon = true,
91
+ color = "yellow",
92
+ selectionSuffix,
93
+ noSearchSuffix,
94
+ placement = "bottom",
95
+ placeholder = "Search...",
96
+ selection = null,
97
+ onChangeSelection,
98
+ autocompleteOptionComponent,
99
+ loading = false,
100
+ clearSearchButton = false,
101
+ searchValue,
102
+ onChangeSearchValue,
103
+ options,
104
+ selectOnFocus = true,
105
+ }: AutocompleteProps<O>,
106
+ inputRef: ForwardedRef<HTMLInputElement>,
107
+ ) {
108
+ const [isOpen, setIsOpen] = useState(false);
109
+ const [activeIndex, setActiveIndex] = useState<number | null>(null);
110
+
111
+ const onChangeSearchValueStable = useEventCallback(onChangeSearchValue);
112
+ const onChangeSelectionStable = useEventCallback(onChangeSelection);
113
+
114
+ const OptionComponent = autocompleteOptionComponent ?? AutocompleteOption;
115
+
116
+ const listRef = useRef<Array<HTMLElement | null>>([]);
117
+
118
+ const { refs, floatingStyles, context } = useFloating({
119
+ placement,
120
+ open: isOpen,
121
+ onOpenChange: setIsOpen,
122
+ whileElementsMounted: autoUpdate,
123
+ middleware: [
124
+ offset(5),
125
+ flip({ padding: 10 }),
126
+ size({
127
+ apply({ rects, elements, availableHeight }) {
128
+ Object.assign(elements.floating.style, {
129
+ width: `${Math.max(130, rects.reference.width)}px`,
130
+ });
131
+ const firstChild = elements.floating.firstElementChild as HTMLElement;
132
+ if (firstChild) {
133
+ firstChild.style.maxHeight = `${Math.min(availableHeight, 288)}px`;
134
+ }
135
+ },
136
+ padding: 10,
137
+ }),
138
+ ],
139
+ });
140
+
141
+ const role = useRole(context, { role: "listbox" });
142
+ const dismiss = useDismiss(context);
143
+ const listNav = useListNavigation(context, {
144
+ listRef,
145
+ activeIndex,
146
+ onNavigate: setActiveIndex,
147
+ /* we want the real focus to stay on the input, so focus associated to active items is simulated */
148
+ virtual: true,
149
+ loop: true,
150
+ });
151
+
152
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
153
+ role,
154
+ dismiss,
155
+ listNav,
156
+ ]);
157
+
158
+ function handleChangeSearchValue(e: ChangeEvent<HTMLInputElement>) {
159
+ const value = e.target.value;
160
+
161
+ onChangeSearchValueStable(value, false);
162
+
163
+ if (value !== searchValue && selection !== null) {
164
+ onChangeSelectionStable(null);
165
+ setActiveIndex(null);
166
+ return;
167
+ }
168
+
169
+ if (value) {
170
+ setIsOpen(true);
171
+ /**
172
+ * bug whith LazyAutocomplete activeIndex is set to 0 but the options array has immediately 0 elements
173
+ * so focus can't be set to the first Dom element.
174
+ */
175
+ setActiveIndex(listRef.current[0] ? 0 : null);
176
+ } else {
177
+ setIsOpen(false);
178
+ setActiveIndex(null);
179
+ }
180
+ }
181
+
182
+ const handleSelect = useCallback(
183
+ (index: number | null) => {
184
+ if (index === null) {
185
+ onChangeSelectionStable(null);
186
+ } else if (options[index]) {
187
+ const newSelection = options[index];
188
+ onChangeSelectionStable(newSelection);
189
+
190
+ onChangeSearchValueStable(getOptionLabel(newSelection), true);
191
+ }
192
+ setActiveIndex(null);
193
+ setIsOpen(false);
194
+ },
195
+
196
+ [options, onChangeSelectionStable, onChangeSearchValueStable],
197
+ );
198
+
199
+ const autocompleteContext = useMemo(
200
+ () => ({
201
+ activeIndex,
202
+ selection,
203
+ getItemProps,
204
+ handleSelect,
205
+ }),
206
+ [activeIndex, selection, getItemProps, handleSelect],
207
+ );
208
+
209
+ return (
210
+ <div className={className}>
211
+ <div className={clsx(inputConfig.container)} ref={refs.setReference} data-color={color}>
212
+ {icon !== false && (
213
+ <div className="flex-center relative">
214
+ {loading && <Loader size="medium" color="gray" className="absolute top-0 left-0" />}
215
+ {icon === true ? (
216
+ <span className="w-8 h-8 flex-center">
217
+ <i className="fe-search"></i>
218
+ </span>
219
+ ) : (
220
+ icon
221
+ )}
222
+ </div>
223
+ )}
224
+ <input
225
+ spellCheck="false"
226
+ className={clsx(inputConfig.input, icon === false && "pl-4")}
227
+ ref={inputRef}
228
+ type="search"
229
+ value={searchValue}
230
+ placeholder={placeholder}
231
+ aria-autocomplete="list"
232
+ {...getReferenceProps({
233
+ onFocus() {
234
+ if (selectOnFocus) {
235
+ (document.activeElement as HTMLInputElement)?.select();
236
+ }
237
+ },
238
+ onChange: handleChangeSearchValue,
239
+ onMouseDown() {
240
+ if (options.length === 0) {
241
+ return;
242
+ }
243
+ /**
244
+ * There is a already selection
245
+ * and the only element available is the selection,
246
+ * no need to display the dropdown.
247
+ */
248
+ // if (options.length === 1 && options[0].value === selection?.value) {
249
+ // return;
250
+ // }
251
+ setIsOpen((isOpen) => !isOpen);
252
+ },
253
+ onKeyDown(e: KeyboardEvent<HTMLInputElement>) {
254
+ if (e.key === "Enter" && activeIndex != null) {
255
+ handleSelect(activeIndex);
256
+ }
257
+ },
258
+ })}
259
+ />
260
+ <div className="flex-center relative">
261
+ {selection && selectionSuffix}
262
+ {clearSearchButton && (searchValue.trim() !== "" || selection !== null) && (
263
+ <Button
264
+ withRipple={false}
265
+ icon
266
+ color="gray"
267
+ variant="text"
268
+ onClick={() => {
269
+ setIsOpen(false);
270
+ onChangeSelectionStable(null);
271
+ setActiveIndex(null);
272
+ onChangeSearchValueStable("", true);
273
+ }}
274
+ >
275
+ <i className="fe-cancel"></i>
276
+ </Button>
277
+ )}
278
+ {icon === false && loading && <Loader size="medium" color="gray" />}
279
+ {!selection && searchValue === "" && noSearchSuffix}
280
+ </div>
281
+ </div>
282
+ <FloatingPortal>
283
+ <AutocompleteContext.Provider value={autocompleteContext}>
284
+ {isOpen && (
285
+ <FloatingFocusManager
286
+ disabled={selectOnFocus}
287
+ context={context}
288
+ initialFocus={-1}
289
+ visuallyHiddenDismiss
290
+ >
291
+ <div
292
+ className="z-dialog"
293
+ ref={refs.setFloating}
294
+ style={floatingStyles}
295
+ {...getFloatingProps()}
296
+ >
297
+ <Dialog
298
+ placement={context.placement}
299
+ className="animate-fade-in-list overflow-auto"
300
+ >
301
+ <FloatingList elementsRef={listRef}>
302
+ {options.map((option) => {
303
+ return <OptionComponent {...option} key={getOptionValue(option)} />;
304
+ })}
305
+ </FloatingList>
306
+ </Dialog>
307
+ </div>
308
+ </FloatingFocusManager>
309
+ )}
310
+ </AutocompleteContext.Provider>
311
+ </FloatingPortal>
312
+ </div>
313
+ );
314
+ }
315
+
316
+ export default forwardRef(Autocomplete);
@@ -0,0 +1,35 @@
1
+ import { memo, useId } from "react";
2
+ import { OptionLike } from "../select";
3
+ import { useListItem } from "@floating-ui/react";
4
+ import useAutocomplete from "./useAutocompleteContext";
5
+ import clsx from "clsx";
6
+ import { getOptionLabel, getOptionValue } from "./util";
7
+
8
+ export type AutocompleteOptionProps<O extends OptionLike> = O;
9
+
10
+ function AutocompleteOption<O extends OptionLike>(props: AutocompleteOptionProps<O>) {
11
+ const label = getOptionLabel(props);
12
+
13
+ const id = useId();
14
+ const { activeIndex, selection, getItemProps, handleSelect } = useAutocomplete();
15
+ const { ref, index } = useListItem({ label });
16
+ const isActive = activeIndex === index;
17
+ const isSelected = selection ? getOptionValue(selection) === getOptionValue(props) : false;
18
+
19
+ return (
20
+ <div
21
+ className={clsx("option", isSelected && "bg-gray-2", isActive && "bg-gray-1")}
22
+ ref={ref}
23
+ role="option"
24
+ id={id}
25
+ aria-selected={isActive}
26
+ {...getItemProps({
27
+ onClick: () => handleSelect(index),
28
+ })}
29
+ >
30
+ {label}
31
+ </div>
32
+ );
33
+ }
34
+
35
+ export default memo(AutocompleteOption);
@@ -0,0 +1,130 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { Autocomplete, AutocompleteProps } from ".";
3
+ import { OptionLike, Option } from "../select";
4
+ import { getOptionLabel } from "./util";
5
+ import { useStateDebounce, useEventCallback } from "~/hooks";
6
+
7
+ interface Props<O extends OptionLike = Option>
8
+ // Omit "searchValue" | "onChangeSearchValue" | "options"
9
+ extends Pick<
10
+ AutocompleteProps<O>,
11
+ | "className"
12
+ | "icon"
13
+ | "color"
14
+ | "selectionSuffix"
15
+ | "noSearchSuffix"
16
+ | "placement"
17
+ | "placeholder"
18
+ | "selection"
19
+ | "onChangeSelection"
20
+ | "autocompleteOptionComponent"
21
+ | "loading"
22
+ | "clearSearchButton"
23
+ > {
24
+ onChangeSearchValueCallback: (searchValue: string) => Promise<O[]>;
25
+ debounce?: number;
26
+ }
27
+
28
+ export default function LazyAutocomplete<O extends OptionLike = Option>({
29
+ onChangeSearchValueCallback,
30
+ debounce = 500,
31
+ selection = null,
32
+ onChangeSelection,
33
+ ...rest
34
+ }: Props<O>) {
35
+ const onChangeSelectionStable = useEventCallback(onChangeSelection);
36
+
37
+ const [searchValue, searchValueDebounced, setSearchValue] = useStateDebounce(
38
+ selection ? getOptionLabel(selection) : "",
39
+ debounce,
40
+ );
41
+
42
+ const searchValueRef = useRef(searchValue);
43
+ searchValueRef.current = searchValue;
44
+
45
+ const inputRef = useRef<HTMLInputElement>(null!);
46
+
47
+ const [loading, setLoading] = useState(false);
48
+
49
+ const [options, setOptions] = useState<O[]>([]);
50
+
51
+ const handleChangeSelection = useCallback(
52
+ (selection: O | null) => {
53
+ onChangeSelectionStable(selection);
54
+ if (selection) {
55
+ setOptions([]);
56
+ }
57
+ },
58
+ [onChangeSelectionStable],
59
+ );
60
+
61
+ // side effect, update the searchValue <input /> value when
62
+ // selection come from outside
63
+ useLayoutEffect(() => {
64
+ if (selection === null) {
65
+ if (document.activeElement !== inputRef.current) {
66
+ setSearchValue("");
67
+ }
68
+ return;
69
+ }
70
+
71
+ // we need the fresh value of searchValue but we don't
72
+ // want searchValue as useEffect dependency
73
+ const selectionLabel = getOptionLabel(selection);
74
+ if (searchValueRef.current !== selectionLabel) {
75
+ setSearchValue(selectionLabel, true);
76
+ }
77
+ }, [selection, setSearchValue]);
78
+
79
+ /**
80
+ * set new options when searchValueDebounced change
81
+ */
82
+ useEffect(() => {
83
+ let abort = false;
84
+
85
+ // when we're selecting option we change searchValue to be the complete label
86
+ // value but we don't want to fetch new research
87
+ if (selection) {
88
+ return;
89
+ }
90
+
91
+ if (searchValueDebounced.trim() === "") {
92
+ setOptions([]);
93
+ return;
94
+ }
95
+
96
+ setLoading(true);
97
+
98
+ onChangeSearchValueCallback(searchValueDebounced)
99
+ .then((newOptions) => {
100
+ setLoading(false);
101
+
102
+ if (!abort) {
103
+ setOptions(newOptions);
104
+ }
105
+ })
106
+ .catch(() => {
107
+ setLoading(false);
108
+ });
109
+
110
+ return () => {
111
+ setLoading(false);
112
+ abort = true;
113
+ };
114
+ }, [selection, searchValueDebounced, onChangeSearchValueCallback]);
115
+
116
+ return (
117
+ <Autocomplete
118
+ ref={inputRef}
119
+ searchValue={searchValue}
120
+ onChangeSearchValue={(newValue, immediate) => {
121
+ setSearchValue(newValue, immediate || newValue === "" ? true : false);
122
+ }}
123
+ selection={selection}
124
+ onChangeSelection={handleChangeSelection}
125
+ options={options}
126
+ loading={loading}
127
+ {...rest}
128
+ />
129
+ );
130
+ }
@@ -0,0 +1,74 @@
1
+ import { useLayoutEffect, useRef, useState } from "react";
2
+ import { Autocomplete, AutocompleteProps } from ".";
3
+ import { Option, OptionLike } from "../select";
4
+ import { useEventCallback } from "~/hooks";
5
+ import { getOptionLabel } from "./util";
6
+
7
+ interface Props<O extends OptionLike = Option>
8
+ extends Pick<
9
+ AutocompleteProps<O>,
10
+ | "className"
11
+ | "icon"
12
+ | "color"
13
+ | "placement"
14
+ | "placeholder"
15
+ | "selection"
16
+ | "onChangeSelection"
17
+ | "autocompleteOptionComponent"
18
+ | "options"
19
+ | "selection"
20
+ > {}
21
+
22
+ export default function SimpleAutocomplete<O extends OptionLike = Option>({
23
+ options,
24
+ selection = null,
25
+ onChangeSelection,
26
+ ...rest
27
+ }: Props<O>) {
28
+ const onChangeSelectionStable = useEventCallback(onChangeSelection);
29
+
30
+ const [searchValue, setSearchValue] = useState(selection ? getOptionLabel(selection) : "");
31
+
32
+ const searchValueRef = useRef(searchValue);
33
+ searchValueRef.current = searchValue;
34
+
35
+ const inputRef = useRef<HTMLInputElement>(null!);
36
+
37
+ // side effect, update the searchValue <input /> value when
38
+ // selection come from outside
39
+ useLayoutEffect(() => {
40
+ if (selection === null) {
41
+ if (document.activeElement !== inputRef.current) {
42
+ setSearchValue("");
43
+ }
44
+ return;
45
+ }
46
+
47
+ // we need the fresh value of searchValue but we don't
48
+ // want searchValue as useEffect dependency
49
+ const selectionLabel = getOptionLabel(selection);
50
+ if (searchValueRef.current !== getOptionLabel(selection)) {
51
+ setSearchValue(selectionLabel);
52
+ }
53
+ }, [selection, setSearchValue]);
54
+
55
+ const filteredOptions =
56
+ selection !== null
57
+ ? []
58
+ : options.filter((option) => {
59
+ const optionLabel = getOptionLabel(option);
60
+ return optionLabel.toLowerCase().startsWith(searchValue.toLowerCase());
61
+ });
62
+
63
+ return (
64
+ <Autocomplete
65
+ ref={inputRef}
66
+ searchValue={searchValue}
67
+ onChangeSearchValue={setSearchValue}
68
+ selection={selection}
69
+ onChangeSelection={onChangeSelectionStable}
70
+ options={filteredOptions}
71
+ {...rest}
72
+ />
73
+ );
74
+ }
@@ -0,0 +1,6 @@
1
+ export { default as Autocomplete, type AutocompleteProps } from "./Autocomplete";
2
+ export { default as SimpleAutocomplete } from "./SimpleAutocomplete";
3
+ export { default as AutocompleteOption } from "./AutocompleteOption";
4
+ export { default as LazyAutocomplete } from "./LazyAutocomplete";
5
+ export { default as useAutocomplete } from "./useAutocompleteContext";
6
+ export { getOptionValue, getOptionLabel } from "./util";
@@ -0,0 +1,20 @@
1
+ import { useInteractions } from "@floating-ui/react";
2
+ import { createContext, useContext } from "react";
3
+ import { OptionLike } from "../select";
4
+
5
+ interface AutocompleteContext {
6
+ activeIndex: number | null;
7
+ selection: OptionLike | null;
8
+ getItemProps: ReturnType<typeof useInteractions>["getItemProps"];
9
+ handleSelect: (index: number | null) => void;
10
+ }
11
+
12
+ export const AutocompleteContext = createContext<AutocompleteContext | null>(null);
13
+
14
+ export default function useAutocomplete() {
15
+ const context = useContext(AutocompleteContext);
16
+ if (context === null) {
17
+ throw new Error("Autocomplete components must be wrapped in <Autocomplete>");
18
+ }
19
+ return context;
20
+ }
@@ -0,0 +1,19 @@
1
+ import { OptionLike } from "../select";
2
+
3
+ export function getOptionLabel(option: OptionLike) {
4
+ switch (option.type) {
5
+ case "Feature":
6
+ return option.properties.label;
7
+ default:
8
+ return option.label;
9
+ }
10
+ }
11
+
12
+ export function getOptionValue(option: OptionLike) {
13
+ switch (option.type) {
14
+ case "Feature":
15
+ return option.properties.id;
16
+ default:
17
+ return option.value;
18
+ }
19
+ }
@@ -0,0 +1,72 @@
1
+ import Badge from "./Badge";
2
+ import { Meta, StoryObj } from "@storybook/react";
3
+ import { action } from "@storybook/addon-actions";
4
+
5
+ const onClickAction = action("onClick");
6
+ const onRemoveAction = action("onRemove");
7
+
8
+ const meta = {
9
+ title: "Components/Badge",
10
+ component: Badge,
11
+ argTypes: {
12
+ color: {
13
+ control: {
14
+ type: "select",
15
+ },
16
+ options: ["yellow", "gray", "red", "orange", "green", "blue"],
17
+ },
18
+ },
19
+ } satisfies Meta<typeof Badge>;
20
+ export default meta;
21
+
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ export const Playbook: Story = {
25
+ args: {
26
+ tooltip: "More infos...",
27
+ url: "https://lonlat.org",
28
+ color: "yellow",
29
+ children: "Lonlat",
30
+ },
31
+ };
32
+
33
+ export const Context = () => {
34
+ return (
35
+ <div className="flex flex-col gap-2">
36
+ <Badge color="yellow">Primary</Badge>
37
+ <Badge color="orange">Warning</Badge>
38
+ <Badge color="green">Success</Badge>
39
+ <Badge color="red">Danger</Badge>
40
+ <Badge color="blue">Info</Badge>
41
+ <Badge color="gray">Weak</Badge>
42
+ <Badge color="yellow" onClick={onClickAction}>
43
+ Primary clickable
44
+ </Badge>
45
+ <Badge color="orange" onClick={onClickAction}>
46
+ Warning clickable
47
+ </Badge>
48
+ <Badge color="green" onClick={onClickAction}>
49
+ Success clickable
50
+ </Badge>
51
+ <Badge color="red" onClick={onClickAction}>
52
+ Danger clickable
53
+ </Badge>
54
+ <Badge color="blue" onClick={onClickAction}>
55
+ Info clickable
56
+ </Badge>
57
+ <Badge color="gray" onClick={onClickAction}>
58
+ Weak clickable
59
+ </Badge>
60
+ <Badge color="yellow" onRemove={onRemoveAction}>
61
+ Primary removable
62
+ </Badge>
63
+ <Badge color="yellow" onClick={onClickAction} onRemove={onRemoveAction}>
64
+ Primary clickable removable
65
+ </Badge>
66
+
67
+ <Badge color="yellow" tooltip="More infos...">
68
+ With tooltip !
69
+ </Badge>
70
+ </div>
71
+ );
72
+ };