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.
- package/.storybook/main.ts +33 -0
- package/.storybook/manager.ts +8 -0
- package/.storybook/preview-head.html +2 -0
- package/.storybook/preview.ts +26 -0
- package/.storybook/public/bg-map.jpg +0 -0
- package/.storybook/public/favicon-32x32.png +0 -0
- package/.storybook/public/favicon.svg +28 -0
- package/.storybook/public/movies.json +2153 -0
- package/.storybook/public/population.json +1178 -0
- package/.storybook/public/town-74.geojson +8995 -0
- package/.storybook/public/town-74.json +4062 -0
- package/.storybook/theme.ts +14 -0
- package/Makefile +14 -0
- package/README.md +40 -0
- package/TODO.md +5 -0
- package/dist/index.d.ts +1110 -0
- package/dist/index.js +3707 -0
- package/dist/redux/index.d.ts +43 -0
- package/dist/redux/index.js +583 -0
- package/dist/types.d-DlHa_IaW.d.ts +14 -0
- package/images/logo-shadow.svg +173 -0
- package/images/logo.svg +76 -0
- package/package.json +85 -0
- package/postcss.config.js +7 -0
- package/src/components/autocomplete/Autocomplete.stories.tsx +89 -0
- package/src/components/autocomplete/Autocomplete.tsx +316 -0
- package/src/components/autocomplete/AutocompleteOption.tsx +35 -0
- package/src/components/autocomplete/LazyAutocomplete.tsx +130 -0
- package/src/components/autocomplete/SimpleAutocomplete.tsx +74 -0
- package/src/components/autocomplete/index.ts +6 -0
- package/src/components/autocomplete/useAutocompleteContext.ts +20 -0
- package/src/components/autocomplete/util.ts +19 -0
- package/src/components/badge/Badge.stories.tsx +72 -0
- package/src/components/badge/Badge.tsx +70 -0
- package/src/components/button/Button.stories.tsx +189 -0
- package/src/components/button/Button.test.tsx +20 -0
- package/src/components/button/Button.tsx +200 -0
- package/src/components/button/ButtonGroup.stories.tsx +46 -0
- package/src/components/button/ButtonGroup.tsx +29 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/card/Card.stories.tsx +21 -0
- package/src/components/card/config.ts +4 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/code/Code.tsx +15 -0
- package/src/components/context-menu/ContextMenu.stories.tsx +22 -0
- package/src/components/context-menu/ContextMenu.tsx +175 -0
- package/src/components/context-menu/ContextMenuItem.tsx +34 -0
- package/src/components/context-menu/index.ts +2 -0
- package/src/components/dialog/Dialog.stories.tsx +270 -0
- package/src/components/dialog/Dialog.tsx +46 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/dialog/util.ts +29 -0
- package/src/components/dropdown-menu/DropdownMenu.stories.tsx +19 -0
- package/src/components/dropdown-menu/DropdownMenu.tsx +12 -0
- package/src/components/dropdown-menu/DropdownMenuContent.tsx +46 -0
- package/src/components/dropdown-menu/DropdownMenuItem.tsx +33 -0
- package/src/components/dropdown-menu/DropdownMenuTrigger.tsx +42 -0
- package/src/components/dropdown-menu/index.ts +7 -0
- package/src/components/dropdown-menu/interface.d.ts +13 -0
- package/src/components/dropdown-menu/useDropdownMenu.tsx +115 -0
- package/src/components/dropdown-menu/useDropdownMenuContext.ts +16 -0
- package/src/components/dropdown-menu-nested/DropdownMenuNested.stories.tsx +33 -0
- package/src/components/dropdown-menu-nested/DropdownMenuNested.tsx +19 -0
- package/src/components/dropdown-menu-nested/MenuContext.ts +15 -0
- package/src/components/dropdown-menu-nested/MenuItem.tsx +43 -0
- package/src/components/dropdown-menu-nested/MenuItemWithChildren.tsx +204 -0
- package/src/components/dropdown-menu-nested/index.ts +3 -0
- package/src/components/flash/Flash.stories.tsx +34 -0
- package/src/components/flash/Flash.tsx +24 -0
- package/src/components/highlight/Highlight.stories.tsx +128 -0
- package/src/components/highlight/Highlight.tsx +29 -0
- package/src/components/href/Href.stories.tsx +55 -0
- package/src/components/href/Href.tsx +18 -0
- package/src/components/input/Checkbox.stories.tsx +52 -0
- package/src/components/input/Checkbox.tsx +41 -0
- package/src/components/input/Input.stories.tsx +103 -0
- package/src/components/input/Input.test.tsx +10 -0
- package/src/components/input/Input.tsx +60 -0
- package/src/components/input/RadioGroup.stories.tsx +76 -0
- package/src/components/input/RadioGroup.tsx +84 -0
- package/src/components/input/Toggle.stories.tsx +29 -0
- package/src/components/input/Toggle.tsx +35 -0
- package/src/components/input/index.ts +4 -0
- package/src/components/input-field/InputField.stories.tsx +227 -0
- package/src/components/input-field/InputField.tsx +62 -0
- package/src/components/loader/Loader.stories.tsx +37 -0
- package/src/components/loader/Loader.tsx +54 -0
- package/src/components/modal/Components.tsx +58 -0
- package/src/components/modal/Modal.stories.tsx +113 -0
- package/src/components/modal/Modal.tsx +13 -0
- package/src/components/modal/ModalContent.tsx +46 -0
- package/src/components/modal/ModalTrigger.tsx +41 -0
- package/src/components/modal/index.ts +7 -0
- package/src/components/modal/interface.d.ts +10 -0
- package/src/components/modal/useModal.ts +48 -0
- package/src/components/modal/useModalContext.ts +16 -0
- package/src/components/notification/Notification.stories.tsx +51 -0
- package/src/components/notification/NotificationsContext.ts +79 -0
- package/src/components/notification/NotificationsProvider.tsx +58 -0
- package/src/components/notification/index.ts +2 -0
- package/src/components/popover/Components.tsx +45 -0
- package/src/components/popover/Popover.stories.tsx +46 -0
- package/src/components/popover/Popover.test.tsx +21 -0
- package/src/components/popover/Popover.tsx +12 -0
- package/src/components/popover/PopoverContent.tsx +46 -0
- package/src/components/popover/PopoverTrigger.tsx +42 -0
- package/src/components/popover/index.ts +7 -0
- package/src/components/popover/interface.d.ts +13 -0
- package/src/components/popover/usePopover.tsx +80 -0
- package/src/components/popover/usePopoverContext.ts +16 -0
- package/src/components/portal/Portal.tsx +12 -0
- package/src/components/quick-settings/QuickSettings.stories.tsx +182 -0
- package/src/components/resize-area/ResizeArea.stories.css +40 -0
- package/src/components/resize-area/ResizeArea.stories.tsx +30 -0
- package/src/components/resize-area/ResizeArea.tsx +79 -0
- package/src/components/scroll/Scroll.stories.tsx +81 -0
- package/src/components/scroll/Scroll.tsx +111 -0
- package/src/components/select/Select.stories.tsx +248 -0
- package/src/components/select/Select.tsx +362 -0
- package/src/components/select/SelectOption.tsx +29 -0
- package/src/components/select/SelectSelection.tsx +13 -0
- package/src/components/select/index.ts +5 -0
- package/src/components/select/interface.d.ts +36 -0
- package/src/components/select/useSelectContext.ts +19 -0
- package/src/components/select/util.ts +12 -0
- package/src/components/snack/Snack.stories.tsx +33 -0
- package/src/components/snack/Snack.tsx +47 -0
- package/src/components/sortable/Sortable.stories.tsx +98 -0
- package/src/components/sortable/Sortable.tsx +37 -0
- package/src/components/sortable/index.ts +2 -0
- package/src/components/sortable/types.d.ts +12 -0
- package/src/components/steps/Step.tsx +41 -0
- package/src/components/steps/Steps.stories.tsx +141 -0
- package/src/components/steps/Steps.tsx +49 -0
- package/src/components/steps/index.ts +3 -0
- package/src/components/steps/util.ts +3 -0
- package/src/components/table/Table.stories.tsx +36 -0
- package/src/components/table/Table.tsx +13 -0
- package/src/components/table/TableBody.tsx +9 -0
- package/src/components/table/TableCell.tsx +16 -0
- package/src/components/table/TableFooter.tsx +9 -0
- package/src/components/table/TableHeader.tsx +14 -0
- package/src/components/table/TableHeaderCell.tsx +14 -0
- package/src/components/table/TableRow.tsx +14 -0
- package/src/components/table/index.ts +7 -0
- package/src/components/tabs/Tabs.stories.tsx +66 -0
- package/src/components/tabs/Tabs.tsx +80 -0
- package/src/components/tooltip/SimpleTooltip.tsx +17 -0
- package/src/components/tooltip/Tooltip.stories.tsx +129 -0
- package/src/components/tooltip/Tooltip.test.tsx +18 -0
- package/src/components/tooltip/Tooltip.tsx +13 -0
- package/src/components/tooltip/TooltipContent.tsx +41 -0
- package/src/components/tooltip/TooltipTrigger.tsx +36 -0
- package/src/components/tooltip/index.ts +6 -0
- package/src/components/tooltip/interface.d.ts +15 -0
- package/src/components/tooltip/useTooltip.ts +87 -0
- package/src/components/tooltip/useTooltipContext.ts +16 -0
- package/src/env.d.ts +27 -0
- package/src/hooks/index.ts +13 -0
- package/src/hooks/useCombinedRefs.test.tsx +42 -0
- package/src/hooks/useCombinedRefs.ts +21 -0
- package/src/hooks/useCopyToClipboard.ts +28 -0
- package/src/hooks/useDebounce.ts +59 -0
- package/src/hooks/useEventCallback.ts +13 -0
- package/src/hooks/useEventListener.ts +86 -0
- package/src/hooks/useFetch.tsx +21 -0
- package/src/hooks/useIsClosing.ts +25 -0
- package/src/hooks/useIsMounted.tsx +16 -0
- package/src/hooks/useIsomorphicLayoutEffect.ts +4 -0
- package/src/hooks/useNotifications.tsx +13 -0
- package/src/hooks/useOnClickOutside.ts +35 -0
- package/src/hooks/usePrevious.ts +9 -0
- package/src/hooks/useRipple.test.tsx +52 -0
- package/src/hooks/useRipple.tsx +79 -0
- package/src/index.css +14 -0
- package/src/index.ts +39 -0
- package/src/lib/arrUtil.ts +8 -0
- package/src/lib/error.ts +18 -0
- package/src/lib/fetch.test.ts +107 -0
- package/src/lib/fetch.ts +115 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/tailwindVariants.ts +35 -0
- package/src/redux/index.ts +1 -0
- package/src/redux/notification/components/NotificationConsumer.tsx +52 -0
- package/src/redux/notification/errorCatcherMiddleware.ts +17 -0
- package/src/redux/notification/hooks/useNotification.ts +26 -0
- package/src/redux/notification/index.ts +5 -0
- package/src/redux/notification/notificationSlice.ts +68 -0
- package/src/styles/Animation.stories.tsx +43 -0
- package/src/styles/Shadow.stories.tsx +43 -0
- package/src/styles/Themes.stories.tsx +31 -0
- package/src/styles/Typography.stories.tsx +101 -0
- package/src/tailwind/base.css +188 -0
- package/src/tailwind/components-input-outline.css +62 -0
- package/src/tailwind/components-resize-area.css +112 -0
- package/src/tailwind/components-step.css +161 -0
- package/src/tailwind/components.css +51 -0
- package/src/tailwind/keyframes.ts +69 -0
- package/src/tailwind/pentatrionTw.ts +147 -0
- package/src/tailwind/utilities-dialog.css +103 -0
- package/src/tailwind/utilities.css +3 -0
- package/src/types.d.ts +12 -0
- package/tailwind.config.js +18 -0
- package/tests/README.md +32 -0
- package/tests/setup.ts +1 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +23 -0
- package/vite.config.ts +24 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Dispatch,
|
|
3
|
+
MutableRefObject,
|
|
4
|
+
SetStateAction,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
|
|
11
|
+
export function useDebounce<T>(value: T, delay?: number): [T, Dispatch<SetStateAction<T>>] {
|
|
12
|
+
const [debouncedValue, setImmediateValue] = useState(value);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const timer = setTimeout(() => setImmediateValue(value), delay || 500);
|
|
16
|
+
return () => {
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
};
|
|
19
|
+
}, [value, delay]);
|
|
20
|
+
return [debouncedValue, setImmediateValue];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useRefDebounce<T>(value: T, delay?: number): MutableRefObject<T> {
|
|
24
|
+
const debouncedValueRef = useRef(value);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
debouncedValueRef.current = value;
|
|
29
|
+
}, delay || 500);
|
|
30
|
+
return () => {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
};
|
|
33
|
+
}, [value, delay]);
|
|
34
|
+
return debouncedValueRef;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useStateDebounce<T>(
|
|
38
|
+
initialValue: T,
|
|
39
|
+
delay?: number,
|
|
40
|
+
): [T, T, (initialValue: SetStateAction<T>, immediate?: boolean) => void] {
|
|
41
|
+
const [value, setImmediateValue] = useState(initialValue);
|
|
42
|
+
const [debouncedValue, setDebouncedValue] = useState(initialValue);
|
|
43
|
+
|
|
44
|
+
const setValue = useCallback((value: SetStateAction<T>, immediate = false) => {
|
|
45
|
+
setImmediateValue(value);
|
|
46
|
+
if (immediate) {
|
|
47
|
+
setDebouncedValue(value);
|
|
48
|
+
}
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
|
|
53
|
+
return () => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
};
|
|
56
|
+
}, [value, delay]);
|
|
57
|
+
|
|
58
|
+
return [value, debouncedValue, setValue];
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export function useEventCallback<Args extends unknown[], R>(
|
|
4
|
+
callback: ((...args: Args) => R) | null,
|
|
5
|
+
) {
|
|
6
|
+
const ref = useRef<typeof callback>(() => {
|
|
7
|
+
throw new Error("Cannot call an event handler while rendering.");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
ref.current = callback;
|
|
11
|
+
|
|
12
|
+
return useCallback((...args: Args) => ref.current?.(...args), [ref]);
|
|
13
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import type { RefObject } from "react";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
|
|
5
|
+
|
|
6
|
+
// MediaQueryList Event based useEventListener interface
|
|
7
|
+
function useEventListener<K extends keyof MediaQueryListEventMap>(
|
|
8
|
+
eventName: K,
|
|
9
|
+
handler: (event: MediaQueryListEventMap[K]) => void,
|
|
10
|
+
element: RefObject<MediaQueryList>,
|
|
11
|
+
options?: boolean | AddEventListenerOptions,
|
|
12
|
+
): void;
|
|
13
|
+
|
|
14
|
+
// Window Event based useEventListener interface
|
|
15
|
+
function useEventListener<K extends keyof WindowEventMap>(
|
|
16
|
+
eventName: K,
|
|
17
|
+
handler: (event: WindowEventMap[K]) => void,
|
|
18
|
+
element?: undefined,
|
|
19
|
+
options?: boolean | AddEventListenerOptions,
|
|
20
|
+
): void;
|
|
21
|
+
|
|
22
|
+
// Element Event based useEventListener interface
|
|
23
|
+
function useEventListener<
|
|
24
|
+
K extends keyof HTMLElementEventMap & keyof SVGElementEventMap,
|
|
25
|
+
T extends Element = K extends keyof HTMLElementEventMap ? HTMLDivElement : SVGElement,
|
|
26
|
+
>(
|
|
27
|
+
eventName: K,
|
|
28
|
+
handler: ((event: HTMLElementEventMap[K]) => void) | ((event: SVGElementEventMap[K]) => void),
|
|
29
|
+
element: RefObject<T>,
|
|
30
|
+
options?: boolean | AddEventListenerOptions,
|
|
31
|
+
): void;
|
|
32
|
+
|
|
33
|
+
// Document Event based useEventListener interface
|
|
34
|
+
function useEventListener<K extends keyof DocumentEventMap>(
|
|
35
|
+
eventName: K,
|
|
36
|
+
handler: (event: DocumentEventMap[K]) => void,
|
|
37
|
+
element: RefObject<Document>,
|
|
38
|
+
options?: boolean | AddEventListenerOptions,
|
|
39
|
+
): void;
|
|
40
|
+
|
|
41
|
+
function useEventListener<
|
|
42
|
+
KW extends keyof WindowEventMap,
|
|
43
|
+
KH extends keyof HTMLElementEventMap & keyof SVGElementEventMap,
|
|
44
|
+
KM extends keyof MediaQueryListEventMap,
|
|
45
|
+
T extends HTMLElement | SVGAElement | MediaQueryList = HTMLElement,
|
|
46
|
+
>(
|
|
47
|
+
eventName: KW | KH | KM,
|
|
48
|
+
handler: (
|
|
49
|
+
event:
|
|
50
|
+
| WindowEventMap[KW]
|
|
51
|
+
| HTMLElementEventMap[KH]
|
|
52
|
+
| SVGElementEventMap[KH]
|
|
53
|
+
| MediaQueryListEventMap[KM]
|
|
54
|
+
| Event,
|
|
55
|
+
) => void,
|
|
56
|
+
element?: RefObject<T>,
|
|
57
|
+
options?: boolean | AddEventListenerOptions,
|
|
58
|
+
) {
|
|
59
|
+
// Create a ref that stores handler
|
|
60
|
+
const savedHandler = useRef(handler);
|
|
61
|
+
|
|
62
|
+
useIsomorphicLayoutEffect(() => {
|
|
63
|
+
savedHandler.current = handler;
|
|
64
|
+
}, [handler]);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
// Define the listening target
|
|
68
|
+
const targetElement: T | Window = element?.current ?? window;
|
|
69
|
+
|
|
70
|
+
if (!(targetElement && targetElement.addEventListener)) return;
|
|
71
|
+
|
|
72
|
+
// Create event listener that calls handler function stored in ref
|
|
73
|
+
const listener: typeof handler = (event) => {
|
|
74
|
+
savedHandler.current(event);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
targetElement.addEventListener(eventName, listener, options);
|
|
78
|
+
|
|
79
|
+
// Remove event listener on cleanup
|
|
80
|
+
return () => {
|
|
81
|
+
targetElement.removeEventListener(eventName, listener, options);
|
|
82
|
+
};
|
|
83
|
+
}, [eventName, element, options]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { useEventListener };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { CustomFetchOptions, fetchAPI } from "../lib";
|
|
3
|
+
import { useNotifications } from "./useNotifications";
|
|
4
|
+
|
|
5
|
+
export const useFetch = () => {
|
|
6
|
+
// is it the good useNotifications ? from redux or not ??
|
|
7
|
+
const notificationManager = useNotifications();
|
|
8
|
+
|
|
9
|
+
const appFetch = useCallback(
|
|
10
|
+
async (urlObjOrString: string | URL, enhancedOptions: CustomFetchOptions = {}) => {
|
|
11
|
+
try {
|
|
12
|
+
return await fetchAPI(urlObjOrString, enhancedOptions);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
notificationManager.notifyError(err);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
[notificationManager],
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return appFetch;
|
|
21
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useIsClosing(initialValue = false, delay = 500) {
|
|
4
|
+
const [[isOpen, isClosing], setState] = useState([initialValue, false]);
|
|
5
|
+
const isMounted = useRef(false);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
isMounted.current = true;
|
|
8
|
+
return () => void (isMounted.current = false);
|
|
9
|
+
}, []);
|
|
10
|
+
const setIsOpen = useCallback(
|
|
11
|
+
(newState: boolean) => {
|
|
12
|
+
if (newState) {
|
|
13
|
+
setState([true, false]);
|
|
14
|
+
} else {
|
|
15
|
+
setState([true, true]);
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
isMounted.current && setState([false, false]);
|
|
18
|
+
}, delay);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
[delay],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return { isOpen, isClosing, setIsOpen };
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export const useIsMounted = () => {
|
|
4
|
+
const mountedRef = useRef(false);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
mountedRef.current = true;
|
|
8
|
+
return () => {
|
|
9
|
+
mountedRef.current = false;
|
|
10
|
+
};
|
|
11
|
+
}, []);
|
|
12
|
+
|
|
13
|
+
return useCallback(() => {
|
|
14
|
+
return mountedRef.current;
|
|
15
|
+
}, []);
|
|
16
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
|
|
3
|
+
import { NotificationsContext } from "../components/notification/NotificationsContext";
|
|
4
|
+
|
|
5
|
+
export const useNotifications = () => {
|
|
6
|
+
const manager = useContext(NotificationsContext);
|
|
7
|
+
|
|
8
|
+
if (!manager) {
|
|
9
|
+
throw new Error("Trying to use uninitialized NotificationsContext");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return manager;
|
|
13
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import { useEventListener } from "./useEventListener";
|
|
3
|
+
|
|
4
|
+
type EventType = "mousedown" | "mouseup" | "touchstart" | "touchend" | "focusin" | "focusout";
|
|
5
|
+
|
|
6
|
+
export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
|
|
7
|
+
ref: RefObject<T> | RefObject<T>[],
|
|
8
|
+
handler: (event: MouseEvent | TouchEvent | FocusEvent) => void,
|
|
9
|
+
eventType: EventType = "mousedown",
|
|
10
|
+
eventListenerOptions: AddEventListenerOptions = {},
|
|
11
|
+
): void {
|
|
12
|
+
useEventListener(
|
|
13
|
+
eventType,
|
|
14
|
+
(event) => {
|
|
15
|
+
const target = event.target as Node;
|
|
16
|
+
|
|
17
|
+
// Do nothing if the target is not connected element with document
|
|
18
|
+
if (!target || !target.isConnected) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isOutside = Array.isArray(ref)
|
|
23
|
+
? ref
|
|
24
|
+
.filter((r) => Boolean(r.current))
|
|
25
|
+
.every((r) => r.current && !r.current.contains(target))
|
|
26
|
+
: ref.current && !ref.current.contains(target);
|
|
27
|
+
|
|
28
|
+
if (isOutside) {
|
|
29
|
+
handler(event);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
undefined,
|
|
33
|
+
eventListenerOptions,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { afterEach, describe, it, vi } from "vitest";
|
|
3
|
+
import { useRipple } from ".";
|
|
4
|
+
import { cleanup, fireEvent, render } from "@testing-library/react";
|
|
5
|
+
|
|
6
|
+
const TestEltWithRipple = () => {
|
|
7
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
8
|
+
const ripples = useRipple(ref);
|
|
9
|
+
return (
|
|
10
|
+
<button role="button" ref={ref}>
|
|
11
|
+
{ripples}
|
|
12
|
+
</button>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
afterEach(cleanup);
|
|
17
|
+
|
|
18
|
+
describe("useRipple", () => {
|
|
19
|
+
it("add as many ripples as mouse clicks", async ({ expect }) => {
|
|
20
|
+
const { getByRole, getAllByTestId } = render(<TestEltWithRipple />);
|
|
21
|
+
const button = getByRole("button");
|
|
22
|
+
|
|
23
|
+
fireEvent.pointerDown(button);
|
|
24
|
+
fireEvent.pointerDown(button);
|
|
25
|
+
|
|
26
|
+
expect(getAllByTestId("ripple")).toHaveLength(2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("cleanup after 1 second", ({ expect }) => {
|
|
30
|
+
vi.useFakeTimers();
|
|
31
|
+
|
|
32
|
+
const { getByRole, queryAllByTestId, rerender } = render(<TestEltWithRipple />);
|
|
33
|
+
const button = getByRole("button");
|
|
34
|
+
|
|
35
|
+
fireEvent.pointerDown(button);
|
|
36
|
+
|
|
37
|
+
vi.advanceTimersByTime(999);
|
|
38
|
+
|
|
39
|
+
rerender(<TestEltWithRipple />);
|
|
40
|
+
expect(queryAllByTestId("ripple")).toHaveLength(1);
|
|
41
|
+
|
|
42
|
+
vi.advanceTimersByTime(999);
|
|
43
|
+
|
|
44
|
+
rerender(<TestEltWithRipple />);
|
|
45
|
+
|
|
46
|
+
vi.advanceTimersByTime(2);
|
|
47
|
+
|
|
48
|
+
expect(queryAllByTestId("ripple")).toHaveLength(0);
|
|
49
|
+
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { useDebounce } from ".";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This hook accepts a ref to any element and adds a click event handler that creates ripples when click
|
|
6
|
+
*/
|
|
7
|
+
export const useRipple = <T extends HTMLElement>(ref: React.RefObject<T>) => {
|
|
8
|
+
//ripples are just styles that we attach to span elements
|
|
9
|
+
const [ripples, setRipples] = useState<React.CSSProperties[]>([]);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
//add a click handler for the ripple
|
|
13
|
+
const handlePointerDown = (e: MouseEvent) => {
|
|
14
|
+
if (ref.current) {
|
|
15
|
+
const elem = ref.current;
|
|
16
|
+
|
|
17
|
+
//calculate the position and dimensions of the ripple.
|
|
18
|
+
//based on click position and button dimensions
|
|
19
|
+
const rect = elem.getBoundingClientRect();
|
|
20
|
+
const left = (e.clientX ?? 0) - rect.left;
|
|
21
|
+
const top = (e.clientY ?? 0) - rect.top;
|
|
22
|
+
const height = elem.clientHeight;
|
|
23
|
+
const width = elem.clientWidth;
|
|
24
|
+
const diameter = Math.max(width, height);
|
|
25
|
+
setRipples((r) => [
|
|
26
|
+
...r,
|
|
27
|
+
{
|
|
28
|
+
top: top - diameter / 2,
|
|
29
|
+
left: left - diameter / 2,
|
|
30
|
+
height: Math.max(width, height),
|
|
31
|
+
width: Math.max(width, height),
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
//check if there's a ref
|
|
38
|
+
if (ref.current) {
|
|
39
|
+
const elem = ref.current;
|
|
40
|
+
|
|
41
|
+
//add an event listener to the button
|
|
42
|
+
elem.addEventListener("pointerdown", handlePointerDown);
|
|
43
|
+
|
|
44
|
+
//clean up when the component is unmounted
|
|
45
|
+
return () => {
|
|
46
|
+
elem.removeEventListener("pointerdown", handlePointerDown);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}, [ref]);
|
|
50
|
+
|
|
51
|
+
//add a debounce so that if the user doesn't click after 1s, we remove all the ripples
|
|
52
|
+
const [_debounced] = useDebounce(ripples, 1000);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (_debounced.length) {
|
|
55
|
+
setRipples([]);
|
|
56
|
+
}
|
|
57
|
+
}, [_debounced]);
|
|
58
|
+
|
|
59
|
+
//map through the ripples and return span elements.
|
|
60
|
+
//this will be added to the button component later
|
|
61
|
+
return ripples?.map((style, i) => {
|
|
62
|
+
return (
|
|
63
|
+
<span
|
|
64
|
+
key={i}
|
|
65
|
+
data-testid="ripple"
|
|
66
|
+
className="animate-ripple"
|
|
67
|
+
style={{
|
|
68
|
+
...style,
|
|
69
|
+
//should be absolutely positioned
|
|
70
|
+
position: "absolute",
|
|
71
|
+
backgroundColor: "#FFFFFF",
|
|
72
|
+
opacity: "25%",
|
|
73
|
+
transform: "scale(0)",
|
|
74
|
+
borderRadius: "50%",
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
};
|
package/src/index.css
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@import "tailwindcss/base";
|
|
2
|
+
@import "./tailwind/base.css" layer(base);
|
|
3
|
+
|
|
4
|
+
@import "tailwindcss/components";
|
|
5
|
+
@import "./tailwind/components.css" layer(components);
|
|
6
|
+
@import "./tailwind/components-input-outline.css" layer(components);
|
|
7
|
+
@import "./tailwind/components-resize-area.css" layer(components);
|
|
8
|
+
@import "./tailwind/components-step.css" layer(components);
|
|
9
|
+
|
|
10
|
+
@import "tailwindcss/utilities";
|
|
11
|
+
@import "./tailwind/utilities.css" layer(utilities);
|
|
12
|
+
@import "./tailwind/utilities-dialog.css" layer(utilities);
|
|
13
|
+
|
|
14
|
+
@import "pentatrion-fonts/fontello-lonlat";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// issue: https://github.com/floating-ui/floating-ui/issues/2507
|
|
2
|
+
import type {} from "@floating-ui/react-dom";
|
|
3
|
+
|
|
4
|
+
export type { ThemeColor } from "./types.d";
|
|
5
|
+
|
|
6
|
+
export * from "./components/table";
|
|
7
|
+
export * from "./components/card";
|
|
8
|
+
export { default as Code } from "./components/code/Code";
|
|
9
|
+
export { default as Tabs } from "./components/tabs/Tabs";
|
|
10
|
+
export { default as Flash } from "./components/flash/Flash";
|
|
11
|
+
export { default as Scroll } from "./components/scroll/Scroll";
|
|
12
|
+
export { default as Loader } from "./components/loader/Loader";
|
|
13
|
+
export { default as Portal } from "./components/portal/Portal";
|
|
14
|
+
export { default as Badge } from "./components/badge/Badge";
|
|
15
|
+
export { default as Highlight } from "./components/highlight/Highlight";
|
|
16
|
+
export { default as InputField } from "./components/input-field/InputField";
|
|
17
|
+
export { default as ResizeArea } from "./components/resize-area/ResizeArea";
|
|
18
|
+
export { default as Snack } from "./components/snack/Snack";
|
|
19
|
+
export { default as Href } from "./components/href/Href";
|
|
20
|
+
|
|
21
|
+
export * from "./components/autocomplete";
|
|
22
|
+
export * from "./components/button";
|
|
23
|
+
export * from "./components/context-menu";
|
|
24
|
+
export * from "./components/dialog";
|
|
25
|
+
export * from "./components/dropdown-menu";
|
|
26
|
+
export * from "./components/dropdown-menu-nested";
|
|
27
|
+
export * from "./components/input";
|
|
28
|
+
export * from "./components/modal";
|
|
29
|
+
export * from "./components/notification";
|
|
30
|
+
export * from "./components/popover";
|
|
31
|
+
export * from "./components/select";
|
|
32
|
+
export * from "./components/steps";
|
|
33
|
+
export * from "./components/tooltip";
|
|
34
|
+
export * from "./components/sortable";
|
|
35
|
+
|
|
36
|
+
export * from "./lib";
|
|
37
|
+
export * from "./hooks";
|
|
38
|
+
|
|
39
|
+
export { default as pentatrionTw } from "./tailwind/pentatrionTw";
|
package/src/lib/error.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MessageOptions } from "~/types.d";
|
|
2
|
+
|
|
3
|
+
export function isErrorLike(err: any): err is Error {
|
|
4
|
+
if (err instanceof Error) {
|
|
5
|
+
return true;
|
|
6
|
+
} else if (err.name !== undefined && err.message !== undefined) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseError(err: any): [string, MessageOptions] | null {
|
|
14
|
+
if (isErrorLike(err)) {
|
|
15
|
+
return [err.message || "An error occured", { color: "red" }];
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, describe, test, vi } from "vitest";
|
|
2
|
+
import { fetchAPI } from "./fetch";
|
|
3
|
+
|
|
4
|
+
function get200Response() {
|
|
5
|
+
return {
|
|
6
|
+
status: 200,
|
|
7
|
+
ok: true,
|
|
8
|
+
headers: new Headers(),
|
|
9
|
+
text() {
|
|
10
|
+
return Promise.resolve("hello world");
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("fetchAPI", () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.unstubAllGlobals();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("accept relative url", async ({ expect }): Promise<void> => {
|
|
21
|
+
const mock = vi.fn().mockResolvedValueOnce(get200Response());
|
|
22
|
+
|
|
23
|
+
vi.stubGlobal("fetch", mock);
|
|
24
|
+
|
|
25
|
+
const result = await fetchAPI("/hello");
|
|
26
|
+
|
|
27
|
+
expect(mock).toBeCalledTimes(1);
|
|
28
|
+
expect(mock).toHaveBeenCalledWith("http://localhost:3000/hello", {});
|
|
29
|
+
expect(result).toBe("hello world");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("parse urlParams", async ({ expect }): Promise<void> => {
|
|
33
|
+
const mock = vi.fn().mockResolvedValue(get200Response());
|
|
34
|
+
|
|
35
|
+
vi.stubGlobal("fetch", mock);
|
|
36
|
+
|
|
37
|
+
await fetchAPI("/user/{name}/{id}", {
|
|
38
|
+
urlParams: {
|
|
39
|
+
name: "dupond",
|
|
40
|
+
id: 3,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(mock).toHaveBeenCalledWith("http://localhost:3000/user/dupond/3", {});
|
|
45
|
+
|
|
46
|
+
await fetchAPI("/user/{name}/{id}", {
|
|
47
|
+
urlParams: {
|
|
48
|
+
name: "dupond",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(mock).toHaveBeenCalledWith("http://localhost:3000/user/dupond/%7Bid%7D", {});
|
|
53
|
+
|
|
54
|
+
await fetchAPI("/user/{name}/{id}", {
|
|
55
|
+
urlParams: {
|
|
56
|
+
name: "dupond",
|
|
57
|
+
id: 3,
|
|
58
|
+
lastname: "dupont",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(mock).toHaveBeenCalledWith("http://localhost:3000/user/dupond/3", {});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("parse query", async ({ expect }): Promise<void> => {
|
|
66
|
+
const mock = vi.fn().mockResolvedValue(get200Response());
|
|
67
|
+
|
|
68
|
+
vi.stubGlobal("fetch", mock);
|
|
69
|
+
|
|
70
|
+
await fetchAPI("/user", {
|
|
71
|
+
query: {
|
|
72
|
+
name: "dupond",
|
|
73
|
+
id: 3,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
expect(mock).toHaveBeenCalledWith("http://localhost:3000/user?name=dupond&id=3", {});
|
|
77
|
+
|
|
78
|
+
await fetchAPI("/user?page=1#hash", {
|
|
79
|
+
query: {
|
|
80
|
+
name: "dupond",
|
|
81
|
+
id: 3,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
expect(mock).toHaveBeenCalledWith(
|
|
85
|
+
"http://localhost:3000/user?page=1&name=dupond&id=3#hash",
|
|
86
|
+
{},
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("stringify json object", async ({ expect }): Promise<void> => {
|
|
91
|
+
const mock = vi.fn().mockResolvedValue(get200Response());
|
|
92
|
+
|
|
93
|
+
vi.stubGlobal("fetch", mock);
|
|
94
|
+
await fetchAPI("/user", {
|
|
95
|
+
body: {
|
|
96
|
+
name: "dupond",
|
|
97
|
+
id: 3,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
expect(mock).toHaveBeenCalledWith("http://localhost:3000/user", {
|
|
101
|
+
body: '{"name":"dupond","id":3}',
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|