atmosphere explorer pds.ls
tool typescript atproto

add PDS favicon

handle.invalid 8f534482 a7420ccf

verified
+90 -71
+1 -1
src/components/backlinks.tsx
··· 151 151 onClick={() => setExpanded(!expanded())} 152 152 > 153 153 <div class="flex min-w-0 flex-1 items-center gap-2"> 154 - <Favicon authority={authority()} /> 154 + <Favicon domain={authority()} reverse /> 155 155 <div class="flex min-w-0 flex-1 flex-col"> 156 156 <span class="w-full truncate">{props.collection}</span> 157 157 <span class="w-full text-xs wrap-break-word text-neutral-500 dark:text-neutral-400">
+3 -2
src/components/favicon.tsx
··· 1 1 import { createSignal, JSX, Match, Show, Switch } from "solid-js"; 2 2 3 3 export const Favicon = (props: { 4 - authority: string; 4 + domain: string; 5 + reverse?: boolean; 5 6 wrapper?: (children: JSX.Element) => JSX.Element; 6 7 }) => { 7 8 const [loaded, setLoaded] = createSignal(false); 8 - const domain = () => props.authority.split(".").reverse().join("."); 9 + const domain = () => (props.reverse ? props.domain.split(".").reverse().join(".") : props.domain); 9 10 10 11 const content = ( 11 12 <Switch>
+81 -58
src/components/navbar.tsx
··· 1 1 import * as TID from "@atcute/tid"; 2 2 import { A, Params } from "@solidjs/router"; 3 - import { createEffect, createMemo, createSignal, Show } from "solid-js"; 3 + import { createEffect, createMemo, createSignal, JSX, Match, Show, Switch } from "solid-js"; 4 4 import { canHover } from "../layout"; 5 5 import { didDocCache } from "../utils/api"; 6 6 import { addToClipboard } from "../utils/copy"; 7 7 import { localDateFromTimestamp } from "../utils/date"; 8 - import { Favicon } from "./favicon"; 9 8 import Tooltip from "./tooltip"; 10 9 11 10 export const [pds, setPDS] = createSignal<string>(); ··· 31 30 ); 32 31 }; 33 32 33 + const HoverFavicon = (props: { domain: string; hovered: boolean; children: JSX.Element }) => { 34 + const [hasHovered, setHasHovered] = createSignal(false); 35 + const [loaded, setLoaded] = createSignal(false); 36 + 37 + createEffect(() => { 38 + props.domain; 39 + setHasHovered(false); 40 + setLoaded(false); 41 + }); 42 + 43 + createEffect(() => { 44 + if (props.hovered) setHasHovered(true); 45 + }); 46 + 47 + return ( 48 + <div class="relative flex h-5 w-3.5 shrink-0 items-center justify-center sm:w-4"> 49 + <Show when={!props.hovered || !loaded()}>{props.children}</Show> 50 + <Show when={hasHovered()}> 51 + <Switch> 52 + <Match when={props.domain === "tangled.sh" || props.domain === "tangled.org"}> 53 + <span 54 + class="iconify i-tangled size-4" 55 + classList={{ hidden: !props.hovered || !loaded() }} 56 + ref={() => setLoaded(true)} 57 + /> 58 + </Match> 59 + <Match when={true}> 60 + <img 61 + src={ 62 + ["bsky.app", "bsky.chat"].includes(props.domain) ? 63 + "https://web-cdn.bsky.app/static/apple-touch-icon.png" 64 + : `https://${props.domain}/favicon.ico` 65 + } 66 + class="size-4" 67 + classList={{ hidden: !props.hovered || !loaded() }} 68 + onLoad={() => setLoaded(true)} 69 + onError={() => setLoaded(false)} 70 + /> 71 + </Match> 72 + </Switch> 73 + </Show> 74 + </div> 75 + ); 76 + }; 77 + 34 78 export const NavBar = (props: { params: Params }) => { 35 79 const [handle, setHandle] = createSignal(props.params.repo); 80 + const [pdsHovered, setPdsHovered] = createSignal(false); 36 81 const [repoHovered, setRepoHovered] = createSignal(false); 37 - const [hasHoveredRepo, setHasHoveredRepo] = createSignal(false); 38 - const [faviconLoaded, setFaviconLoaded] = createSignal(false); 39 82 const [collectionHovered, setCollectionHovered] = createSignal(false); 40 83 const isCustomDomain = () => handle() && !handle()!.endsWith(".bsky.social"); 41 84 ··· 49 92 } 50 93 }); 51 94 52 - createEffect(() => { 53 - handle(); 54 - setHasHoveredRepo(false); 55 - setFaviconLoaded(false); 56 - }); 57 - 58 95 const rkeyTimestamp = createMemo(() => { 59 96 if (!props.params.rkey || !TID.validate(props.params.rkey)) return undefined; 60 97 const timestamp = TID.parse(props.params.rkey).timestamp / 1000; ··· 64 101 return ( 65 102 <nav class="flex w-full flex-col text-sm wrap-anywhere sm:text-base"> 66 103 {/* PDS Level */} 67 - <div class="group relative flex items-center justify-between gap-1 rounded-md border-[0.5px] border-transparent bg-transparent px-2 transition-all duration-200 hover:border-neutral-300 hover:bg-neutral-50/40 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/40"> 104 + <div 105 + class="group relative flex items-center justify-between gap-1 rounded-md border-[0.5px] border-transparent bg-transparent px-2 transition-all duration-200 hover:border-neutral-300 hover:bg-neutral-50/40 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/40" 106 + onMouseEnter={() => { 107 + if (canHover) setPdsHovered(true); 108 + }} 109 + onMouseLeave={() => { 110 + if (canHover) setPdsHovered(false); 111 + }} 112 + > 68 113 <div class="flex min-h-6 basis-full items-center gap-2 sm:min-h-7"> 69 114 <Tooltip text="PDS"> 70 - <span 71 - classList={{ 72 - "iconify shrink-0 transition-colors duration-200": true, 73 - "lucide--unplug text-red-500 dark:text-red-400": 74 - pds() === "Missing PDS" && props.params.repo?.startsWith("did:"), 75 - "lucide--hard-drive text-neutral-500 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200": 76 - pds() !== "Missing PDS" || !props.params.repo?.startsWith("did:"), 77 - }} 78 - ></span> 115 + <HoverFavicon 116 + domain={pds() ?? ""} 117 + hovered={pdsHovered() && !!pds() && pds() !== "Missing PDS"} 118 + > 119 + <span 120 + classList={{ 121 + "iconify shrink-0 transition-colors duration-200": true, 122 + "lucide--unplug text-red-500 dark:text-red-400": 123 + pds() === "Missing PDS" && props.params.repo?.startsWith("did:"), 124 + "lucide--hard-drive text-neutral-500 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200": 125 + pds() !== "Missing PDS" || !props.params.repo?.startsWith("did:"), 126 + }} 127 + ></span> 128 + </HoverFavicon> 79 129 </Tooltip> 80 130 <Show when={pds() && (pds() !== "Missing PDS" || props.params.repo?.startsWith("did:"))}> 81 131 <Show ··· 110 160 <div 111 161 class="group relative flex items-center justify-between gap-1 rounded-md border-[0.5px] border-transparent bg-transparent px-2 transition-all duration-200 hover:border-neutral-300 hover:bg-neutral-50/40 dark:hover:border-neutral-600 dark:hover:bg-neutral-800/40" 112 162 onMouseEnter={() => { 113 - if (canHover) { 114 - setRepoHovered(true); 115 - setHasHoveredRepo(true); 116 - } 163 + if (canHover) setRepoHovered(true); 117 164 }} 118 165 onMouseLeave={() => { 119 - if (canHover) { 120 - setRepoHovered(false); 121 - } 166 + if (canHover) setRepoHovered(false); 122 167 }} 123 168 > 124 169 <div class="flex min-w-0 basis-full items-center gap-2"> 125 170 <Tooltip text="Repository"> 126 - <div class="relative flex h-5 w-3.5 shrink-0 items-center justify-center sm:w-4"> 127 - <span 128 - class="iconify lucide--book-user absolute text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200" 129 - classList={{ 130 - hidden: !!(repoHovered() && isCustomDomain() && faviconLoaded()), 131 - }} 132 - ></span> 133 - <Show when={hasHoveredRepo() && isCustomDomain()}> 134 - <img 135 - src={`https://${handle()}/favicon.ico`} 136 - class="size-4" 137 - classList={{ hidden: !repoHovered() || !faviconLoaded() }} 138 - onLoad={() => setFaviconLoaded(true)} 139 - onError={() => setFaviconLoaded(false)} 140 - /> 141 - </Show> 142 - </div> 171 + <HoverFavicon domain={handle() ?? ""} hovered={repoHovered() && !!isCustomDomain()}> 172 + <span class="iconify lucide--book-user text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span> 173 + </HoverFavicon> 143 174 </Tooltip> 144 175 <Show 145 176 when={props.params.collection} ··· 189 220 > 190 221 <div class="flex basis-full items-center gap-2"> 191 222 <Tooltip text="Collection"> 192 - <div class="relative flex h-5 w-3.5 shrink-0 items-center justify-center sm:w-4"> 193 - <Show 194 - when={collectionHovered()} 195 - fallback={ 196 - <span class="iconify lucide--folder-open text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span> 197 - } 198 - > 199 - {(() => { 200 - const parts = props.params.collection!.split("."); 201 - const authority = `${parts[0]}.${parts[1]}`; 202 - return <Favicon authority={authority} wrapper={(c) => c} />; 203 - })()} 204 - </Show> 205 - </div> 223 + <HoverFavicon 224 + domain={props.params.collection!.split(".").slice(0, 2).reverse().join(".")} 225 + hovered={collectionHovered()} 226 + > 227 + <span class="iconify lucide--folder-open text-neutral-500 transition-colors duration-200 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-200"></span> 228 + </HoverFavicon> 206 229 </Tooltip> 207 230 <Show 208 231 when={props.params.rkey}
+1 -1
src/views/car/explore.tsx
··· 437 437 }} 438 438 class="flex w-full items-center gap-2 rounded p-2 text-left text-sm hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-800 dark:active:bg-neutral-700" 439 439 > 440 - <Favicon authority={authority()} /> 440 + <Favicon domain={authority()} reverse /> 441 441 <span 442 442 class="truncate font-medium" 443 443 classList={{
+2 -8
src/views/record.tsx
··· 33 33 import { AtUri, uriTemplates } from "../utils/templates.js"; 34 34 import { lexicons } from "../utils/types/lexicons.js"; 35 35 36 - const toAuthority = (hostname: string) => hostname.split(".").reverse().join("."); 37 - 38 36 const faviconWrapper = (children: any) => ( 39 37 <div class="flex size-4 items-center justify-center">{children}</div> 40 38 ); ··· 493 491 }} 494 492 > 495 493 <Favicon 496 - authority={toAuthority(new URL(link().link).hostname)} 494 + domain={new URL(link().link).hostname} 497 495 wrapper={faviconWrapper} 498 496 /> 499 497 </a> ··· 512 510 > 513 511 {alt.icon ? 514 512 <img src={alt.icon} class="size-4" /> 515 - : <Favicon 516 - authority={toAuthority(alt.hostname)} 517 - wrapper={faviconWrapper} 518 - /> 519 - } 513 + : <Favicon domain={alt.hostname} wrapper={faviconWrapper} />} 520 514 </a> 521 515 )} 522 516 </For>
+2 -1
src/views/repo.tsx
··· 476 476 }} 477 477 > 478 478 <Favicon 479 - authority={authority} 479 + domain={authority} 480 + reverse 480 481 wrapper={(children) => ( 481 482 <a 482 483 href={`#collections:${authority}`}