atproto explorer

add navbar copy menu

+86 -71
+67 -44
src/components/navbar.tsx
··· 1 1 import { A, Params, useLocation } from "@solidjs/router"; 2 2 import Tooltip from "./tooltip"; 3 - import { createEffect, createSignal, Show } from "solid-js"; 3 + import { createEffect, createSignal, onMount, Show } from "solid-js"; 4 4 import { didDocCache, labelerCache, validateHandle } from "../utils/api"; 5 5 import { Did, Handle } from "@atcute/lexicons"; 6 6 import { addToClipboard } from "../utils/copy"; ··· 32 32 const [validHandle, setValidHandle] = createSignal<boolean | undefined>(undefined); 33 33 const [fullCid, setFullCid] = createSignal(false); 34 34 const [showHandle, setShowHandle] = createSignal(localStorage.showHandle === "true"); 35 + const [showCopyMenu, setShowCopyMenu] = createSignal(false); 36 + const [copyMenu, setCopyMenu] = createSignal<HTMLDivElement>(); 37 + const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>(); 35 38 36 39 createEffect(() => { 37 40 if (cid() !== undefined) setFullCid(false); ··· 51 54 } 52 55 }); 53 56 57 + onMount(() => 58 + window.addEventListener("click", (ev) => { 59 + if (!menuButton()?.contains(ev.target as Node) && !copyMenu()?.contains(ev.target as Node)) 60 + setShowCopyMenu(false); 61 + }), 62 + ); 63 + 64 + const CopyButton = (props: { copyContent: string; label: string }) => { 65 + return ( 66 + <button 67 + onClick={() => { 68 + addToClipboard(props.copyContent); 69 + setShowCopyMenu(false); 70 + }} 71 + class="flex rounded-lg p-1 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200/50 dark:hover:bg-neutral-700 dark:active:bg-neutral-700" 72 + > 73 + {props.label} 74 + </button> 75 + ); 76 + }; 77 + 54 78 return ( 55 79 <nav class="mt-4 flex w-[22rem] flex-col text-sm wrap-anywhere sm:w-[24rem]"> 56 80 <div class="relative flex items-center justify-between gap-1"> 57 81 <div class="flex min-h-[1.25rem] basis-full items-center gap-2"> 58 82 <Tooltip text="PDS"> 59 - <button 60 - class="iconify lucide--hard-drive shrink-0 text-lg" 61 - onclick={() => addToClipboard(pds()!)} 62 - ></button> 83 + <span class="iconify lucide--hard-drive shrink-0 text-lg"></span> 63 84 </Tooltip> 64 85 <Show when={pds()}> 65 86 <Show when={props.params.repo}> ··· 76 97 </Show> 77 98 </Show> 78 99 </div> 79 - <Tooltip 80 - text={`Copy ${ 81 - props.params.collection ? "AT URI" 82 - : props.params.repo ? "DID" 83 - : "PDS" 84 - }`} 85 - > 100 + <div class="relative"> 86 101 <button 87 - class="iconify lucide--copy shrink-0 text-lg" 88 - onclick={() => 89 - addToClipboard( 90 - props.params.collection ? 91 - `at://${props.params.repo}/${props.params.collection}${props.params.rkey ? `/${props.params.rkey}` : ""}` 92 - : props.params.repo ? props.params.repo 93 - : pds()!, 94 - ) 95 - } 96 - ></button> 97 - </Tooltip> 102 + class="flex items-center rounded p-0.5 hover:bg-neutral-200 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-700" 103 + ref={setMenuButton} 104 + onClick={() => setShowCopyMenu(!showCopyMenu())} 105 + > 106 + <span class="iconify lucide--copy text-base"></span> 107 + </button> 108 + <Show when={showCopyMenu()}> 109 + <div 110 + ref={setCopyMenu} 111 + class="dark:bg-dark-300 absolute top-6 right-0 z-20 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-xs shadow-md dark:border-neutral-700" 112 + > 113 + <Show when={pds()}> 114 + <CopyButton copyContent={pds()!} label="Copy PDS" /> 115 + </Show> 116 + <Show when={props.params.repo}> 117 + <CopyButton copyContent={props.params.repo} label="Copy DID" /> 118 + <CopyButton 119 + copyContent={`at://${props.params.repo}${props.params.collection ? `/${props.params.collection}` : ""}${props.params.rkey ? `/${props.params.rkey}` : ""}`} 120 + label="Copy AT URI" 121 + /> 122 + </Show> 123 + <Show when={props.params.rkey && cid()}> 124 + <CopyButton copyContent={cid()!} label="Copy CID" /> 125 + </Show> 126 + </div> 127 + </Show> 128 + </div> 98 129 </div> 99 130 <div class="flex flex-col flex-wrap"> 100 131 <Show when={props.params.repo}> 101 132 <div class="relative mt-1 flex items-center justify-between gap-1"> 102 133 <div class="flex basis-full items-center gap-2"> 103 134 <Tooltip text="Repository"> 104 - <button 105 - class="iconify lucide--book-user text-lg" 106 - onclick={() => addToClipboard(props.params.repo)} 107 - ></button> 135 + <span class="iconify lucide--book-user text-lg"></span> 108 136 </Tooltip> 109 137 <div class="flex w-full gap-1"> 110 138 {props.params.collection || location.pathname.includes("/labels") ? ··· 139 167 </div> 140 168 <Tooltip text={showHandle() ? "Show DID" : "Show handle"}> 141 169 <button 170 + class="flex items-center rounded p-0.5 hover:bg-neutral-200 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-700" 142 171 onclick={() => { 143 172 localStorage.showHandle = !showHandle(); 144 173 setShowHandle(!showHandle()); 145 174 }} 146 - class={ 147 - `iconify shrink-0 text-lg transition-transform duration-400 ${showHandle() ? "rotate-y-180" : ""} ` + 148 - (swapIcons[props.params.repo] ?? "lucide--arrow-left-right") 149 - } 150 - ></button> 175 + > 176 + <span 177 + class={ 178 + `iconify shrink-0 text-base transition-transform duration-400 ${showHandle() ? "rotate-y-180" : ""} ` + 179 + (swapIcons[props.params.repo] ?? "lucide--arrow-left-right") 180 + } 181 + ></span> 182 + </button> 151 183 </Tooltip> 152 184 </div> 153 185 </Show> ··· 166 198 <Show when={props.params.collection}> 167 199 <div class="mt-1 flex items-center gap-2"> 168 200 <Tooltip text="Collection"> 169 - <button 170 - onclick={() => addToClipboard(props.params.collection)} 171 - class="iconify lucide--folder-open text-lg" 172 - ></button> 201 + <span class="iconify lucide--folder-open text-lg"></span> 173 202 </Tooltip> 174 203 <Show when={props.params.rkey}> 175 204 <A ··· 188 217 <Show when={props.params.rkey}> 189 218 <div class="mt-1 flex items-center gap-2"> 190 219 <Tooltip text="Record"> 191 - <button 192 - onclick={() => addToClipboard(props.params.rkey)} 193 - class="iconify lucide--file-json text-lg" 194 - ></button> 220 + <span class="iconify lucide--file-json text-lg"></span> 195 221 </Tooltip> 196 222 <div class="flex gap-1"> 197 223 <span>{props.params.rkey}</span> ··· 228 254 {(cid) => ( 229 255 <div class="mt-1 flex gap-2"> 230 256 <Tooltip text="CID"> 231 - <button 232 - onclick={() => addToClipboard(cid())} 233 - class="iconify lucide--box text-lg" 234 - ></button> 257 + <span class="iconify lucide--box text-lg"></span> 235 258 </Tooltip> 236 259 <button 237 260 dir="rtl"
+19 -27
src/layout.tsx
··· 38 38 } 39 39 }); 40 40 41 - const clickEvent = (event: MouseEvent) => { 42 - if (!menuButton()?.contains(event.target as Node) && !menu()?.contains(event.target as Node)) 43 - setShowMenu(false); 44 - }; 45 - 46 41 onMount(() => { 47 42 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent); 48 - window.addEventListener("click", clickEvent); 43 + window.addEventListener("click", (ev) => { 44 + if (!menuButton()?.contains(ev.target as Node) && !menu()?.contains(ev.target as Node)) 45 + setShowMenu(false); 46 + }); 49 47 }); 48 + 49 + const NavButton = (props: { href: string; label: string }) => { 50 + return ( 51 + <A 52 + href={props.href} 53 + onClick={() => setShowMenu(false)} 54 + class="rounded-lg p-1 hover:bg-neutral-200/50 active:bg-neutral-200/50 dark:hover:bg-neutral-700 dark:active:bg-neutral-700" 55 + > 56 + {props.label} 57 + </A> 58 + ); 59 + }; 50 60 51 61 return ( 52 62 <div id="main" class="m-4 flex flex-col items-center text-neutral-900 dark:text-neutral-200"> ··· 82 92 ref={setMenu} 83 93 class="dark:bg-dark-300 absolute top-8 right-0 z-20 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-3 text-sm shadow-md dark:border-neutral-700" 84 94 > 85 - <A 86 - href="/jetstream" 87 - onClick={() => setShowMenu(false)} 88 - class="rounded-lg p-1 hover:bg-neutral-200/50 active:bg-neutral-200/50 dark:hover:bg-neutral-700 dark:active:bg-neutral-700" 89 - > 90 - <span>Jetstream</span> 91 - </A> 92 - <A 93 - href="/firehose" 94 - onClick={() => setShowMenu(false)} 95 - class="rounded-lg p-1 hover:bg-neutral-200/50 active:bg-neutral-200/50 dark:hover:bg-neutral-700 dark:active:bg-neutral-700" 96 - > 97 - <span>Firehose</span> 98 - </A> 99 - <A 100 - href="/settings" 101 - onClick={() => setShowMenu(false)} 102 - class="rounded-lg p-1 hover:bg-neutral-200/50 active:bg-neutral-200/50 dark:hover:bg-neutral-700 dark:active:bg-neutral-700" 103 - > 104 - <span>Settings</span> 105 - </A> 95 + <NavButton href="/jetstream" label="Jetstream" /> 96 + <NavButton href="/firehose" label="Firehose" /> 97 + <NavButton href="/settings" label="Settings" /> 106 98 <ThemeSelection /> 107 99 </div> 108 100 </Show>