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 { addToClipboard } from "../utils/copy";
14
15const MenuContext = createContext<{
16 showMenu: Accessor<boolean>;
17 setShowMenu: Setter<boolean>;
18}>();
19
20export const MenuProvider = (props: { children?: JSX.Element }) => {
21 const [showMenu, setShowMenu] = createSignal(false);
22 const value = { showMenu, setShowMenu };
23
24 return <MenuContext.Provider value={value}>{props.children}</MenuContext.Provider>;
25};
26
27export const CopyMenu = (props: { content: string; label: string; icon?: string }) => {
28 const ctx = useContext(MenuContext);
29
30 return (
31 <button
32 onClick={() => {
33 addToClipboard(props.content);
34 ctx?.setShowMenu(false);
35 }}
36 class="flex items-center gap-1.5 rounded-md p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
37 >
38 <Show when={props.icon}>
39 <span class={"iconify shrink-0 " + props.icon}></span>
40 </Show>
41 <span class="whitespace-nowrap">{props.label}</span>
42 </button>
43 );
44};
45
46export const NavMenu = (props: {
47 href: string;
48 label: string;
49 icon?: string;
50 newTab?: boolean;
51 external?: boolean;
52}) => {
53 const ctx = useContext(MenuContext);
54
55 return (
56 <A
57 href={props.href}
58 onClick={() => ctx?.setShowMenu(false)}
59 class="flex items-center gap-1.5 rounded-md p-1 hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
60 classList={{ "justify-between": props.external }}
61 target={props.newTab ? "_blank" : undefined}
62 >
63 <Show when={props.icon}>
64 <span class={"iconify shrink-0 " + props.icon}></span>
65 </Show>
66 <span class="whitespace-nowrap">{props.label}</span>
67 <Show when={props.external}>
68 <span class="iconify lucide--external-link"></span>
69 </Show>
70 </A>
71 );
72};
73
74export const ActionMenu = (props: {
75 label: string;
76 icon: string;
77 onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>;
78}) => {
79 return (
80 <button
81 onClick={props.onClick}
82 class="flex items-center gap-1.5 rounded-md p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
83 >
84 <Show when={props.icon}>
85 <span class={"iconify shrink-0 " + props.icon}></span>
86 </Show>
87 <span class="whitespace-nowrap">{props.label}</span>
88 </button>
89 );
90};
91
92export const MenuSeparator = () => {
93 return <div class="my-1 h-[0.5px] bg-neutral-300 dark:bg-neutral-600" />;
94};
95
96export const DropdownMenu = (props: {
97 icon: string;
98 buttonClass?: string;
99 menuClass?: string;
100 children?: JSX.Element;
101}) => {
102 const ctx = useContext(MenuContext);
103 const [menu, setMenu] = createSignal<HTMLDivElement>();
104 const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>();
105
106 const clickEvent = (event: MouseEvent) => {
107 const target = event.target as Node;
108 if (!menuButton()?.contains(target) && !menu()?.contains(target)) ctx?.setShowMenu(false);
109 };
110
111 onMount(() => window.addEventListener("click", clickEvent));
112 onCleanup(() => window.removeEventListener("click", clickEvent));
113
114 return (
115 <div class="relative">
116 <button
117 class={
118 "flex items-center hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600 " +
119 props.buttonClass
120 }
121 ref={setMenuButton}
122 onClick={() => ctx?.setShowMenu(!ctx?.showMenu())}
123 >
124 <span class={"iconify " + props.icon}></span>
125 </button>
126 <Show when={ctx?.showMenu()}>
127 <div
128 ref={setMenu}
129 class={
130 "dark:bg-dark-300 dark:shadow-dark-700 absolute right-0 z-40 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 shadow-md dark:border-neutral-700 " +
131 props.menuClass
132 }
133 >
134 {props.children}
135 </div>
136 </Show>
137 </div>
138 );
139};