atmosphere explorer
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

improve dropdown flow

Juliet d813d590 911b5529

+78 -57
+35 -24
src/components/account.tsx
··· 10 10 import { createSignal, For, onMount, Show } from "solid-js"; 11 11 import { createStore, produce } from "solid-js/store"; 12 12 import { resolveDidDoc } from "../utils/api.js"; 13 + import { ActionMenu, DropdownMenu, MenuProvider, NavMenu } from "./dropdown.jsx"; 13 14 import { agent, Login, retrieveSession, Sessions, setAgent } from "./login.jsx"; 14 15 import { Modal } from "./modal.jsx"; 15 16 16 17 export const [sessions, setSessions] = createStore<Sessions>(); 17 18 19 + const AccountDropdown = (props: { did: Did }) => { 20 + const removeSession = async (did: Did) => { 21 + const currentSession = agent()?.sub; 22 + try { 23 + const session = await getSession(did, { allowStale: true }); 24 + const agent = new OAuthUserAgent(session); 25 + await agent.signOut(); 26 + } catch { 27 + deleteStoredSession(did); 28 + } 29 + setSessions( 30 + produce((accs) => { 31 + delete accs[did]; 32 + }), 33 + ); 34 + localStorage.setItem("sessions", JSON.stringify(sessions)); 35 + if (currentSession === did) setAgent(undefined); 36 + }; 37 + 38 + return ( 39 + <MenuProvider> 40 + <DropdownMenu icon="lucide--ellipsis" buttonClass="rounded-md p-2"> 41 + <NavMenu href={`/at://${props.did}`} label="Go to repo" icon="lucide--user-round" /> 42 + <ActionMenu 43 + icon="lucide--x" 44 + label="Remove account" 45 + onClick={() => removeSession(props.did)} 46 + /> 47 + </DropdownMenu> 48 + </MenuProvider> 49 + ); 50 + }; 51 + 18 52 export const AccountManager = () => { 19 53 const [openManager, setOpenManager] = createSignal(false); 20 54 const [avatars, setAvatars] = createStore<Record<Did, string>>(); ··· 61 95 } 62 96 }; 63 97 64 - const removeSession = async (did: Did) => { 65 - const currentSession = agent()?.sub; 66 - try { 67 - const session = await getSession(did, { allowStale: true }); 68 - const agent = new OAuthUserAgent(session); 69 - await agent.signOut(); 70 - } catch { 71 - deleteStoredSession(did); 72 - } 73 - setSessions( 74 - produce((accs) => { 75 - delete accs[did]; 76 - }), 77 - ); 78 - localStorage.setItem("sessions", JSON.stringify(sessions)); 79 - if (currentSession === did) setAgent(undefined); 80 - }; 81 - 82 98 const getAvatar = async (did: Did) => { 83 99 const rpc = new Client({ 84 100 handler: new CredentialManager({ service: "https://public.api.bsky.app" }), ··· 130 146 <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span> 131 147 </Show> 132 148 </button> 133 - <button 134 - onclick={() => removeSession(did as Did)} 135 - class="flex items-center rounded-md p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 136 - > 137 - <span class="iconify lucide--x"></span> 138 - </button> 149 + <AccountDropdown did={did as Did} /> 139 150 </div> 140 151 )} 141 152 </For>
+38 -12
src/components/dropdown.tsx
··· 10 10 Show, 11 11 useContext, 12 12 } from "solid-js"; 13 + import { Portal } from "solid-js/web"; 13 14 import { addToClipboard } from "../utils/copy"; 14 15 15 16 const MenuContext = createContext<{ ··· 102 103 const ctx = useContext(MenuContext); 103 104 const [menu, setMenu] = createSignal<HTMLDivElement>(); 104 105 const [menuButton, setMenuButton] = createSignal<HTMLButtonElement>(); 106 + const [buttonRect, setButtonRect] = createSignal<DOMRect>(); 105 107 106 108 const clickEvent = (event: MouseEvent) => { 107 109 const target = event.target as Node; 108 110 if (!menuButton()?.contains(target) && !menu()?.contains(target)) ctx?.setShowMenu(false); 109 111 }; 110 112 111 - onMount(() => window.addEventListener("click", clickEvent)); 112 - onCleanup(() => window.removeEventListener("click", clickEvent)); 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 + }); 113 129 114 130 return ( 115 131 <div class="relative"> ··· 119 135 props.buttonClass 120 136 } 121 137 ref={setMenuButton} 122 - onClick={() => ctx?.setShowMenu(!ctx?.showMenu())} 138 + onClick={() => { 139 + updatePosition(); 140 + ctx?.setShowMenu(!ctx?.showMenu()); 141 + }} 123 142 > 124 143 <span class={"iconify " + props.icon}></span> 125 144 </button> 126 145 <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 min-w-40 flex-col rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 shadow-md dark:border-neutral-700 " + 131 - props.menuClass 132 - } 133 - > 134 - {props.children} 135 - </div> 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> 136 162 </Show> 137 163 </div> 138 164 );
+1 -1
src/components/notification.tsx
··· 36 36 37 37 export const NotificationContainer = () => { 38 38 return ( 39 - <div class="pointer-events-none fixed bottom-4 left-4 z-50 flex flex-col gap-2"> 39 + <div class="pointer-events-none fixed bottom-4 left-4 z-60 flex flex-col gap-2"> 40 40 <For each={notifications}> 41 41 {(notification) => ( 42 42 <div
+1 -5
src/layout.tsx
··· 139 139 </Show> 140 140 <AccountManager /> 141 141 <MenuProvider> 142 - <DropdownMenu 143 - icon="lucide--menu text-lg" 144 - buttonClass="rounded-lg p-1.5" 145 - menuClass="top-11 text-sm" 146 - > 142 + <DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5"> 147 143 <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 148 144 <NavMenu href="/firehose" label="Firehose" icon="lucide--droplet" /> 149 145 <NavMenu href="/labels" label="Labels" icon="lucide--tags" />
+1 -5
src/views/pds.tsx
··· 145 145 <Tab tab="info" label="Info" /> 146 146 </div> 147 147 <MenuProvider> 148 - <DropdownMenu 149 - icon="lucide--ellipsis-vertical" 150 - buttonClass="rounded-sm p-1.5" 151 - menuClass="top-9 text-sm" 152 - > 148 + <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 153 149 <CopyMenu content={params.pds!} label="Copy PDS" icon="lucide--copy" /> 154 150 <NavMenu 155 151 href={`/firehose?instance=wss://${params.pds}`}
+1 -5
src/views/record.tsx
··· 414 414 </Modal> 415 415 </Show> 416 416 <MenuProvider> 417 - <DropdownMenu 418 - icon="lucide--ellipsis-vertical" 419 - buttonClass="rounded-sm p-1.5" 420 - menuClass="top-9 text-sm" 421 - > 417 + <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 422 418 <CopyMenu 423 419 content={JSON.stringify(record()?.value, null, 2)} 424 420 label="Copy record"
+1 -5
src/views/repo.tsx
··· 303 303 </Tooltip> 304 304 </Show> 305 305 <MenuProvider> 306 - <DropdownMenu 307 - icon="lucide--ellipsis-vertical" 308 - buttonClass="rounded-sm p-1.5" 309 - menuClass="top-9 text-sm" 310 - > 306 + <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 311 307 <CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" /> 312 308 <NavMenu 313 309 href={`/jetstream?dids=${params.repo}`}