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,41 @@
|
|
|
1
|
+
import { HTMLProps, cloneElement, forwardRef, isValidElement } from "react";
|
|
2
|
+
import { useMergeRefs } from "@floating-ui/react";
|
|
3
|
+
import { Button } from "../button";
|
|
4
|
+
import { useModalContext } from ".";
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLProps<HTMLElement> {
|
|
7
|
+
asChild?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ModalTrigger = forwardRef<HTMLElement, Props>(
|
|
11
|
+
({ children, asChild = false, ...props }, propRef) => {
|
|
12
|
+
const context = useModalContext();
|
|
13
|
+
const childrenRef = (children as any).ref;
|
|
14
|
+
|
|
15
|
+
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);
|
|
16
|
+
|
|
17
|
+
if (asChild && isValidElement(children)) {
|
|
18
|
+
return cloneElement(
|
|
19
|
+
children,
|
|
20
|
+
context.getReferenceProps({
|
|
21
|
+
ref,
|
|
22
|
+
...props,
|
|
23
|
+
...children.props,
|
|
24
|
+
"data-state": context.open ? "open" : "closed",
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Button
|
|
31
|
+
ref={ref}
|
|
32
|
+
data-state={context.open ? "open" : "closed"}
|
|
33
|
+
{...context.getReferenceProps(props)}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</Button>
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export default ModalTrigger;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as Modal } from "./Modal";
|
|
2
|
+
export { default as ModalContent } from "./ModalContent";
|
|
3
|
+
export { default as ModalTrigger } from "./ModalTrigger";
|
|
4
|
+
export { default as useModal } from "./useModal";
|
|
5
|
+
export { default as useModalContext, ModalContext } from "./useModalContext";
|
|
6
|
+
export { ModalHeader, ModalDescription, ModalFooter } from "./Components";
|
|
7
|
+
export type * from "./interface";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction, useMemo, useState } from "react";
|
|
2
|
+
import { ModalOptions } from "./interface";
|
|
3
|
+
import { useClick, useDismiss, useFloating, useInteractions, useRole } from "@floating-ui/react";
|
|
4
|
+
|
|
5
|
+
export default function useModal({
|
|
6
|
+
initialOpen = false,
|
|
7
|
+
open: controlledOpen,
|
|
8
|
+
onOpen: setControlledOpen,
|
|
9
|
+
color = "default",
|
|
10
|
+
}: ModalOptions = {}) {
|
|
11
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
|
|
12
|
+
const [labelId, setLabelId] = useState<string | undefined>();
|
|
13
|
+
const [descriptionId, setDescriptionId] = useState<string | undefined>();
|
|
14
|
+
|
|
15
|
+
const isUncontrolled = controlledOpen === null || controlledOpen === undefined;
|
|
16
|
+
const open = isUncontrolled ? uncontrolledOpen : controlledOpen;
|
|
17
|
+
const setOpen = isUncontrolled
|
|
18
|
+
? setUncontrolledOpen
|
|
19
|
+
: (setControlledOpen as Dispatch<SetStateAction<boolean>>);
|
|
20
|
+
|
|
21
|
+
const data = useFloating({
|
|
22
|
+
open,
|
|
23
|
+
onOpenChange: setOpen,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const { context } = data;
|
|
27
|
+
|
|
28
|
+
const click = useClick(context);
|
|
29
|
+
const dismiss = useDismiss(context);
|
|
30
|
+
const role = useRole(context);
|
|
31
|
+
|
|
32
|
+
const interactions = useInteractions([click, dismiss, role]);
|
|
33
|
+
|
|
34
|
+
return useMemo(
|
|
35
|
+
() => ({
|
|
36
|
+
open,
|
|
37
|
+
setOpen,
|
|
38
|
+
...interactions,
|
|
39
|
+
...data,
|
|
40
|
+
color,
|
|
41
|
+
labelId,
|
|
42
|
+
descriptionId,
|
|
43
|
+
setLabelId,
|
|
44
|
+
setDescriptionId,
|
|
45
|
+
}),
|
|
46
|
+
[open, setOpen, interactions, data, color, labelId, descriptionId],
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import useModal from "./useModal";
|
|
3
|
+
|
|
4
|
+
type ContextType = ReturnType<typeof useModal> | null;
|
|
5
|
+
|
|
6
|
+
export const ModalContext = createContext<ContextType>(null);
|
|
7
|
+
|
|
8
|
+
export default function useModalContext() {
|
|
9
|
+
const context = useContext(ModalContext);
|
|
10
|
+
|
|
11
|
+
if (context === null) {
|
|
12
|
+
throw new Error("Modal components must be wrapped in <Modal />");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return context;
|
|
16
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Meta, ReactRenderer } from "@storybook/react";
|
|
2
|
+
import { PartialStoryFn } from "@storybook/types";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import NotificationsProvider from "./NotificationsProvider";
|
|
5
|
+
import { useFetch, useNotifications } from "~/hooks";
|
|
6
|
+
import { Button } from "../button";
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: "Components/NotificationsProvider",
|
|
10
|
+
component: NotificationsProvider,
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<NotificationsProvider>
|
|
14
|
+
<Story />
|
|
15
|
+
</NotificationsProvider>
|
|
16
|
+
),
|
|
17
|
+
] as ((story: PartialStoryFn<ReactRenderer, any>) => JSX.Element)[],
|
|
18
|
+
} satisfies Meta<typeof NotificationsProvider>;
|
|
19
|
+
export default meta;
|
|
20
|
+
|
|
21
|
+
export const Context = () => {
|
|
22
|
+
const counter = useRef(0);
|
|
23
|
+
const { addNotification, notifyError } = useNotifications();
|
|
24
|
+
const fetch = useFetch();
|
|
25
|
+
function handleClick() {
|
|
26
|
+
counter.current += 1;
|
|
27
|
+
addNotification(`Notification num. ${counter.current}`, {
|
|
28
|
+
expiration: 5000,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function throwError() {
|
|
33
|
+
try {
|
|
34
|
+
throw new Error("Custom error message");
|
|
35
|
+
} catch (e) {
|
|
36
|
+
notifyError(e);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function handle404Error() {
|
|
41
|
+
fetch("/throw-404-error");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="grid grid-cols-1 gap-8">
|
|
46
|
+
<Button onClick={handleClick}>Show info notification</Button>
|
|
47
|
+
<Button onClick={throwError}>Throw custom error</Button>
|
|
48
|
+
<Button onClick={handle404Error}>Throw 404 fetch error</Button>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction, createContext } from "react";
|
|
2
|
+
import { parseError } from "~/lib";
|
|
3
|
+
import { Message, MessageOptions } from "~/types.d";
|
|
4
|
+
|
|
5
|
+
export function createNotificationsManager(setNotifications: Dispatch<SetStateAction<Message[]>>) {
|
|
6
|
+
let idx = 1;
|
|
7
|
+
const timeoutIds = new Map<string, any>();
|
|
8
|
+
|
|
9
|
+
const removeNotification = (id: string) => {
|
|
10
|
+
const timeoutId = timeoutIds.get(id);
|
|
11
|
+
if (!timeoutId) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
clearTimeout(timeoutId);
|
|
15
|
+
timeoutIds.delete(id);
|
|
16
|
+
|
|
17
|
+
return setNotifications((oldNotifications) => {
|
|
18
|
+
return oldNotifications.filter(({ id: otherId }) => id !== otherId);
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const addNotification = (
|
|
23
|
+
content = "",
|
|
24
|
+
{
|
|
25
|
+
expiration = 5000,
|
|
26
|
+
color = "yellow",
|
|
27
|
+
canClose = true,
|
|
28
|
+
withLoader = false,
|
|
29
|
+
}: MessageOptions = {},
|
|
30
|
+
) => {
|
|
31
|
+
const id = (idx++).toString();
|
|
32
|
+
if (timeoutIds.has(id)) {
|
|
33
|
+
throw new Error("notification already exists");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setNotifications((oldNotifications) => {
|
|
37
|
+
const newNotification: Message = {
|
|
38
|
+
id,
|
|
39
|
+
content,
|
|
40
|
+
expiration,
|
|
41
|
+
color,
|
|
42
|
+
canClose,
|
|
43
|
+
withLoader,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return [newNotification, ...oldNotifications];
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
timeoutIds.set(
|
|
50
|
+
id,
|
|
51
|
+
expiration === -1 ? -1 : setTimeout(() => removeNotification(id), expiration),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return id;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const notifyError = (err: unknown) => {
|
|
58
|
+
const errorMessage = parseError(err);
|
|
59
|
+
if (errorMessage) {
|
|
60
|
+
addNotification(...errorMessage);
|
|
61
|
+
} else {
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
addNotification,
|
|
68
|
+
removeNotification,
|
|
69
|
+
notifyError,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type NotificationsManager = ReturnType<typeof createNotificationsManager>;
|
|
74
|
+
|
|
75
|
+
export const NotificationsContext = createContext<NotificationsManager>(
|
|
76
|
+
null as unknown as NotificationsManager,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
export type { Message };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ReactNode, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import {
|
|
4
|
+
NotificationsContext,
|
|
5
|
+
createNotificationsManager,
|
|
6
|
+
type NotificationsManager,
|
|
7
|
+
} from "./NotificationsContext";
|
|
8
|
+
import Snack from "../snack/Snack";
|
|
9
|
+
import { Message } from "~/types.d";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function NotificationsProvider({ children }: Props) {
|
|
16
|
+
const [notifications, setNotifications] = useState<Message[]>([]);
|
|
17
|
+
const container = useRef<HTMLDivElement>(null!);
|
|
18
|
+
if (!container.current) {
|
|
19
|
+
container.current = document.createElement("div");
|
|
20
|
+
container.current.id = Math.floor(Math.random() * 100000).toString();
|
|
21
|
+
/* .ll-snack-bar */
|
|
22
|
+
container.current.classList.add("fixed", "bottom-0", "left-0", "right-0", "z-notification");
|
|
23
|
+
document.body.append(container.current);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
return () => {
|
|
28
|
+
container.current && container.current.remove();
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const managerRef = useRef<NotificationsManager>();
|
|
33
|
+
|
|
34
|
+
if (!managerRef.current) {
|
|
35
|
+
managerRef.current = createNotificationsManager(setNotifications);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const manager = managerRef.current;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<NotificationsContext.Provider value={manager}>{children}</NotificationsContext.Provider>
|
|
43
|
+
{createPortal(
|
|
44
|
+
/* snack-bar-inner */
|
|
45
|
+
<div className="mb-4 mx-4 flex flex-col gap-4 items-center">
|
|
46
|
+
{notifications.map((props) => (
|
|
47
|
+
<Snack
|
|
48
|
+
{...props}
|
|
49
|
+
key={props.id}
|
|
50
|
+
onRemove={() => manager.removeNotification(props.id)}
|
|
51
|
+
/>
|
|
52
|
+
))}
|
|
53
|
+
</div>,
|
|
54
|
+
container.current,
|
|
55
|
+
)}
|
|
56
|
+
</>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ComponentProps } from "react";
|
|
2
|
+
import { usePopoverContext } from ".";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { Button } from "../button";
|
|
5
|
+
|
|
6
|
+
interface PopoverHeaderProps extends ComponentProps<"h2"> {
|
|
7
|
+
closeButton?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export function PopoverHeader({
|
|
10
|
+
children,
|
|
11
|
+
closeButton = true,
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: PopoverHeaderProps) {
|
|
15
|
+
const { setOpen } = usePopoverContext();
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
{closeButton && (
|
|
19
|
+
<div className="p-1 float-right">
|
|
20
|
+
<Button icon variant="text" color="gray" onClick={() => setOpen(false)}>
|
|
21
|
+
<i className="fe-cancel"></i>
|
|
22
|
+
</Button>
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
<header className={clsx("flex items-center px-2 pt-2", className)} {...props}>
|
|
26
|
+
<h4>{children}</h4>
|
|
27
|
+
</header>
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
export function PopoverDescription({ children, className, ...props }: ComponentProps<"div">) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={clsx("p-2", className)} {...props}>
|
|
34
|
+
{children}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function PopoverFooter({ children, className, ...props }: ComponentProps<"footer">) {
|
|
40
|
+
return (
|
|
41
|
+
<footer className={clsx("relative z-10 px-2 pb-2", className)} {...props}>
|
|
42
|
+
{children}
|
|
43
|
+
</footer>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Popover, PopoverContent, PopoverTrigger, PopoverHeader, PopoverDescription } from ".";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/Popover",
|
|
6
|
+
component: Popover,
|
|
7
|
+
} satisfies Meta<typeof Popover>;
|
|
8
|
+
export default meta;
|
|
9
|
+
|
|
10
|
+
type Story = StoryObj<typeof meta>;
|
|
11
|
+
|
|
12
|
+
export const Basic: Story = {
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
14
|
+
render: ({ children, ...args }) => (
|
|
15
|
+
<>
|
|
16
|
+
<p>
|
|
17
|
+
Contrairement au tooltip il doit s'activer par un clic. le Popover est masqué si
|
|
18
|
+
l'utilisateur clique autre part
|
|
19
|
+
</p>
|
|
20
|
+
<Popover {...args}>
|
|
21
|
+
<PopoverTrigger>My trigger</PopoverTrigger>
|
|
22
|
+
<PopoverContent style={{ width: "400px" }}>
|
|
23
|
+
<PopoverHeader>Heading</PopoverHeader>
|
|
24
|
+
<PopoverDescription>description</PopoverDescription>
|
|
25
|
+
</PopoverContent>
|
|
26
|
+
</Popover>
|
|
27
|
+
</>
|
|
28
|
+
),
|
|
29
|
+
args: {
|
|
30
|
+
initialOpen: false,
|
|
31
|
+
placement: "bottom",
|
|
32
|
+
color: "yellow",
|
|
33
|
+
modal: false,
|
|
34
|
+
children: "",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Context = () => (
|
|
39
|
+
<Popover color="yellow">
|
|
40
|
+
<PopoverTrigger>My trigger</PopoverTrigger>
|
|
41
|
+
<PopoverContent style={{ width: "400px" }}>
|
|
42
|
+
<PopoverHeader>Heading</PopoverHeader>
|
|
43
|
+
<PopoverDescription>description</PopoverDescription>
|
|
44
|
+
</PopoverContent>
|
|
45
|
+
</Popover>
|
|
46
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, it } from "vitest";
|
|
3
|
+
import { Popover, PopoverContent, PopoverDescription, PopoverTrigger } from ".";
|
|
4
|
+
|
|
5
|
+
afterEach(cleanup);
|
|
6
|
+
|
|
7
|
+
describe("<Popover />", () => {
|
|
8
|
+
it("open popup on click", async () => {
|
|
9
|
+
const { getByText, getByRole } = render(
|
|
10
|
+
<Popover color="yellow">
|
|
11
|
+
<PopoverTrigger>My trigger</PopoverTrigger>
|
|
12
|
+
<PopoverContent>
|
|
13
|
+
<PopoverDescription>Content</PopoverDescription>
|
|
14
|
+
</PopoverContent>
|
|
15
|
+
</Popover>,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
fireEvent.click(getByRole("button"));
|
|
19
|
+
getByText("Content");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { PopoverOptions } from "./interface";
|
|
3
|
+
import { PopoverContext, usePopover } from ".";
|
|
4
|
+
|
|
5
|
+
interface Props extends PopoverOptions {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function Popover({ children, ...options }: Props) {
|
|
10
|
+
const popover = usePopover(options);
|
|
11
|
+
return <PopoverContext.Provider value={popover}>{children}</PopoverContext.Provider>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ComponentProps, forwardRef } from "react";
|
|
2
|
+
import { usePopoverContext } from ".";
|
|
3
|
+
import { FloatingFocusManager, FloatingPortal, useMergeRefs } from "@floating-ui/react";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import { computeArrowStyle } from "../dialog/util";
|
|
6
|
+
import { Dialog } from "../dialog";
|
|
7
|
+
|
|
8
|
+
const PopoverContent = forwardRef<HTMLDivElement, ComponentProps<"div">>(
|
|
9
|
+
({ style, children, className, ...props }, propRef) => {
|
|
10
|
+
const context = usePopoverContext();
|
|
11
|
+
const floatingContext = context.context;
|
|
12
|
+
|
|
13
|
+
const ref = useMergeRefs([context.refs.setFloating, propRef]);
|
|
14
|
+
if (!context.open) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return (
|
|
18
|
+
<FloatingPortal>
|
|
19
|
+
<FloatingFocusManager context={floatingContext} modal={context.modal}>
|
|
20
|
+
<div
|
|
21
|
+
ref={ref}
|
|
22
|
+
style={{ ...context.floatingStyles, ...style }}
|
|
23
|
+
aria-labelledby={context.labelId}
|
|
24
|
+
aria-describedby={context.descriptionId}
|
|
25
|
+
{...context.getFloatingProps(props)}
|
|
26
|
+
>
|
|
27
|
+
<Dialog
|
|
28
|
+
placement={context.placement}
|
|
29
|
+
color={context.color}
|
|
30
|
+
className={clsx("animate-fade-in-list", className)}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
<div
|
|
34
|
+
ref={context.arrowRef}
|
|
35
|
+
style={computeArrowStyle(context)}
|
|
36
|
+
className="p8n-arrow"
|
|
37
|
+
></div>
|
|
38
|
+
</Dialog>
|
|
39
|
+
</div>
|
|
40
|
+
</FloatingFocusManager>
|
|
41
|
+
</FloatingPortal>
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export default PopoverContent;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { HTMLProps, cloneElement, forwardRef, isValidElement } from "react";
|
|
2
|
+
import { usePopoverContext } from ".";
|
|
3
|
+
import { useMergeRefs } from "@floating-ui/react";
|
|
4
|
+
import { Button } from "../button";
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLProps<HTMLElement> {
|
|
7
|
+
asChild?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const PopoverTrigger = forwardRef<HTMLElement, Props>(
|
|
11
|
+
({ children, asChild = false, ...props }, propRef) => {
|
|
12
|
+
const context = usePopoverContext();
|
|
13
|
+
const childrenRef = (children as any).ref;
|
|
14
|
+
|
|
15
|
+
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);
|
|
16
|
+
|
|
17
|
+
if (asChild && isValidElement(children)) {
|
|
18
|
+
return cloneElement(
|
|
19
|
+
children,
|
|
20
|
+
context.getReferenceProps({
|
|
21
|
+
ref,
|
|
22
|
+
...props,
|
|
23
|
+
...children.props,
|
|
24
|
+
"data-state": context.open ? "open" : "closed",
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Button
|
|
31
|
+
variant="outlined"
|
|
32
|
+
ref={ref}
|
|
33
|
+
data-state={context.open ? "open" : "closed"}
|
|
34
|
+
{...context.getReferenceProps(props)}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</Button>
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export default PopoverTrigger;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as Popover } from "./Popover";
|
|
2
|
+
export { default as PopoverContent } from "./PopoverContent";
|
|
3
|
+
export { default as PopoverTrigger } from "./PopoverTrigger";
|
|
4
|
+
export { default as usePopover } from "./usePopover";
|
|
5
|
+
export { default as usePopoverContext, PopoverContext } from "./usePopoverContext";
|
|
6
|
+
export { PopoverHeader, PopoverDescription, PopoverFooter } from "./Components";
|
|
7
|
+
export type * from "./interface";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Placement } from "@floating-ui/react";
|
|
2
|
+
import { ThemeColor } from "~/types.d";
|
|
3
|
+
|
|
4
|
+
export interface PopoverOptions {
|
|
5
|
+
initialOpen?: boolean;
|
|
6
|
+
placement?: Placement;
|
|
7
|
+
open?: boolean;
|
|
8
|
+
onOpen?: (open: boolean) => void;
|
|
9
|
+
color?: ThemeColor;
|
|
10
|
+
modal?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { ThemeColor };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useMemo, useRef, useState } from "react";
|
|
2
|
+
import { PopoverOptions } from "./interface";
|
|
3
|
+
import {
|
|
4
|
+
arrow,
|
|
5
|
+
autoUpdate,
|
|
6
|
+
flip,
|
|
7
|
+
offset,
|
|
8
|
+
shift,
|
|
9
|
+
useClick,
|
|
10
|
+
useDismiss,
|
|
11
|
+
useFloating,
|
|
12
|
+
useInteractions,
|
|
13
|
+
useRole,
|
|
14
|
+
} from "@floating-ui/react";
|
|
15
|
+
|
|
16
|
+
const arrowWidth = 12;
|
|
17
|
+
|
|
18
|
+
export default function usePopover({
|
|
19
|
+
initialOpen = false,
|
|
20
|
+
placement = "bottom",
|
|
21
|
+
open: controlledOpen,
|
|
22
|
+
onOpen: setControlledOpen,
|
|
23
|
+
color,
|
|
24
|
+
modal = false,
|
|
25
|
+
}: PopoverOptions) {
|
|
26
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
|
|
27
|
+
const [labelId, setLabelId] = useState<string | undefined>();
|
|
28
|
+
const [descriptionId, setDescriptionId] = useState<string | undefined>();
|
|
29
|
+
|
|
30
|
+
const isUncontrolled = controlledOpen === null || controlledOpen === undefined;
|
|
31
|
+
const open = isUncontrolled ? uncontrolledOpen : controlledOpen;
|
|
32
|
+
const setOpen = isUncontrolled ? setUncontrolledOpen : setControlledOpen!;
|
|
33
|
+
|
|
34
|
+
const arrowRef = useRef<HTMLDivElement>(null);
|
|
35
|
+
const offsetVal = arrowWidth / 2;
|
|
36
|
+
|
|
37
|
+
const data = useFloating({
|
|
38
|
+
placement,
|
|
39
|
+
open,
|
|
40
|
+
onOpenChange: setOpen,
|
|
41
|
+
whileElementsMounted: autoUpdate,
|
|
42
|
+
middleware: [
|
|
43
|
+
offset(offsetVal),
|
|
44
|
+
flip(),
|
|
45
|
+
shift({ padding: 5 }),
|
|
46
|
+
arrow({
|
|
47
|
+
element: arrowRef,
|
|
48
|
+
padding: 12,
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const { context } = data;
|
|
54
|
+
|
|
55
|
+
const click = useClick(context, {
|
|
56
|
+
// TODO verify if use case to disable it
|
|
57
|
+
// enabled: isUncontrolled,
|
|
58
|
+
});
|
|
59
|
+
const dismiss = useDismiss(context);
|
|
60
|
+
const role = useRole(context);
|
|
61
|
+
|
|
62
|
+
const interactions = useInteractions([click, dismiss, role]);
|
|
63
|
+
|
|
64
|
+
return useMemo(
|
|
65
|
+
() => ({
|
|
66
|
+
open,
|
|
67
|
+
setOpen,
|
|
68
|
+
...interactions,
|
|
69
|
+
...data,
|
|
70
|
+
arrowRef,
|
|
71
|
+
color,
|
|
72
|
+
modal,
|
|
73
|
+
labelId,
|
|
74
|
+
descriptionId,
|
|
75
|
+
setLabelId,
|
|
76
|
+
setDescriptionId,
|
|
77
|
+
}),
|
|
78
|
+
[open, setOpen, interactions, data, color, modal, labelId, descriptionId],
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import usePopover from "./usePopover";
|
|
3
|
+
|
|
4
|
+
type ContextType = ReturnType<typeof usePopover> | null;
|
|
5
|
+
|
|
6
|
+
export const PopoverContext = createContext<ContextType>(null);
|
|
7
|
+
|
|
8
|
+
export default function usePopoverContext() {
|
|
9
|
+
const context = useContext(PopoverContext);
|
|
10
|
+
|
|
11
|
+
if (context === null) {
|
|
12
|
+
throw new Error("Popover components must be wrapped in <Popover />");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return context;
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import ReactDOM from "react-dom";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const Portal = ({ children }: Props) => {
|
|
9
|
+
return ReactDOM.createPortal(children, document.body);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default Portal;
|