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,42 @@
|
|
|
1
|
+
import { HTMLProps, cloneElement, forwardRef, isValidElement } from "react";
|
|
2
|
+
import { useDropdownMenuContext } 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 DropdownMenuTrigger = forwardRef<HTMLElement, Props>(
|
|
11
|
+
({ children, asChild = false, ...props }, propRef) => {
|
|
12
|
+
const context = useDropdownMenuContext();
|
|
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 DropdownMenuTrigger;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as DropdownMenu } from "./DropdownMenu";
|
|
2
|
+
export { default as DropdownMenuContent } from "./DropdownMenuContent";
|
|
3
|
+
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger";
|
|
4
|
+
export { default as useDropdownMenu } from "./useDropdownMenu";
|
|
5
|
+
export { default as useDropdownMenuContext, DropdownMenuContext } from "./useDropdownMenuContext";
|
|
6
|
+
export { default as DropdownMenuItem } from "./DropdownMenuItem";
|
|
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 DropdownMenuOptions {
|
|
5
|
+
initialOpen?: boolean;
|
|
6
|
+
placement?: Placement;
|
|
7
|
+
open?: boolean;
|
|
8
|
+
onOpen?: (open: boolean) => void;
|
|
9
|
+
color?: ThemeColor | "default";
|
|
10
|
+
modal?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { ThemeColor };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { DropdownMenuOptions } from "./interface";
|
|
3
|
+
import {
|
|
4
|
+
autoUpdate,
|
|
5
|
+
flip,
|
|
6
|
+
offset,
|
|
7
|
+
shift,
|
|
8
|
+
useClick,
|
|
9
|
+
useDismiss,
|
|
10
|
+
useFloating,
|
|
11
|
+
useInteractions,
|
|
12
|
+
useListNavigation,
|
|
13
|
+
useRole,
|
|
14
|
+
useTypeahead,
|
|
15
|
+
} from "@floating-ui/react";
|
|
16
|
+
|
|
17
|
+
const arrowWidth = 12;
|
|
18
|
+
|
|
19
|
+
export default function useDropdownMenu({
|
|
20
|
+
initialOpen = false,
|
|
21
|
+
placement = "bottom",
|
|
22
|
+
open: controlledOpen,
|
|
23
|
+
onOpen: setControlledOpen,
|
|
24
|
+
color = "default",
|
|
25
|
+
modal = false,
|
|
26
|
+
}: DropdownMenuOptions) {
|
|
27
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
|
|
28
|
+
|
|
29
|
+
const isUncontrolled = controlledOpen === null || controlledOpen === undefined;
|
|
30
|
+
const open = isUncontrolled ? uncontrolledOpen : controlledOpen;
|
|
31
|
+
const setOpen = isUncontrolled ? setUncontrolledOpen : setControlledOpen!;
|
|
32
|
+
|
|
33
|
+
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
|
34
|
+
|
|
35
|
+
const elementsRef = useRef<Array<HTMLElement | null>>([]);
|
|
36
|
+
const labelsRef = useRef<Array<string | null>>([]);
|
|
37
|
+
|
|
38
|
+
const offsetVal = 2 + arrowWidth / 2;
|
|
39
|
+
|
|
40
|
+
const data = useFloating({
|
|
41
|
+
placement,
|
|
42
|
+
open,
|
|
43
|
+
onOpenChange: setOpen,
|
|
44
|
+
whileElementsMounted: autoUpdate,
|
|
45
|
+
middleware: [
|
|
46
|
+
offset(offsetVal),
|
|
47
|
+
flip({
|
|
48
|
+
// crossAxis: placement.includes('-'),
|
|
49
|
+
// fallbackAxisSideDirection: "end",
|
|
50
|
+
}),
|
|
51
|
+
shift({ padding: 5 }),
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const { context } = data;
|
|
56
|
+
|
|
57
|
+
const click = useClick(context, {
|
|
58
|
+
// TODO verify if use case to disable it
|
|
59
|
+
// enabled: isUncontrolled,
|
|
60
|
+
});
|
|
61
|
+
const dismiss = useDismiss(context);
|
|
62
|
+
const role = useRole(context);
|
|
63
|
+
const listNav = useListNavigation(context, {
|
|
64
|
+
listRef: elementsRef,
|
|
65
|
+
activeIndex,
|
|
66
|
+
onNavigate: setActiveIndex,
|
|
67
|
+
loop: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const handleSelect = useCallback(() => {
|
|
71
|
+
setOpen(false);
|
|
72
|
+
}, [setOpen]);
|
|
73
|
+
|
|
74
|
+
function handleTypeaheadMatch(index: number | null) {
|
|
75
|
+
if (open) {
|
|
76
|
+
setActiveIndex(index);
|
|
77
|
+
} else {
|
|
78
|
+
handleSelect();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const typeahead = useTypeahead(context, {
|
|
83
|
+
listRef: labelsRef,
|
|
84
|
+
activeIndex,
|
|
85
|
+
onMatch: handleTypeaheadMatch,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const interactions = useInteractions([click, dismiss, role, listNav, typeahead]);
|
|
89
|
+
|
|
90
|
+
return useMemo(
|
|
91
|
+
() => ({
|
|
92
|
+
open,
|
|
93
|
+
setOpen,
|
|
94
|
+
...interactions,
|
|
95
|
+
...data,
|
|
96
|
+
elementsRef,
|
|
97
|
+
labelsRef,
|
|
98
|
+
color,
|
|
99
|
+
modal,
|
|
100
|
+
activeIndex,
|
|
101
|
+
handleSelect,
|
|
102
|
+
}),
|
|
103
|
+
[
|
|
104
|
+
open,
|
|
105
|
+
setOpen,
|
|
106
|
+
interactions,
|
|
107
|
+
data,
|
|
108
|
+
color,
|
|
109
|
+
modal,
|
|
110
|
+
activeIndex,
|
|
111
|
+
// getItemProps,
|
|
112
|
+
handleSelect,
|
|
113
|
+
],
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import useDropdownMenu from "./useDropdownMenu";
|
|
3
|
+
|
|
4
|
+
type ContextType = ReturnType<typeof useDropdownMenu> | null;
|
|
5
|
+
|
|
6
|
+
export const DropdownMenuContext = createContext<ContextType>(null);
|
|
7
|
+
|
|
8
|
+
export default function useDropdownMenuContext() {
|
|
9
|
+
const context = useContext(DropdownMenuContext);
|
|
10
|
+
|
|
11
|
+
if (context === null) {
|
|
12
|
+
throw new Error("DropdownMenu components must be wrapped in <DropdownMenu />");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return context;
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DropdownMenuNested, MenuItem, MenuItemWithChildren } from ".";
|
|
2
|
+
import { Button } from "../button";
|
|
3
|
+
|
|
4
|
+
import { Meta } from "@storybook/react";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/DropdownMenuNested",
|
|
8
|
+
component: DropdownMenuNested,
|
|
9
|
+
} satisfies Meta<typeof DropdownMenuNested>;
|
|
10
|
+
export default meta;
|
|
11
|
+
|
|
12
|
+
export const Basic = () => (
|
|
13
|
+
<DropdownMenuNested label="Edit" triggerComponent={Button} color="yellow">
|
|
14
|
+
<MenuItem label="Undo" onClick={() => console.log("Undo")} />
|
|
15
|
+
<MenuItem label="Redo" disabled />
|
|
16
|
+
<MenuItem label="Cut" />
|
|
17
|
+
<MenuItemWithChildren label="Copy as">
|
|
18
|
+
<MenuItem label="Text" />
|
|
19
|
+
<MenuItem label="Video" />
|
|
20
|
+
<MenuItemWithChildren label="Image">
|
|
21
|
+
<MenuItem label=".png" />
|
|
22
|
+
<MenuItem label=".jpg" />
|
|
23
|
+
<MenuItem label=".svg" />
|
|
24
|
+
<MenuItem label=".gif" />
|
|
25
|
+
</MenuItemWithChildren>
|
|
26
|
+
<MenuItem label="Audio" />
|
|
27
|
+
</MenuItemWithChildren>
|
|
28
|
+
<MenuItemWithChildren label="Share">
|
|
29
|
+
<MenuItem label="Mail" />
|
|
30
|
+
<MenuItem label="Instagram" />
|
|
31
|
+
</MenuItemWithChildren>
|
|
32
|
+
</DropdownMenuNested>
|
|
33
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import MenuItemWithChildren from "./MenuItemWithChildren";
|
|
3
|
+
import { FloatingTree } from "@floating-ui/react";
|
|
4
|
+
import { Button, type ButtonProps } from "../button";
|
|
5
|
+
|
|
6
|
+
export interface Props extends ButtonProps {
|
|
7
|
+
label: string;
|
|
8
|
+
triggerComponent: typeof Button;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DropdownMenuNested = forwardRef<HTMLButtonElement, Props>((props, ref) => {
|
|
12
|
+
return (
|
|
13
|
+
<FloatingTree>
|
|
14
|
+
<MenuItemWithChildren {...props} ref={ref} />
|
|
15
|
+
</FloatingTree>
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export default DropdownMenuNested;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Dispatch, HTMLProps, SetStateAction, createContext } from "react";
|
|
2
|
+
|
|
3
|
+
export const MenuContext = createContext<{
|
|
4
|
+
getItemProps: (userProps?: HTMLProps<HTMLElement>) => Record<string, unknown>;
|
|
5
|
+
activeIndex: number | null;
|
|
6
|
+
setActiveIndex: Dispatch<SetStateAction<number | null>>;
|
|
7
|
+
setHasFocusInside: Dispatch<SetStateAction<boolean>>;
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
}>({
|
|
10
|
+
getItemProps: () => ({}),
|
|
11
|
+
activeIndex: null,
|
|
12
|
+
setActiveIndex: () => {},
|
|
13
|
+
setHasFocusInside: () => {},
|
|
14
|
+
isOpen: false,
|
|
15
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useFloatingTree, useListItem, useMergeRefs } from "@floating-ui/react";
|
|
2
|
+
import { ButtonHTMLAttributes, FocusEvent, MouseEvent, forwardRef, useContext } from "react";
|
|
3
|
+
import { MenuContext } from "./MenuContext";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
label: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MenuItem = forwardRef<HTMLButtonElement, Props & ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
11
|
+
({ label, disabled, ...props }, forwardedRef) => {
|
|
12
|
+
const menu = useContext(MenuContext);
|
|
13
|
+
const item = useListItem({ label: disabled ? null : label });
|
|
14
|
+
const tree = useFloatingTree();
|
|
15
|
+
const isActive = item.index === menu.activeIndex;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
{...props}
|
|
20
|
+
ref={useMergeRefs([item.ref, forwardedRef])}
|
|
21
|
+
type="button"
|
|
22
|
+
role="menuitem"
|
|
23
|
+
className="option"
|
|
24
|
+
tabIndex={isActive ? 0 : -1}
|
|
25
|
+
disabled={disabled}
|
|
26
|
+
{...menu.getItemProps({
|
|
27
|
+
onClick(event: MouseEvent<HTMLButtonElement>) {
|
|
28
|
+
props.onClick?.(event);
|
|
29
|
+
tree?.events.emit("click");
|
|
30
|
+
},
|
|
31
|
+
onFocus(event: FocusEvent<HTMLButtonElement>) {
|
|
32
|
+
props.onFocus?.(event);
|
|
33
|
+
menu.setHasFocusInside(true);
|
|
34
|
+
},
|
|
35
|
+
})}
|
|
36
|
+
>
|
|
37
|
+
{label}
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export default MenuItem;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FloatingFocusManager,
|
|
3
|
+
FloatingList,
|
|
4
|
+
FloatingNode,
|
|
5
|
+
FloatingPortal,
|
|
6
|
+
autoUpdate,
|
|
7
|
+
flip,
|
|
8
|
+
offset,
|
|
9
|
+
safePolygon,
|
|
10
|
+
shift,
|
|
11
|
+
useClick,
|
|
12
|
+
useDismiss,
|
|
13
|
+
useFloating,
|
|
14
|
+
useFloatingNodeId,
|
|
15
|
+
useFloatingParentNodeId,
|
|
16
|
+
useFloatingTree,
|
|
17
|
+
useHover,
|
|
18
|
+
useInteractions,
|
|
19
|
+
useListItem,
|
|
20
|
+
useListNavigation,
|
|
21
|
+
useMergeRefs,
|
|
22
|
+
useRole,
|
|
23
|
+
useTypeahead,
|
|
24
|
+
} from "@floating-ui/react";
|
|
25
|
+
import { FocusEvent, forwardRef, useContext, useEffect, useRef, useState } from "react";
|
|
26
|
+
import { MenuContext } from "./MenuContext";
|
|
27
|
+
import clsx from "clsx";
|
|
28
|
+
import { Button, type ButtonProps } from "../button";
|
|
29
|
+
import { Dialog } from "../dialog";
|
|
30
|
+
|
|
31
|
+
export interface Props extends ButtonProps {
|
|
32
|
+
label?: string;
|
|
33
|
+
triggerComponent?: typeof Button;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const MenuItemWithChildren = forwardRef<HTMLButtonElement, Props>(
|
|
37
|
+
({ children, label, triggerComponent: TriggerComponent = Button, ...props }, forwardedRef) => {
|
|
38
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
39
|
+
const [hasFocusInside, setHasFocusInside] = useState(false);
|
|
40
|
+
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
|
41
|
+
|
|
42
|
+
const elementsRef = useRef<Array<HTMLButtonElement | null>>([]);
|
|
43
|
+
const labelsRef = useRef<Array<string | null>>([]);
|
|
44
|
+
const parent = useContext(MenuContext);
|
|
45
|
+
|
|
46
|
+
const tree = useFloatingTree();
|
|
47
|
+
const nodeId = useFloatingNodeId();
|
|
48
|
+
const parentId = useFloatingParentNodeId();
|
|
49
|
+
const item = useListItem();
|
|
50
|
+
|
|
51
|
+
const isNested = parentId != null;
|
|
52
|
+
|
|
53
|
+
const { floatingStyles, refs, context } = useFloating<HTMLButtonElement>({
|
|
54
|
+
nodeId,
|
|
55
|
+
open: isOpen,
|
|
56
|
+
onOpenChange: setIsOpen,
|
|
57
|
+
placement: isNested ? "right-start" : "bottom-start",
|
|
58
|
+
middleware: [
|
|
59
|
+
offset({ mainAxis: isNested ? -8 : 10, alignmentAxis: isNested ? 0 : 0 }),
|
|
60
|
+
flip(),
|
|
61
|
+
shift(),
|
|
62
|
+
],
|
|
63
|
+
whileElementsMounted: autoUpdate,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const hover = useHover(context, {
|
|
67
|
+
enabled: isNested,
|
|
68
|
+
delay: { open: 75 },
|
|
69
|
+
handleClose: safePolygon({ blockPointerEvents: true }),
|
|
70
|
+
});
|
|
71
|
+
const click = useClick(context, {
|
|
72
|
+
event: "mousedown",
|
|
73
|
+
toggle: !isNested,
|
|
74
|
+
ignoreMouse: isNested,
|
|
75
|
+
});
|
|
76
|
+
const role = useRole(context, { role: "menu" });
|
|
77
|
+
const dismiss = useDismiss(context, { bubbles: true });
|
|
78
|
+
const listNavigation = useListNavigation(context, {
|
|
79
|
+
listRef: elementsRef,
|
|
80
|
+
activeIndex,
|
|
81
|
+
nested: isNested,
|
|
82
|
+
onNavigate: setActiveIndex,
|
|
83
|
+
});
|
|
84
|
+
const typeahead = useTypeahead(context, {
|
|
85
|
+
listRef: labelsRef,
|
|
86
|
+
onMatch: isOpen ? setActiveIndex : undefined,
|
|
87
|
+
activeIndex,
|
|
88
|
+
});
|
|
89
|
+
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
|
|
90
|
+
hover,
|
|
91
|
+
click,
|
|
92
|
+
role,
|
|
93
|
+
dismiss,
|
|
94
|
+
listNavigation,
|
|
95
|
+
typeahead,
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!tree) return;
|
|
100
|
+
|
|
101
|
+
function handleTreeClick() {
|
|
102
|
+
setIsOpen(false);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function onSubMenuOpen(event: { nodeId: string; parentId: string }) {
|
|
106
|
+
// close sibling menu
|
|
107
|
+
if (event.nodeId !== nodeId && event.parentId === parentId) {
|
|
108
|
+
setIsOpen(false);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
tree.events.on("click", handleTreeClick);
|
|
113
|
+
tree.events.on("menuopen", onSubMenuOpen);
|
|
114
|
+
|
|
115
|
+
return () => {
|
|
116
|
+
tree.events.off("click", handleTreeClick);
|
|
117
|
+
tree.events.off("menuopen", onSubMenuOpen);
|
|
118
|
+
};
|
|
119
|
+
}, [tree, nodeId, parentId]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (isOpen && tree) {
|
|
123
|
+
tree.events.emit("menuopen", { parentId, nodeId });
|
|
124
|
+
}
|
|
125
|
+
}, [tree, isOpen, nodeId, parentId]);
|
|
126
|
+
|
|
127
|
+
const ref = useMergeRefs([refs.setReference, item.ref, forwardedRef]);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<FloatingNode id={nodeId}>
|
|
131
|
+
{isNested ? (
|
|
132
|
+
<button
|
|
133
|
+
ref={ref}
|
|
134
|
+
tabIndex={parent.activeIndex === item.index ? 0 : -1}
|
|
135
|
+
role="menuitem"
|
|
136
|
+
data-open={isOpen ? "" : undefined}
|
|
137
|
+
data-nested=""
|
|
138
|
+
data-focus-inside={hasFocusInside ? "" : undefined}
|
|
139
|
+
{...getReferenceProps(
|
|
140
|
+
parent.getItemProps({
|
|
141
|
+
onFocus(event: FocusEvent<HTMLButtonElement>) {
|
|
142
|
+
props.onFocus?.(event);
|
|
143
|
+
setHasFocusInside(false);
|
|
144
|
+
parent.setHasFocusInside(true);
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
)}
|
|
148
|
+
className={clsx(isNested && "option")}
|
|
149
|
+
>
|
|
150
|
+
<span>{label}</span>
|
|
151
|
+
<span className="ml-auto">
|
|
152
|
+
<i className="fe-angle-right mr-0" aria-hidden></i>
|
|
153
|
+
</span>
|
|
154
|
+
</button>
|
|
155
|
+
) : (
|
|
156
|
+
<TriggerComponent
|
|
157
|
+
ref={ref}
|
|
158
|
+
data-open={isOpen ? "" : undefined}
|
|
159
|
+
data-focus-inside={hasFocusInside ? "" : undefined}
|
|
160
|
+
{...props}
|
|
161
|
+
{...getReferenceProps(
|
|
162
|
+
parent.getItemProps({
|
|
163
|
+
onFocus(event: FocusEvent<HTMLButtonElement>) {
|
|
164
|
+
props.onFocus?.(event);
|
|
165
|
+
setHasFocusInside(false);
|
|
166
|
+
parent.setHasFocusInside(true);
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
)}
|
|
170
|
+
className={clsx(
|
|
171
|
+
isNested && "option",
|
|
172
|
+
!isNested && !TriggerComponent && "ll-dropdown-menu-trigger",
|
|
173
|
+
)}
|
|
174
|
+
>
|
|
175
|
+
{label}
|
|
176
|
+
</TriggerComponent>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
<MenuContext.Provider
|
|
180
|
+
value={{ activeIndex, setActiveIndex, getItemProps, setHasFocusInside, isOpen }}
|
|
181
|
+
>
|
|
182
|
+
{isOpen && (
|
|
183
|
+
<FloatingPortal>
|
|
184
|
+
<FloatingFocusManager
|
|
185
|
+
context={context}
|
|
186
|
+
modal={false}
|
|
187
|
+
initialFocus={isNested ? -1 : 0}
|
|
188
|
+
returnFocus={!isNested}
|
|
189
|
+
>
|
|
190
|
+
<FloatingList elementsRef={elementsRef} labelsRef={labelsRef}>
|
|
191
|
+
<div ref={refs.setFloating} style={floatingStyles} className="outline-none">
|
|
192
|
+
<Dialog {...getFloatingProps()}>{children}</Dialog>
|
|
193
|
+
</div>
|
|
194
|
+
</FloatingList>
|
|
195
|
+
</FloatingFocusManager>
|
|
196
|
+
</FloatingPortal>
|
|
197
|
+
)}
|
|
198
|
+
</MenuContext.Provider>
|
|
199
|
+
</FloatingNode>
|
|
200
|
+
);
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
export default MenuItemWithChildren;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Flash from "./Flash";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/Flash",
|
|
6
|
+
component: Flash,
|
|
7
|
+
} satisfies Meta<typeof Flash>;
|
|
8
|
+
export default meta;
|
|
9
|
+
|
|
10
|
+
type Story = StoryObj<typeof meta>;
|
|
11
|
+
|
|
12
|
+
export const Playbook: Story = {
|
|
13
|
+
args: {
|
|
14
|
+
color: "yellow",
|
|
15
|
+
children:
|
|
16
|
+
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta quod consequatur exercitationem delectus ad quibusdam ipsa officia consectetur laboriosam perspiciatis beatae incidunt sequi, velit sint aspernatur? Aliquid eum culpa cum.",
|
|
17
|
+
className: "",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const colors = ["yellow", "green", "blue", "orange", "red", "gray"] as const;
|
|
22
|
+
|
|
23
|
+
export const Context = () => (
|
|
24
|
+
<div className="flex flex-col gap-2">
|
|
25
|
+
{colors.map((color) => (
|
|
26
|
+
<Flash key={color} color={color}>
|
|
27
|
+
There is a plant seed on its back right from the day this Pokémon is born. The seed slowly
|
|
28
|
+
grows larger. There is a plant seed on its back right from the day this Pokémon is born. The
|
|
29
|
+
seed slowly grows larger. There is a plant seed on its back right from the day this Pokémon
|
|
30
|
+
is born. The seed slowly grows larger.
|
|
31
|
+
</Flash>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { ThemeColor } from "~/types.d";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { colorVariants } from "~/lib/tailwindVariants";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
color?: ThemeColor;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function Alert({ color = "yellow", children, className }: Props) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className={clsx(
|
|
16
|
+
"p-2 shadow border-l-4 dark:shadow-dark",
|
|
17
|
+
colorVariants[color].border,
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|