pentatrion-design 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/.storybook/main.ts +33 -0
  2. package/.storybook/manager.ts +8 -0
  3. package/.storybook/preview-head.html +2 -0
  4. package/.storybook/preview.ts +26 -0
  5. package/.storybook/public/bg-map.jpg +0 -0
  6. package/.storybook/public/favicon-32x32.png +0 -0
  7. package/.storybook/public/favicon.svg +28 -0
  8. package/.storybook/public/movies.json +2153 -0
  9. package/.storybook/public/population.json +1178 -0
  10. package/.storybook/public/town-74.geojson +8995 -0
  11. package/.storybook/public/town-74.json +4062 -0
  12. package/.storybook/theme.ts +14 -0
  13. package/Makefile +14 -0
  14. package/README.md +40 -0
  15. package/TODO.md +5 -0
  16. package/dist/index.d.ts +1110 -0
  17. package/dist/index.js +3707 -0
  18. package/dist/redux/index.d.ts +43 -0
  19. package/dist/redux/index.js +583 -0
  20. package/dist/types.d-DlHa_IaW.d.ts +14 -0
  21. package/images/logo-shadow.svg +173 -0
  22. package/images/logo.svg +76 -0
  23. package/package.json +85 -0
  24. package/postcss.config.js +7 -0
  25. package/src/components/autocomplete/Autocomplete.stories.tsx +89 -0
  26. package/src/components/autocomplete/Autocomplete.tsx +316 -0
  27. package/src/components/autocomplete/AutocompleteOption.tsx +35 -0
  28. package/src/components/autocomplete/LazyAutocomplete.tsx +130 -0
  29. package/src/components/autocomplete/SimpleAutocomplete.tsx +74 -0
  30. package/src/components/autocomplete/index.ts +6 -0
  31. package/src/components/autocomplete/useAutocompleteContext.ts +20 -0
  32. package/src/components/autocomplete/util.ts +19 -0
  33. package/src/components/badge/Badge.stories.tsx +72 -0
  34. package/src/components/badge/Badge.tsx +70 -0
  35. package/src/components/button/Button.stories.tsx +189 -0
  36. package/src/components/button/Button.test.tsx +20 -0
  37. package/src/components/button/Button.tsx +200 -0
  38. package/src/components/button/ButtonGroup.stories.tsx +46 -0
  39. package/src/components/button/ButtonGroup.tsx +29 -0
  40. package/src/components/button/index.ts +2 -0
  41. package/src/components/card/Card.stories.tsx +21 -0
  42. package/src/components/card/config.ts +4 -0
  43. package/src/components/card/index.ts +1 -0
  44. package/src/components/code/Code.tsx +15 -0
  45. package/src/components/context-menu/ContextMenu.stories.tsx +22 -0
  46. package/src/components/context-menu/ContextMenu.tsx +175 -0
  47. package/src/components/context-menu/ContextMenuItem.tsx +34 -0
  48. package/src/components/context-menu/index.ts +2 -0
  49. package/src/components/dialog/Dialog.stories.tsx +270 -0
  50. package/src/components/dialog/Dialog.tsx +46 -0
  51. package/src/components/dialog/index.ts +1 -0
  52. package/src/components/dialog/util.ts +29 -0
  53. package/src/components/dropdown-menu/DropdownMenu.stories.tsx +19 -0
  54. package/src/components/dropdown-menu/DropdownMenu.tsx +12 -0
  55. package/src/components/dropdown-menu/DropdownMenuContent.tsx +46 -0
  56. package/src/components/dropdown-menu/DropdownMenuItem.tsx +33 -0
  57. package/src/components/dropdown-menu/DropdownMenuTrigger.tsx +42 -0
  58. package/src/components/dropdown-menu/index.ts +7 -0
  59. package/src/components/dropdown-menu/interface.d.ts +13 -0
  60. package/src/components/dropdown-menu/useDropdownMenu.tsx +115 -0
  61. package/src/components/dropdown-menu/useDropdownMenuContext.ts +16 -0
  62. package/src/components/dropdown-menu-nested/DropdownMenuNested.stories.tsx +33 -0
  63. package/src/components/dropdown-menu-nested/DropdownMenuNested.tsx +19 -0
  64. package/src/components/dropdown-menu-nested/MenuContext.ts +15 -0
  65. package/src/components/dropdown-menu-nested/MenuItem.tsx +43 -0
  66. package/src/components/dropdown-menu-nested/MenuItemWithChildren.tsx +204 -0
  67. package/src/components/dropdown-menu-nested/index.ts +3 -0
  68. package/src/components/flash/Flash.stories.tsx +34 -0
  69. package/src/components/flash/Flash.tsx +24 -0
  70. package/src/components/highlight/Highlight.stories.tsx +128 -0
  71. package/src/components/highlight/Highlight.tsx +29 -0
  72. package/src/components/href/Href.stories.tsx +55 -0
  73. package/src/components/href/Href.tsx +18 -0
  74. package/src/components/input/Checkbox.stories.tsx +52 -0
  75. package/src/components/input/Checkbox.tsx +41 -0
  76. package/src/components/input/Input.stories.tsx +103 -0
  77. package/src/components/input/Input.test.tsx +10 -0
  78. package/src/components/input/Input.tsx +60 -0
  79. package/src/components/input/RadioGroup.stories.tsx +76 -0
  80. package/src/components/input/RadioGroup.tsx +84 -0
  81. package/src/components/input/Toggle.stories.tsx +29 -0
  82. package/src/components/input/Toggle.tsx +35 -0
  83. package/src/components/input/index.ts +4 -0
  84. package/src/components/input-field/InputField.stories.tsx +227 -0
  85. package/src/components/input-field/InputField.tsx +62 -0
  86. package/src/components/loader/Loader.stories.tsx +37 -0
  87. package/src/components/loader/Loader.tsx +54 -0
  88. package/src/components/modal/Components.tsx +58 -0
  89. package/src/components/modal/Modal.stories.tsx +113 -0
  90. package/src/components/modal/Modal.tsx +13 -0
  91. package/src/components/modal/ModalContent.tsx +46 -0
  92. package/src/components/modal/ModalTrigger.tsx +41 -0
  93. package/src/components/modal/index.ts +7 -0
  94. package/src/components/modal/interface.d.ts +10 -0
  95. package/src/components/modal/useModal.ts +48 -0
  96. package/src/components/modal/useModalContext.ts +16 -0
  97. package/src/components/notification/Notification.stories.tsx +51 -0
  98. package/src/components/notification/NotificationsContext.ts +79 -0
  99. package/src/components/notification/NotificationsProvider.tsx +58 -0
  100. package/src/components/notification/index.ts +2 -0
  101. package/src/components/popover/Components.tsx +45 -0
  102. package/src/components/popover/Popover.stories.tsx +46 -0
  103. package/src/components/popover/Popover.test.tsx +21 -0
  104. package/src/components/popover/Popover.tsx +12 -0
  105. package/src/components/popover/PopoverContent.tsx +46 -0
  106. package/src/components/popover/PopoverTrigger.tsx +42 -0
  107. package/src/components/popover/index.ts +7 -0
  108. package/src/components/popover/interface.d.ts +13 -0
  109. package/src/components/popover/usePopover.tsx +80 -0
  110. package/src/components/popover/usePopoverContext.ts +16 -0
  111. package/src/components/portal/Portal.tsx +12 -0
  112. package/src/components/quick-settings/QuickSettings.stories.tsx +182 -0
  113. package/src/components/resize-area/ResizeArea.stories.css +40 -0
  114. package/src/components/resize-area/ResizeArea.stories.tsx +30 -0
  115. package/src/components/resize-area/ResizeArea.tsx +79 -0
  116. package/src/components/scroll/Scroll.stories.tsx +81 -0
  117. package/src/components/scroll/Scroll.tsx +111 -0
  118. package/src/components/select/Select.stories.tsx +248 -0
  119. package/src/components/select/Select.tsx +362 -0
  120. package/src/components/select/SelectOption.tsx +29 -0
  121. package/src/components/select/SelectSelection.tsx +13 -0
  122. package/src/components/select/index.ts +5 -0
  123. package/src/components/select/interface.d.ts +36 -0
  124. package/src/components/select/useSelectContext.ts +19 -0
  125. package/src/components/select/util.ts +12 -0
  126. package/src/components/snack/Snack.stories.tsx +33 -0
  127. package/src/components/snack/Snack.tsx +47 -0
  128. package/src/components/sortable/Sortable.stories.tsx +98 -0
  129. package/src/components/sortable/Sortable.tsx +37 -0
  130. package/src/components/sortable/index.ts +2 -0
  131. package/src/components/sortable/types.d.ts +12 -0
  132. package/src/components/steps/Step.tsx +41 -0
  133. package/src/components/steps/Steps.stories.tsx +141 -0
  134. package/src/components/steps/Steps.tsx +49 -0
  135. package/src/components/steps/index.ts +3 -0
  136. package/src/components/steps/util.ts +3 -0
  137. package/src/components/table/Table.stories.tsx +36 -0
  138. package/src/components/table/Table.tsx +13 -0
  139. package/src/components/table/TableBody.tsx +9 -0
  140. package/src/components/table/TableCell.tsx +16 -0
  141. package/src/components/table/TableFooter.tsx +9 -0
  142. package/src/components/table/TableHeader.tsx +14 -0
  143. package/src/components/table/TableHeaderCell.tsx +14 -0
  144. package/src/components/table/TableRow.tsx +14 -0
  145. package/src/components/table/index.ts +7 -0
  146. package/src/components/tabs/Tabs.stories.tsx +66 -0
  147. package/src/components/tabs/Tabs.tsx +80 -0
  148. package/src/components/tooltip/SimpleTooltip.tsx +17 -0
  149. package/src/components/tooltip/Tooltip.stories.tsx +129 -0
  150. package/src/components/tooltip/Tooltip.test.tsx +18 -0
  151. package/src/components/tooltip/Tooltip.tsx +13 -0
  152. package/src/components/tooltip/TooltipContent.tsx +41 -0
  153. package/src/components/tooltip/TooltipTrigger.tsx +36 -0
  154. package/src/components/tooltip/index.ts +6 -0
  155. package/src/components/tooltip/interface.d.ts +15 -0
  156. package/src/components/tooltip/useTooltip.ts +87 -0
  157. package/src/components/tooltip/useTooltipContext.ts +16 -0
  158. package/src/env.d.ts +27 -0
  159. package/src/hooks/index.ts +13 -0
  160. package/src/hooks/useCombinedRefs.test.tsx +42 -0
  161. package/src/hooks/useCombinedRefs.ts +21 -0
  162. package/src/hooks/useCopyToClipboard.ts +28 -0
  163. package/src/hooks/useDebounce.ts +59 -0
  164. package/src/hooks/useEventCallback.ts +13 -0
  165. package/src/hooks/useEventListener.ts +86 -0
  166. package/src/hooks/useFetch.tsx +21 -0
  167. package/src/hooks/useIsClosing.ts +25 -0
  168. package/src/hooks/useIsMounted.tsx +16 -0
  169. package/src/hooks/useIsomorphicLayoutEffect.ts +4 -0
  170. package/src/hooks/useNotifications.tsx +13 -0
  171. package/src/hooks/useOnClickOutside.ts +35 -0
  172. package/src/hooks/usePrevious.ts +9 -0
  173. package/src/hooks/useRipple.test.tsx +52 -0
  174. package/src/hooks/useRipple.tsx +79 -0
  175. package/src/index.css +14 -0
  176. package/src/index.ts +39 -0
  177. package/src/lib/arrUtil.ts +8 -0
  178. package/src/lib/error.ts +18 -0
  179. package/src/lib/fetch.test.ts +107 -0
  180. package/src/lib/fetch.ts +115 -0
  181. package/src/lib/index.ts +3 -0
  182. package/src/lib/tailwindVariants.ts +35 -0
  183. package/src/redux/index.ts +1 -0
  184. package/src/redux/notification/components/NotificationConsumer.tsx +52 -0
  185. package/src/redux/notification/errorCatcherMiddleware.ts +17 -0
  186. package/src/redux/notification/hooks/useNotification.ts +26 -0
  187. package/src/redux/notification/index.ts +5 -0
  188. package/src/redux/notification/notificationSlice.ts +68 -0
  189. package/src/styles/Animation.stories.tsx +43 -0
  190. package/src/styles/Shadow.stories.tsx +43 -0
  191. package/src/styles/Themes.stories.tsx +31 -0
  192. package/src/styles/Typography.stories.tsx +101 -0
  193. package/src/tailwind/base.css +188 -0
  194. package/src/tailwind/components-input-outline.css +62 -0
  195. package/src/tailwind/components-resize-area.css +112 -0
  196. package/src/tailwind/components-step.css +161 -0
  197. package/src/tailwind/components.css +51 -0
  198. package/src/tailwind/keyframes.ts +69 -0
  199. package/src/tailwind/pentatrionTw.ts +147 -0
  200. package/src/tailwind/utilities-dialog.css +103 -0
  201. package/src/tailwind/utilities.css +3 -0
  202. package/src/types.d.ts +12 -0
  203. package/tailwind.config.js +18 -0
  204. package/tests/README.md +32 -0
  205. package/tests/setup.ts +1 -0
  206. package/tsconfig.json +14 -0
  207. package/tsup.config.ts +23 -0
  208. package/vite.config.ts +24 -0
@@ -0,0 +1,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,3 @@
1
+ export { default as DropdownMenuNested } from "./DropdownMenuNested";
2
+ export { default as MenuItem } from "./MenuItem";
3
+ export { default as MenuItemWithChildren } from "./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
+ }