atproto explorer

menu component

+135 -94
+101
src/components/dropdown.tsx
··· 1 + import { A } from "@solidjs/router"; 2 + import { 3 + Accessor, 4 + createContext, 5 + createSignal, 6 + JSX, 7 + onCleanup, 8 + onMount, 9 + Setter, 10 + Show, 11 + useContext, 12 + } from "solid-js"; 13 + import { addToClipboard } from "../utils/copy"; 14 + 15 + const MenuContext = createContext<{ 16 + showMenu: Accessor<boolean>; 17 + setShowMenu: Setter<boolean>; 18 + }>(); 19 + 20 + export 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 + 27 + export const CopyMenu = (props: { copyContent: string; label: string }) => { 28 + const ctx = useContext(MenuContext); 29 + 30 + return ( 31 + <button 32 + onClick={() => { 33 + addToClipboard(props.copyContent); 34 + ctx?.setShowMenu(false); 35 + }} 36 + 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" 37 + > 38 + {props.label} 39 + </button> 40 + ); 41 + }; 42 + 43 + export const NavMenu = (props: { href: string; label: string; icon: string }) => { 44 + const ctx = useContext(MenuContext); 45 + 46 + return ( 47 + <A 48 + href={props.href} 49 + onClick={() => ctx?.setShowMenu(false)} 50 + class="flex items-center gap-1 rounded-lg p-1 hover:bg-neutral-200/50 active:bg-neutral-200/50 dark:hover:bg-neutral-700 dark:active:bg-neutral-700" 51 + > 52 + <span class={"iconify " + props.icon}></span> 53 + <span>{props.label}</span> 54 + </A> 55 + ); 56 + }; 57 + 58 + export const DropdownMenu = (props: { 59 + icon: string; 60 + buttonClass?: string; 61 + menuClass?: string; 62 + children?: JSX.Element; 63 + }) => { 64 + const ctx = useContext(MenuContext); 65 + const [menu, setMenu] = createSignal<HTMLDivElement>(); 66 + const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>(); 67 + 68 + const clickEvent = (event: MouseEvent) => { 69 + const target = event.target as Node; 70 + if (!menuButton()?.contains(target) && !menu()?.contains(target)) ctx?.setShowMenu(false); 71 + }; 72 + 73 + onMount(() => window.addEventListener("click", clickEvent)); 74 + onCleanup(() => window.removeEventListener("click", clickEvent)); 75 + 76 + return ( 77 + <div class="relative"> 78 + <button 79 + class={ 80 + "flex items-center hover:bg-neutral-200 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-700 " + 81 + props.buttonClass 82 + } 83 + ref={setMenuButton} 84 + onClick={() => ctx?.setShowMenu(!ctx?.showMenu())} 85 + > 86 + <span class={"iconify " + props.icon}></span> 87 + </button> 88 + <Show when={ctx?.showMenu()}> 89 + <div 90 + ref={setMenu} 91 + class={ 92 + "dark:bg-dark-300 absolute right-0 z-20 flex flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 shadow-md dark:border-neutral-700 " + 93 + props.menuClass 94 + } 95 + > 96 + {props.children} 97 + </div> 98 + </Show> 99 + </div> 100 + ); 101 + };
+22 -55
src/components/navbar.tsx
··· 1 1 import { Did, Handle } from "@atcute/lexicons"; 2 2 import { A, Params, useLocation } from "@solidjs/router"; 3 - import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js"; 3 + import { createEffect, createSignal, Show } from "solid-js"; 4 4 import { didDocCache, labelerCache, validateHandle } from "../utils/api"; 5 - import { addToClipboard } from "../utils/copy"; 5 + import { CopyMenu, DropdownMenu, MenuProvider } from "./dropdown"; 6 6 import Tooltip from "./tooltip"; 7 7 8 8 export const [pds, setPDS] = createSignal<string>(); ··· 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 [copyButton, setCopyButton] = createSignal<HTMLButtonElement>(); 38 35 39 36 createEffect(() => { 40 37 if (cid() !== undefined) setFullCid(false); ··· 54 51 } 55 52 }); 56 53 57 - const clickEvent = (event: MouseEvent) => { 58 - const target = event.target as Node; 59 - if (!copyButton()?.contains(target) && !copyMenu()?.contains(target)) setShowCopyMenu(false); 60 - }; 61 - 62 - onMount(() => window.addEventListener("click", clickEvent)); 63 - onCleanup(() => window.removeEventListener("click", clickEvent)); 64 - 65 - const CopyButton = (props: { copyContent: string; label: string }) => { 66 - return ( 67 - <button 68 - onClick={() => { 69 - addToClipboard(props.copyContent); 70 - setShowCopyMenu(false); 71 - }} 72 - 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" 73 - > 74 - {props.label} 75 - </button> 76 - ); 77 - }; 78 - 79 54 return ( 80 55 <nav class="mt-4 flex w-[22rem] flex-col text-sm wrap-anywhere sm:w-[24rem]"> 81 56 <div class="relative flex items-center justify-between gap-1"> ··· 98 73 </Show> 99 74 </Show> 100 75 </div> 101 - <div class="relative"> 102 - <button 103 - 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" 104 - ref={setCopyButton} 105 - onClick={() => setShowCopyMenu(!showCopyMenu())} 76 + <MenuProvider> 77 + <DropdownMenu 78 + icon="lucide--copy text-base" 79 + buttonClass="rounded p-0.5" 80 + menuClass="top-6 p-2 text-xs" 106 81 > 107 - <span class="iconify lucide--copy text-base"></span> 108 - </button> 109 - <Show when={showCopyMenu()}> 110 - <div 111 - ref={setCopyMenu} 112 - 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" 113 - > 114 - <Show when={pds()}> 115 - <CopyButton copyContent={pds()!} label="Copy PDS" /> 116 - </Show> 117 - <Show when={props.params.repo}> 118 - <CopyButton copyContent={props.params.repo} label="Copy DID" /> 119 - <CopyButton 120 - copyContent={`at://${props.params.repo}${props.params.collection ? `/${props.params.collection}` : ""}${props.params.rkey ? `/${props.params.rkey}` : ""}`} 121 - label="Copy AT URI" 122 - /> 123 - </Show> 124 - <Show when={props.params.rkey && cid()}> 125 - <CopyButton copyContent={cid()!} label="Copy CID" /> 126 - </Show> 127 - </div> 128 - </Show> 129 - </div> 82 + <Show when={pds()}> 83 + <CopyMenu copyContent={pds()!} label="Copy PDS" /> 84 + </Show> 85 + <Show when={props.params.repo}> 86 + <CopyMenu copyContent={props.params.repo} label="Copy DID" /> 87 + <CopyMenu 88 + copyContent={`at://${props.params.repo}${props.params.collection ? `/${props.params.collection}` : ""}${props.params.rkey ? `/${props.params.rkey}` : ""}`} 89 + label="Copy AT URI" 90 + /> 91 + </Show> 92 + <Show when={props.params.rkey && cid()}> 93 + <CopyMenu copyContent={cid()!} label="Copy CID" /> 94 + </Show> 95 + </DropdownMenu> 96 + </MenuProvider> 130 97 </div> 131 98 <div class="flex flex-col flex-wrap"> 132 99 <Show when={props.params.repo}>
+12 -39
src/layout.tsx
··· 4 4 import { createEffect, createSignal, ErrorBoundary, onMount, Show, Suspense } from "solid-js"; 5 5 import { AccountManager } from "./components/account.jsx"; 6 6 import { RecordEditor } from "./components/create.jsx"; 7 + import { DropdownMenu, MenuProvider, NavMenu } from "./components/dropdown.jsx"; 7 8 import { agent } from "./components/login.jsx"; 8 9 import { NavBar } from "./components/navbar.jsx"; 9 10 import { Search } from "./components/search.jsx"; ··· 20 21 const location = useLocation(); 21 22 const navigate = useNavigate(); 22 23 let timeout: number; 23 - const [showMenu, setShowMenu] = createSignal(false); 24 - const [menu, setMenu] = createSignal<HTMLDivElement>(); 25 - const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>(); 26 24 27 25 createEffect(async () => { 28 26 if (props.params.repo && !props.params.repo.startsWith("did:")) { ··· 40 38 41 39 onMount(() => { 42 40 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", themeEvent); 43 - window.addEventListener("click", (ev) => { 44 - if (!menuButton()?.contains(ev.target as Node) && !menu()?.contains(ev.target as Node)) 45 - setShowMenu(false); 46 - }); 47 41 }); 48 42 49 - const NavButton = (props: { href: string; label: string; icon: string }) => { 50 - return ( 51 - <A 52 - href={props.href} 53 - onClick={() => setShowMenu(false)} 54 - class="flex items-center gap-1 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 - <span class={"iconify " + props.icon}></span> 57 - <span>{props.label}</span> 58 - </A> 59 - ); 60 - }; 61 - 62 43 return ( 63 44 <div id="main" class="m-4 flex flex-col items-center text-neutral-900 dark:text-neutral-200"> 64 45 <MetaProvider> ··· 80 61 <RecordEditor create={true} /> 81 62 </Show> 82 63 <AccountManager /> 83 - <div class="relative"> 84 - <button 85 - onClick={() => setShowMenu(!showMenu())} 86 - ref={setMenuButton} 87 - class="flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-700" 64 + <MenuProvider> 65 + <DropdownMenu 66 + icon="lucide--menu text-xl" 67 + buttonClass="rounded-lg p-1" 68 + menuClass="top-8 p-3 text-sm" 88 69 > 89 - <span class="iconify lucide--menu text-xl"></span> 90 - </button> 91 - <Show when={showMenu()}> 92 - <div 93 - ref={setMenu} 94 - 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" 95 - > 96 - <NavButton href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 97 - <NavButton href="/firehose" label="Firehose" icon="lucide--waves" /> 98 - <NavButton href="/settings" label="Settings" icon="lucide--settings" /> 99 - <ThemeSelection /> 100 - </div> 101 - </Show> 102 - </div> 70 + <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 71 + <NavMenu href="/firehose" label="Firehose" icon="lucide--waves" /> 72 + <NavMenu href="/settings" label="Settings" icon="lucide--settings" /> 73 + <ThemeSelection /> 74 + </DropdownMenu> 75 + </MenuProvider> 103 76 </div> 104 77 </header> 105 78 <div class="mb-4 flex max-w-full min-w-[22rem] flex-col items-center text-pretty sm:min-w-[24rem] md:max-w-[48rem]">