1import { A } from "@solidjs/router";
2import {
3 Accessor,
4 createContext,
5 createSignal,
6 JSX,
7 onCleanup,
8 onMount,
9 Setter,
10 Show,
11 useContext,
12} from "solid-js";
13import { Portal } from "solid-js/web";
14import { addToClipboard } from "../utils/copy";
15
16const MenuContext = createContext<{
17 showMenu: Accessor<boolean>;
18 setShowMenu: Setter<boolean>;
19}>();
20
21export const MenuProvider = (props: { children?: JSX.Element }) => {
22 const [showMenu, setShowMenu] = createSignal(false);
23 const value = { showMenu, setShowMenu };
24
25 return <MenuContext.Provider value={value}>{props.children}</MenuContext.Provider>;
26};
27
28export const CopyMenu = (props: { content: string; label: string; icon?: string }) => {
29 const ctx = useContext(MenuContext);
30
31 return (
32 <button
33 onClick={() => {
34 addToClipboard(props.content);
35 ctx?.setShowMenu(false);
36 }}
37 class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
38 >
39 <Show when={props.icon}>
40 <span class={"iconify shrink-0 " + props.icon}></span>
41 </Show>
42 <span class="whitespace-nowrap">{props.label}</span>
43 </button>
44 );
45};
46
47export const NavMenu = (props: {
48 href: string;
49 label: string;
50 icon?: string;
51 newTab?: boolean;
52 external?: boolean;
53}) => {
54 const ctx = useContext(MenuContext);
55
56 return (
57 <A
58 href={props.href}
59 onClick={() => ctx?.setShowMenu(false)}
60 class="flex items-center gap-2 rounded-md p-1.5 hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
61 classList={{ "justify-between": props.external }}
62 target={props.newTab ? "_blank" : undefined}
63 >
64 <Show when={props.icon}>
65 <span class={"iconify shrink-0 " + props.icon}></span>
66 </Show>
67 <span class="whitespace-nowrap">{props.label}</span>
68 <Show when={props.external}>
69 <span class="iconify lucide--external-link"></span>
70 </Show>
71 </A>
72 );
73};
74
75export const ActionMenu = (props: {
76 label: string;
77 icon: string;
78 onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
79}) => {
80 return (
81 <button
82 onClick={props.onClick}
83 class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
84 >
85 <Show when={props.icon}>
86 <span class={"iconify shrink-0 " + props.icon}></span>
87 </Show>
88 <span class="whitespace-nowrap">{props.label}</span>
89 </button>
90 );
91};
92
93export const MenuSeparator = () => {
94 return <div class="my-1 h-[0.5px] bg-neutral-300 dark:bg-neutral-600" />;
95};
96
97export const DropdownMenu = (props: {
98 icon: string;
99 buttonClass?: string;
100 menuClass?: string;
101 children?: JSX.Element;
102}) => {
103 const ctx = useContext(MenuContext);
104 const [menu, setMenu] = createSignal<HTMLDivElement>();
105 const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>();
106 const [buttonRect, setButtonRect] = createSignal<DOMRect>();
107
108 const clickEvent = (event: MouseEvent) => {
109 const target = event.target as Node;
110 if (!menuButton()?.contains(target) && !menu()?.contains(target)) ctx?.setShowMenu(false);
111 };
112
113 const updatePosition = () => {
114 const rect = menuButton()?.getBoundingClientRect();
115 if (rect) setButtonRect(rect);
116 };
117
118 onMount(() => {
119 window.addEventListener("click", clickEvent);
120 window.addEventListener("scroll", updatePosition, true);
121 window.addEventListener("resize", updatePosition);
122 });
123
124 onCleanup(() => {
125 window.removeEventListener("click", clickEvent);
126 window.removeEventListener("scroll", updatePosition, true);
127 window.removeEventListener("resize", updatePosition);
128 });
129
130 return (
131 <div class="relative">
132 <button
133 class={
134 "flex items-center hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 " +
135 props.buttonClass
136 }
137 ref={setMenuButton}
138 onClick={() => {
139 updatePosition();
140 ctx?.setShowMenu(!ctx?.showMenu());
141 }}
142 >
143 <span class={"iconify " + props.icon}></span>
144 </button>
145 <Show when={ctx?.showMenu()}>
146 <Portal>
147 <div
148 ref={setMenu}
149 style={{
150 position: "fixed",
151 top: `${(buttonRect()?.bottom ?? 0) + 4}px`,
152 left: `${(buttonRect()?.right ?? 0) - 160}px`,
153 }}
154 class={
155 "dark:bg-dark-300 dark:shadow-dark-700 z-50 flex min-w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-md dark:border-neutral-700 " +
156 props.menuClass
157 }
158 >
159 {props.children}
160 </div>
161 </Portal>
162 </Show>
163 </div>
164 );
165};