atproto explorer pdsls.dev
atproto tool
at main 4.9 kB view raw
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};