Fork of atp.tools as a universal profile for people on the ATmosphere

add hover card, handle page collection sections

Natalie B a076577b 780922da

+1
package.json
··· 18 18 "@radix-ui/react-avatar": "^1.1.4", 19 19 "@radix-ui/react-dialog": "^1.1.7", 20 20 "@radix-ui/react-dropdown-menu": "^2.1.7", 21 + "@radix-ui/react-hover-card": "^1.1.11", 21 22 "@radix-ui/react-popover": "^1.1.7", 22 23 "@radix-ui/react-progress": "^1.1.3", 23 24 "@radix-ui/react-separator": "^1.1.3",
+120
pnpm-lock.yaml
··· 32 32 '@radix-ui/react-dropdown-menu': 33 33 specifier: ^2.1.7 34 34 version: 2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 35 + '@radix-ui/react-hover-card': 36 + specifier: ^1.1.11 37 + version: 1.1.11(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 35 38 '@radix-ui/react-popover': 36 39 specifier: ^1.1.7 37 40 version: 1.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) ··· 527 530 '@types/react-dom': 528 531 optional: true 529 532 533 + '@radix-ui/react-arrow@1.1.4': 534 + resolution: {integrity: sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==} 535 + peerDependencies: 536 + '@types/react': '*' 537 + '@types/react-dom': '*' 538 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 539 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 540 + peerDependenciesMeta: 541 + '@types/react': 542 + optional: true 543 + '@types/react-dom': 544 + optional: true 545 + 530 546 '@radix-ui/react-avatar@1.1.4': 531 547 resolution: {integrity: sha512-+kBesLBzwqyDiYCtYFK+6Ktf+N7+Y6QOTUueLGLIbLZ/YeyFW6bsBGDsN+5HxHpM55C90u5fxsg0ErxzXTcwKA==} 532 548 peerDependencies: ··· 632 648 '@types/react-dom': 633 649 optional: true 634 650 651 + '@radix-ui/react-dismissable-layer@1.1.7': 652 + resolution: {integrity: sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==} 653 + peerDependencies: 654 + '@types/react': '*' 655 + '@types/react-dom': '*' 656 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 657 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 658 + peerDependenciesMeta: 659 + '@types/react': 660 + optional: true 661 + '@types/react-dom': 662 + optional: true 663 + 635 664 '@radix-ui/react-dropdown-menu@2.1.7': 636 665 resolution: {integrity: sha512-7/1LiuNZuCQE3IzdicGoHdQOHkS2Q08+7p8w6TXZ6ZjgAULaCI85ZY15yPl4o4FVgoKLRT43/rsfNVN8osClQQ==} 637 666 peerDependencies: ··· 667 696 '@types/react-dom': 668 697 optional: true 669 698 699 + '@radix-ui/react-hover-card@1.1.11': 700 + resolution: {integrity: sha512-q9h9grUpGZKR3MNhtVCLVnPGmx1YnzBgGR+O40mhSNGsUnkR+LChVH8c7FB0mkS+oudhd8KAkZGTJPJCjdAPIg==} 701 + peerDependencies: 702 + '@types/react': '*' 703 + '@types/react-dom': '*' 704 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 705 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 706 + peerDependenciesMeta: 707 + '@types/react': 708 + optional: true 709 + '@types/react-dom': 710 + optional: true 711 + 670 712 '@radix-ui/react-id@1.1.1': 671 713 resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} 672 714 peerDependencies: ··· 715 757 '@types/react-dom': 716 758 optional: true 717 759 760 + '@radix-ui/react-popper@1.2.4': 761 + resolution: {integrity: sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==} 762 + peerDependencies: 763 + '@types/react': '*' 764 + '@types/react-dom': '*' 765 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 766 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 767 + peerDependenciesMeta: 768 + '@types/react': 769 + optional: true 770 + '@types/react-dom': 771 + optional: true 772 + 718 773 '@radix-ui/react-portal@1.1.5': 719 774 resolution: {integrity: sha512-ps/67ZqsFm+Mb6lSPJpfhRLrVL2i2fntgCmGMqqth4eaGUf+knAuuRtWVJrNjUhExgmdRqftSgzpf0DF0n6yXA==} 775 + peerDependencies: 776 + '@types/react': '*' 777 + '@types/react-dom': '*' 778 + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 779 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc 780 + peerDependenciesMeta: 781 + '@types/react': 782 + optional: true 783 + '@types/react-dom': 784 + optional: true 785 + 786 + '@radix-ui/react-portal@1.1.6': 787 + resolution: {integrity: sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==} 720 788 peerDependencies: 721 789 '@types/react': '*' 722 790 '@types/react-dom': '*' ··· 2545 2613 react: 19.0.0 2546 2614 react-dom: 19.0.0(react@19.0.0) 2547 2615 2616 + '@radix-ui/react-arrow@1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2617 + dependencies: 2618 + '@radix-ui/react-primitive': 2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2619 + react: 19.0.0 2620 + react-dom: 19.0.0(react@19.0.0) 2621 + 2548 2622 '@radix-ui/react-avatar@1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2549 2623 dependencies: 2550 2624 '@radix-ui/react-context': 1.1.2(react@19.0.0) ··· 2626 2700 react: 19.0.0 2627 2701 react-dom: 19.0.0(react@19.0.0) 2628 2702 2703 + '@radix-ui/react-dismissable-layer@1.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2704 + dependencies: 2705 + '@radix-ui/primitive': 1.1.2 2706 + '@radix-ui/react-compose-refs': 1.1.2(react@19.0.0) 2707 + '@radix-ui/react-primitive': 2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2708 + '@radix-ui/react-use-callback-ref': 1.1.1(react@19.0.0) 2709 + '@radix-ui/react-use-escape-keydown': 1.1.1(react@19.0.0) 2710 + react: 19.0.0 2711 + react-dom: 19.0.0(react@19.0.0) 2712 + 2629 2713 '@radix-ui/react-dropdown-menu@2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2630 2714 dependencies: 2631 2715 '@radix-ui/primitive': 1.1.2 ··· 2650 2734 react: 19.0.0 2651 2735 react-dom: 19.0.0(react@19.0.0) 2652 2736 2737 + '@radix-ui/react-hover-card@1.1.11(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2738 + dependencies: 2739 + '@radix-ui/primitive': 1.1.2 2740 + '@radix-ui/react-compose-refs': 1.1.2(react@19.0.0) 2741 + '@radix-ui/react-context': 1.1.2(react@19.0.0) 2742 + '@radix-ui/react-dismissable-layer': 1.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2743 + '@radix-ui/react-popper': 1.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2744 + '@radix-ui/react-portal': 1.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2745 + '@radix-ui/react-presence': 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2746 + '@radix-ui/react-primitive': 2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2747 + '@radix-ui/react-use-controllable-state': 1.2.2(react@19.0.0) 2748 + react: 19.0.0 2749 + react-dom: 19.0.0(react@19.0.0) 2750 + 2653 2751 '@radix-ui/react-id@1.1.1(react@19.0.0)': 2654 2752 dependencies: 2655 2753 '@radix-ui/react-use-layout-effect': 1.1.1(react@19.0.0) ··· 2713 2811 react: 19.0.0 2714 2812 react-dom: 19.0.0(react@19.0.0) 2715 2813 2814 + '@radix-ui/react-popper@1.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2815 + dependencies: 2816 + '@floating-ui/react-dom': 2.1.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2817 + '@radix-ui/react-arrow': 1.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2818 + '@radix-ui/react-compose-refs': 1.1.2(react@19.0.0) 2819 + '@radix-ui/react-context': 1.1.2(react@19.0.0) 2820 + '@radix-ui/react-primitive': 2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2821 + '@radix-ui/react-use-callback-ref': 1.1.1(react@19.0.0) 2822 + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.0.0) 2823 + '@radix-ui/react-use-rect': 1.1.1(react@19.0.0) 2824 + '@radix-ui/react-use-size': 1.1.1(react@19.0.0) 2825 + '@radix-ui/rect': 1.1.1 2826 + react: 19.0.0 2827 + react-dom: 19.0.0(react@19.0.0) 2828 + 2716 2829 '@radix-ui/react-portal@1.1.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2717 2830 dependencies: 2718 2831 '@radix-ui/react-primitive': 2.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2832 + '@radix-ui/react-use-layout-effect': 1.1.1(react@19.0.0) 2833 + react: 19.0.0 2834 + react-dom: 19.0.0(react@19.0.0) 2835 + 2836 + '@radix-ui/react-portal@1.1.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': 2837 + dependencies: 2838 + '@radix-ui/react-primitive': 2.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) 2719 2839 '@radix-ui/react-use-layout-effect': 1.1.1(react@19.0.0) 2720 2840 react: 19.0.0 2721 2841 react-dom: 19.0.0(react@19.0.0)
+2 -2
src/components/renderJson.tsx
··· 84 84 console.log("props.data", props.data); 85 85 return ( 86 86 <div style={{ marginLeft: `${20}px` }}> 87 - {props.data.$type}:{" "} 87 + <span className="text-muted-foreground">{props.data.$type}</span>:{" "} 88 88 <Component 89 89 did={props.did} 90 90 dollar_link={props.data?.ref?.$link || undefined} ··· 100 100 {Object.keys(props.data).map((k) => { 101 101 return ( 102 102 <div style={{ marginLeft: `${20}px` }}> 103 - {k}:{" "} 103 + <span className="text-muted-foreground">{k}</span>:{" "} 104 104 <RenderJson 105 105 data={props.data[k]} 106 106 depth={(props.depth ?? 0) + 1}
+27
src/components/ui/hover-card.tsx
··· 1 + import * as React from "react" 2 + import * as HoverCardPrimitive from "@radix-ui/react-hover-card" 3 + 4 + import { cn } from "@/lib/utils" 5 + 6 + const HoverCard = HoverCardPrimitive.Root 7 + 8 + const HoverCardTrigger = HoverCardPrimitive.Trigger 9 + 10 + const HoverCardContent = React.forwardRef< 11 + React.ElementRef<typeof HoverCardPrimitive.Content>, 12 + React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> 13 + >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 + <HoverCardPrimitive.Content 15 + ref={ref} 16 + align={align} 17 + sideOffset={sideOffset} 18 + className={cn( 19 + "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]", 20 + className 21 + )} 22 + {...props} 23 + /> 24 + )) 25 + HoverCardContent.displayName = HoverCardPrimitive.Content.displayName 26 + 27 + export { HoverCard, HoverCardTrigger, HoverCardContent }
+27 -17
src/routes/at:/$handle.index.tsx
··· 27 27 } from "@atcute/oauth-browser-client"; 28 28 import { createFileRoute, Link } from "@tanstack/react-router"; 29 29 import { AtSign } from "lucide-react"; 30 - import { useState, useEffect } from "preact/compat"; 30 + import { useState, useEffect, Fragment } from "preact/compat"; 31 31 32 32 interface RepoData { 33 33 data?: ComAtprotoRepoDescribeRepo.Output; ··· 242 242 href={`https://${identity.identity.pds.hostname}`} 243 243 target="_blank" 244 244 rel="noopener noreferrer" 245 - className="text-blue-500 hover:underline" 245 + className="text-blue-600 dark:text-blue-400 hover:underline" 246 246 > 247 247 {identity.identity.pds.hostname} 248 248 </a> ··· 252 252 <div className="pt-2"> 253 253 <h2 className="text-xl font-bold mb-1">Collections</h2> 254 254 <ul className="list-inside space-y-1"> 255 - {data.collections.map((c) => ( 256 - <li 257 - key={c} 258 - className="text-blue-500 hover:no-underline border-b hover:border-border border-transparent w-min" 259 - > 260 - <Link 261 - to="/at:/$handle/$collection" 262 - params={{ 263 - handle: handle, // Use original handle for navigation consistency 264 - collection: c, 265 - }} 266 - > 267 - {c} 268 - </Link> 269 - </li> 255 + {data.collections.map((c, i) => ( 256 + <Fragment key={c}> 257 + {c.split(".").slice(0, 2).join(".") != 258 + (i > 0 && 259 + data.collections[i - 1] 260 + .split(".") 261 + .slice(0, 2) 262 + .join(".")) && ( 263 + <div className="w-min pt-2"> 264 + {c.split(".").slice(0, 2).join(".")}{" "} 265 + </div> 266 + )} 267 + <li className="text-blue-600 dark:text-blue-400 hover:no-underline border-b hover:border-border border-transparent w-min"> 268 + <Link 269 + className="ml-4" 270 + to="/at:/$handle/$collection" 271 + params={{ 272 + handle: handle, // Use original handle for navigation consistency 273 + collection: c, 274 + }} 275 + > 276 + {c} 277 + </Link> 278 + </li> 279 + </Fragment> 270 280 ))} 271 281 </ul> 272 282 </div>
+163 -13
src/routes/at:/$handle/$collection.index.lazy.tsx
··· 2 2 import { Loader } from "@/components/ui/loader"; 3 3 import { useDocumentTitle } from "@/hooks/useDocumentTitle"; 4 4 import { QtClient } from "@/providers/qtprovider"; 5 - import { ComAtprotoRepoListRecords } from "@atcute/client/lexicons"; 5 + import { RenderJson } from "@/components/renderJson"; 6 + import { 7 + ComAtprotoRepoGetRecord, 8 + ComAtprotoRepoListRecords, 9 + } from "@atcute/client/lexicons"; 6 10 import { 7 11 IdentityMetadata, 8 12 resolveFromIdentity, 9 13 } from "@atcute/oauth-browser-client"; 10 14 import { createLazyFileRoute, Link } from "@tanstack/react-router"; 11 15 import { useEffect, useRef, useState } from "preact/hooks"; 16 + // Import HoverCard components 17 + import { 18 + HoverCard, 19 + HoverCardContent, 20 + HoverCardTrigger, 21 + } from "@/components/ui/hover-card"; 12 22 13 23 export const Route = createLazyFileRoute("/at:/$handle/$collection/")({ 14 24 component: RouteComponent, ··· 23 33 error: Error | null; 24 34 } 25 35 36 + // State to hold fetched data for the *currently* hovered card 37 + interface HoveredRecordState { 38 + uri: string | null; // Which URI is being hovered/fetched for 39 + data: ComAtprotoRepoGetRecord.Output | null; 40 + loading: boolean; 41 + error: Error | null; 42 + } 43 + 26 44 function useCollectionRecords( 27 45 handle: string, 28 46 collection: string, 29 47 ): CollectionRecords { 48 + // (Keep the existing useCollectionRecords hook as is) 30 49 const [state, setState] = useState<CollectionRecords>({ 31 50 isLoading: false, 32 51 error: null, ··· 100 119 useCollectionRecords(handle, collection); 101 120 const loaderRef = useRef<HTMLDivElement>(null); 102 121 122 + // State for the *single* actively fetched/displayed hover card 123 + const [hoveredRecordState, setHoveredRecordState] = 124 + useState<HoveredRecordState>({ 125 + uri: null, 126 + data: null, 127 + loading: false, 128 + error: null, 129 + }); 130 + // Ref to prevent fetching multiple times if hover is rapid 131 + const fetchTimeoutRef = useRef<number | null>(null); 132 + 103 133 useDocumentTitle(records ? `${collection} | atp.tools` : "atp.tools"); 104 134 135 + // Function to fetch single record data (triggered by HoverCard) 136 + const fetchHoverRecordData = async (recordUri: string) => { 137 + if (!identity || hoveredRecordState.uri === recordUri) return; // Don't refetch if already fetched/fetching for this URI 138 + 139 + // Clear previous fetch timeout if any 140 + if (fetchTimeoutRef.current) { 141 + clearTimeout(fetchTimeoutRef.current); 142 + } 143 + 144 + // Set loading state for the new URI 145 + setHoveredRecordState({ 146 + uri: recordUri, 147 + data: null, 148 + loading: true, 149 + error: null, 150 + }); 151 + 152 + // Use a timeout to delay the actual fetch slightly 153 + fetchTimeoutRef.current = window.setTimeout(async () => { 154 + try { 155 + const rpc = new QtClient(identity.pds); 156 + const recordParts = recordUri.replace("at://", "").split("/"); 157 + if (recordParts.length !== 3) throw new Error("Invalid record URI"); 158 + 159 + const response = await rpc 160 + .getXrpcClient() 161 + .get("com.atproto.repo.getRecord", { 162 + params: { 163 + repo: recordParts[0], 164 + collection: recordParts[1], 165 + rkey: recordParts[2], 166 + }, 167 + }); 168 + 169 + // Update state only if the URI still matches the one we started fetching for 170 + setHoveredRecordState( 171 + (prev) => 172 + prev.uri === recordUri 173 + ? { ...prev, data: response.data, loading: false, error: null } 174 + : prev, // Ignore if URI changed during fetch 175 + ); 176 + } catch (err: any) { 177 + console.error("Failed to fetch record on hover:", err); 178 + // Update state only if the URI still matches 179 + setHoveredRecordState( 180 + (prev) => 181 + prev.uri === recordUri 182 + ? { 183 + ...prev, 184 + data: null, 185 + loading: false, 186 + error: 187 + err instanceof Error 188 + ? err 189 + : new Error("Failed to fetch record"), 190 + } 191 + : prev, // Ignore if URI changed during fetch 192 + ); 193 + } finally { 194 + fetchTimeoutRef.current = null; 195 + } 196 + }, 150); // ~150ms delay before fetching starts 197 + }; 198 + 199 + const resetHoverState = () => { 200 + // Clear fetch timeout if card closes before fetch starts 201 + if (fetchTimeoutRef.current) { 202 + clearTimeout(fetchTimeoutRef.current); 203 + fetchTimeoutRef.current = null; 204 + } 205 + // Optionally reset state immediately, or let HoverCard handle closing visual 206 + // setHoveredRecordState({ uri: null, data: null, loading: false, error: null }); 207 + }; 208 + 105 209 useEffect(() => { 210 + // (Intersection Observer logic remains the same) 106 211 if (!loaderRef.current) return; 107 - 108 212 const observer = new IntersectionObserver( 109 213 (entries) => { 110 214 const target = entries[0]; ··· 114 218 }, 115 219 { threshold: 0.1, rootMargin: "50px" }, 116 220 ); 117 - 118 221 observer.observe(loaderRef.current); 119 222 return () => observer.disconnect(); 120 223 }, [cursor, isLoading, fetchMore]); ··· 128 231 } 129 232 130 233 return ( 234 + // No relative positioning needed on the parent here 131 235 <div className="flex flex-row justify-center w-full min-h-[calc(100vh-5rem)]"> 132 236 <div className="max-w-md lg:max-w-2xl w-[90vw] mx-4 md:mt-16 space-y-2"> 237 + {/* Header Link and PDS info */} 133 238 <Link 134 239 to="/at:/$handle" 135 240 params={{ handle: identity?.raw ?? "" }} ··· 149 254 150 255 <h2 className="text-2xl">{collection} collections:</h2> 151 256 <div> 152 - <ul> 257 + <ul className="list-none p-0 m-0"> 153 258 {records?.map((r) => ( 154 - <li key={r.uri} className="text-blue-500"> 155 - <Link 156 - to="/at:/$handle/$collection/$rkey" 157 - params={{ 158 - handle: handle, 159 - collection: collection, 160 - rkey: r.uri.split("/").pop() ?? "", 259 + <li key={r.uri} className="py-1"> 260 + {" "} 261 + {/* Remove hover styling/handlers from li */} 262 + <HoverCard 263 + openDelay={200} // Standard delay before opening 264 + closeDelay={100} // Standard delay before closing 265 + onOpenChange={(isOpen) => { 266 + if (isOpen) { 267 + fetchHoverRecordData(r.uri); 268 + } else { 269 + resetHoverState(); 270 + } 161 271 }} 162 272 > 163 - {r.uri.split("/").pop()} 164 - </Link> 273 + <HoverCardTrigger asChild> 274 + {/* The Link component itself triggers the hover card */} 275 + <Link 276 + className="text-blue-600 dark:text-blue-400 hover:underline" // Add underline on hover for affordance 277 + to="/at:/$handle/$collection/$rkey" 278 + params={{ 279 + handle: handle, // or identity?.raw ?? handle 280 + collection: collection, 281 + rkey: r.uri.split("/").pop() ?? "", 282 + }} 283 + > 284 + {r.uri.split("/").pop()} 285 + </Link> 286 + </HoverCardTrigger> 287 + <HoverCardContent 288 + className="w-auto max-w-lg max-h-96 overflow-auto text-xs" // Adjust width/styling as needed 289 + // Optional: Add side="top|bottom|left|right" align="start|center|end" for positioning 290 + side="right" 291 + align="start" 292 + > 293 + {/* Render content based on the shared hover state, *if* the URI matches */} 294 + {hoveredRecordState.uri === r.uri ? ( 295 + <> 296 + {hoveredRecordState.loading && <Loader />} 297 + {hoveredRecordState.error && ( 298 + <ShowError error={hoveredRecordState.error} /> 299 + )} 300 + {hoveredRecordState.data && identity && ( 301 + <RenderJson 302 + data={hoveredRecordState.data.value} 303 + did={identity.id} 304 + pds={identity.pds.toString()} 305 + /> 306 + )} 307 + </> 308 + ) : ( 309 + // Can show a mini-loader here too if desired while waiting for fetchHoverRecordData to set loading state 310 + <Loader /> 311 + )} 312 + </HoverCardContent> 313 + </HoverCard> 165 314 </li> 166 315 ))} 167 316 </ul> 168 317 318 + {/* Infinite scroll loader */} 169 319 <div 170 320 ref={loaderRef} 171 321 className="flex flex-row justify-center h-10 -pt-16"