atmosphere explorer
0
fork

Configure Feed

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

add did hover card

Juliet 1465e287 f3a337c5

+345 -261
+75
src/components/hover-card/base.tsx
··· 1 + import { A } from "@solidjs/router"; 2 + import { createEffect, createSignal, JSX, Show } from "solid-js"; 3 + import { isTouchDevice } from "../../layout"; 4 + 5 + interface HoverCardProps { 6 + /** Link href - if provided, renders an A tag */ 7 + href?: string; 8 + /** Link/trigger label text */ 9 + label?: string; 10 + /** Open link in new tab */ 11 + newTab?: boolean; 12 + /** Called when hover starts (for prefetching) */ 13 + onHover?: () => void; 14 + /** Custom trigger element - if provided, overrides href/label */ 15 + trigger?: JSX.Element; 16 + /** Additional classes for the wrapper span */ 17 + class?: string; 18 + /** Additional classes for the preview container */ 19 + previewClass?: string; 20 + /** Preview content */ 21 + children: JSX.Element; 22 + } 23 + 24 + const HoverCard = (props: HoverCardProps) => { 25 + const [show, setShow] = createSignal(false); 26 + 27 + const [previewHeight, setPreviewHeight] = createSignal(0); 28 + let anchorRef!: HTMLSpanElement; 29 + let previewRef!: HTMLDivElement; 30 + 31 + createEffect(() => { 32 + if (show() && previewRef) setPreviewHeight(previewRef.offsetHeight); 33 + }); 34 + 35 + const isOverflowing = (previewHeight: number) => 36 + anchorRef && anchorRef.offsetTop - window.scrollY + previewHeight + 32 > window.innerHeight; 37 + 38 + const handleMouseEnter = () => { 39 + props.onHover?.(); 40 + setShow(true); 41 + }; 42 + 43 + const handleMouseLeave = () => { 44 + setShow(false); 45 + }; 46 + 47 + return ( 48 + <span 49 + ref={anchorRef} 50 + class={`group/hover-card relative ${props.class || "inline"}`} 51 + onMouseEnter={handleMouseEnter} 52 + onMouseLeave={handleMouseLeave} 53 + > 54 + {props.trigger ?? ( 55 + <A 56 + class="text-blue-500 hover:underline active:underline dark:text-blue-400" 57 + href={props.href!} 58 + target={props.newTab ? "_blank" : "_self"} 59 + > 60 + {props.label} 61 + </A> 62 + )} 63 + <Show when={show() && !isTouchDevice}> 64 + <div 65 + ref={previewRef} 66 + class={`dark:bg-dark-300 dark:shadow-dark-700 pointer-events-none absolute left-[50%] z-50 block -translate-x-1/2 overflow-hidden rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 shadow-md dark:border-neutral-700 ${props.previewClass || ""} ${isOverflowing(previewHeight()) ? "bottom-7" : "top-7"}`} 67 + > 68 + {props.children} 69 + </div> 70 + </Show> 71 + </span> 72 + ); 73 + }; 74 + 75 + export default HoverCard;
+100
src/components/hover-card/did.tsx
··· 1 + import { getPdsEndpoint, type DidDocument } from "@atcute/identity"; 2 + import { createSignal, Show } from "solid-js"; 3 + import { resolveDidDoc } from "../../utils/api"; 4 + import HoverCard from "./base"; 5 + 6 + interface DidHoverCardProps { 7 + did: string; 8 + newTab?: boolean; 9 + } 10 + 11 + interface DidInfo { 12 + handle?: string; 13 + pds?: string; 14 + loading: boolean; 15 + error?: string; 16 + } 17 + 18 + const didCache = new Map<string, DidInfo>(); 19 + 20 + const prefetchDid = async (did: string) => { 21 + if (didCache.has(did)) return; 22 + 23 + didCache.set(did, { loading: true }); 24 + 25 + try { 26 + const doc: DidDocument = await resolveDidDoc(did as `did:${string}:${string}`); 27 + 28 + const handle = doc.alsoKnownAs?.find((aka) => aka.startsWith("at://"))?.replace("at://", ""); 29 + 30 + const pds = getPdsEndpoint(doc)?.replace("https://", "").replace("http://", ""); 31 + 32 + didCache.set(did, { handle, pds, loading: false }); 33 + } catch (err: any) { 34 + didCache.set(did, { loading: false, error: err.message || "Failed to resolve" }); 35 + } 36 + }; 37 + 38 + const DidHoverCard = (props: DidHoverCardProps) => { 39 + const [didInfo, setDidInfo] = createSignal<DidInfo | null>(null); 40 + 41 + const handlePrefetch = () => { 42 + prefetchDid(props.did); 43 + 44 + const cached = didCache.get(props.did); 45 + setDidInfo(cached || { loading: true }); 46 + 47 + if (!cached || cached.loading) { 48 + const pollInterval = setInterval(() => { 49 + const updated = didCache.get(props.did); 50 + if (updated && !updated.loading) { 51 + setDidInfo(updated); 52 + clearInterval(pollInterval); 53 + } 54 + }, 100); 55 + 56 + setTimeout(() => clearInterval(pollInterval), 10000); 57 + } 58 + }; 59 + 60 + return ( 61 + <HoverCard 62 + href={`/at://${props.did}`} 63 + label={props.did} 64 + newTab={props.newTab} 65 + onHover={handlePrefetch} 66 + previewClass="w-max max-w-xs font-sans text-sm" 67 + > 68 + <Show when={didInfo()?.loading}> 69 + <div class="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400"> 70 + <span class="iconify lucide--loader-circle animate-spin" /> 71 + Loading... 72 + </div> 73 + </Show> 74 + <Show when={didInfo()?.error}> 75 + <div class="text-sm text-red-500 dark:text-red-400">{didInfo()?.error}</div> 76 + </Show> 77 + <Show when={!didInfo()?.loading && !didInfo()?.error}> 78 + <div class="flex flex-col gap-1"> 79 + <Show when={didInfo()?.handle}> 80 + <div class="flex items-center gap-2"> 81 + <span class="iconify lucide--at-sign text-neutral-500 dark:text-neutral-400" /> 82 + <span>{didInfo()?.handle}</span> 83 + </div> 84 + </Show> 85 + <Show when={didInfo()?.pds}> 86 + <div class="flex items-center gap-2"> 87 + <span class="iconify lucide--hard-drive text-neutral-500 dark:text-neutral-400" /> 88 + <span>{didInfo()?.pds}</span> 89 + </div> 90 + </Show> 91 + <Show when={!didInfo()?.handle && !didInfo()?.pds}> 92 + <div class="text-neutral-500 dark:text-neutral-400">No info available</div> 93 + </Show> 94 + </div> 95 + </Show> 96 + </HoverCard> 97 + ); 98 + }; 99 + 100 + export default DidHoverCard;
+112
src/components/hover-card/record.tsx
··· 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 + import { ActorIdentifier } from "@atcute/lexicons"; 3 + import { createSignal, Show } from "solid-js"; 4 + import { getPDS } from "../../utils/api"; 5 + import { JSONValue } from "../json"; 6 + import HoverCard from "./base"; 7 + 8 + interface RecordHoverCardProps { 9 + uri: string; 10 + newTab?: boolean; 11 + } 12 + 13 + const recordCache = new Map<string, { value: unknown; loading: boolean; error?: string }>(); 14 + 15 + const parseAtUri = (uri: string) => { 16 + const match = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/); 17 + if (!match) return null; 18 + return { repo: match[1], collection: match[2], rkey: match[3] }; 19 + }; 20 + 21 + const prefetchRecord = async (uri: string) => { 22 + if (recordCache.has(uri)) return; 23 + 24 + const parsed = parseAtUri(uri); 25 + if (!parsed) return; 26 + 27 + recordCache.set(uri, { value: null, loading: true }); 28 + 29 + try { 30 + const pds = await getPDS(parsed.repo); 31 + const rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 32 + const res = await rpc.get("com.atproto.repo.getRecord", { 33 + params: { 34 + repo: parsed.repo as ActorIdentifier, 35 + collection: parsed.collection as `${string}.${string}.${string}`, 36 + rkey: parsed.rkey, 37 + }, 38 + }); 39 + 40 + if (!res.ok) { 41 + recordCache.set(uri, { value: null, loading: false, error: res.data.error }); 42 + return; 43 + } 44 + 45 + recordCache.set(uri, { value: res.data.value, loading: false }); 46 + } catch (err: any) { 47 + recordCache.set(uri, { value: null, loading: false, error: err.message || "Failed to fetch" }); 48 + } 49 + }; 50 + 51 + const RecordHoverCard = (props: RecordHoverCardProps) => { 52 + const [record, setRecord] = createSignal<{ 53 + value: unknown; 54 + loading: boolean; 55 + error?: string; 56 + } | null>(null); 57 + 58 + const parsed = () => parseAtUri(props.uri); 59 + 60 + const handlePrefetch = () => { 61 + prefetchRecord(props.uri); 62 + 63 + // Start polling for cache updates 64 + const cached = recordCache.get(props.uri); 65 + setRecord(cached || { value: null, loading: true }); 66 + 67 + if (!cached || cached.loading) { 68 + const pollInterval = setInterval(() => { 69 + const updated = recordCache.get(props.uri); 70 + if (updated && !updated.loading) { 71 + setRecord(updated); 72 + clearInterval(pollInterval); 73 + } 74 + }, 100); 75 + 76 + setTimeout(() => clearInterval(pollInterval), 10000); 77 + } 78 + }; 79 + 80 + return ( 81 + <HoverCard 82 + href={`/${props.uri}`} 83 + label={props.uri} 84 + newTab={props.newTab} 85 + onHover={handlePrefetch} 86 + previewClass="max-h-80 w-max max-w-sm text-xs whitespace-pre-wrap sm:max-h-112 lg:max-w-lg" 87 + > 88 + <Show when={record()?.loading}> 89 + <div class="flex items-center gap-2 font-sans text-sm text-neutral-500 dark:text-neutral-400"> 90 + <span class="iconify lucide--loader-circle animate-spin" /> 91 + Loading... 92 + </div> 93 + </Show> 94 + <Show when={record()?.error}> 95 + <div class="font-sans text-sm text-red-500 dark:text-red-400">{record()?.error}</div> 96 + </Show> 97 + <Show when={record()?.value && !record()?.loading}> 98 + <div class="font-mono text-xs whitespace-pre-wrap"> 99 + <JSONValue 100 + data={record()?.value as any} 101 + repo={parsed()?.repo || ""} 102 + truncate 103 + newTab 104 + hideBlobs 105 + /> 106 + </div> 107 + </Show> 108 + </HoverCard> 109 + ); 110 + }; 111 + 112 + export default RecordHoverCard;
+3 -8
src/components/json.tsx
··· 13 13 import { resolveLexiconAuthority } from "../utils/api"; 14 14 import { formatFileSize } from "../utils/format"; 15 15 import { hideMedia } from "../views/settings"; 16 + import DidHoverCard from "./hover-card/did"; 17 + import RecordHoverCard from "./hover-card/record"; 16 18 import { pds } from "./navbar"; 17 19 import { addNotification, removeNotification } from "./notification"; 18 - import RecordHoverCard from "./record-hover-card"; 19 20 import VideoPlayer from "./video-player"; 20 21 21 22 interface JSONContext { ··· 85 86 {isResourceUri(part) ? 86 87 <RecordHoverCard uri={part} newTab={ctx.newTab} /> 87 88 : isDid(part) ? 88 - <A 89 - class="text-blue-500 hover:underline active:underline dark:text-blue-400" 90 - href={`/at://${part}`} 91 - target={ctx.newTab ? "_blank" : "_self"} 92 - > 93 - {part} 94 - </A> 89 + <DidHoverCard did={part} newTab={ctx.newTab} /> 95 90 : isNsid(part.split("#")[0]) && props.isType ? 96 91 <button 97 92 type="button"
-159
src/components/record-hover-card.tsx
··· 1 - import { Client, simpleFetchHandler } from "@atcute/client"; 2 - import { ActorIdentifier } from "@atcute/lexicons"; 3 - import { A } from "@solidjs/router"; 4 - import { createEffect, createSignal, onCleanup, Show } from "solid-js"; 5 - import { isTouchDevice } from "../layout"; 6 - import { getPDS } from "../utils/api"; 7 - import { JSONValue } from "./json"; 8 - 9 - interface RecordHoverCardProps { 10 - uri: string; 11 - newTab?: boolean; 12 - } 13 - 14 - const recordCache = new Map<string, { value: unknown; loading: boolean; error?: string }>(); 15 - 16 - const parseAtUri = (uri: string) => { 17 - const match = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/); 18 - if (!match) return null; 19 - return { repo: match[1], collection: match[2], rkey: match[3] }; 20 - }; 21 - 22 - const prefetchRecord = async (uri: string) => { 23 - if (recordCache.has(uri)) return; 24 - 25 - const parsed = parseAtUri(uri); 26 - if (!parsed) return; 27 - 28 - recordCache.set(uri, { value: null, loading: true }); 29 - 30 - try { 31 - const pds = await getPDS(parsed.repo); 32 - const rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 33 - const res = await rpc.get("com.atproto.repo.getRecord", { 34 - params: { 35 - repo: parsed.repo as ActorIdentifier, 36 - collection: parsed.collection as `${string}.${string}.${string}`, 37 - rkey: parsed.rkey, 38 - }, 39 - }); 40 - 41 - if (!res.ok) { 42 - recordCache.set(uri, { value: null, loading: false, error: res.data.error }); 43 - return; 44 - } 45 - 46 - recordCache.set(uri, { value: res.data.value, loading: false }); 47 - } catch (err: any) { 48 - recordCache.set(uri, { value: null, loading: false, error: err.message || "Failed to fetch" }); 49 - } 50 - }; 51 - 52 - const RecordHoverCard = (props: RecordHoverCardProps) => { 53 - const [show, setShow] = createSignal(false); 54 - const [record, setRecord] = createSignal<{ 55 - value: unknown; 56 - loading: boolean; 57 - error?: string; 58 - } | null>(null); 59 - let hoverTimeout: ReturnType<typeof setTimeout> | undefined; 60 - let hideTimeout: ReturnType<typeof setTimeout> | undefined; 61 - 62 - const [previewHeight, setPreviewHeight] = createSignal(0); 63 - let rkeyRef!: HTMLSpanElement; 64 - let previewRef!: HTMLDivElement; 65 - 66 - createEffect(() => { 67 - if (show() && previewRef) setPreviewHeight(previewRef.offsetHeight); 68 - }); 69 - 70 - const isOverflowing = (previewHeight: number) => 71 - rkeyRef && rkeyRef.offsetTop - window.scrollY + previewHeight + 32 > window.innerHeight; 72 - 73 - const parsed = () => parseAtUri(props.uri); 74 - 75 - const handleMouseEnter = () => { 76 - clearTimeout(hideTimeout); 77 - 78 - prefetchRecord(props.uri); 79 - 80 - hoverTimeout = setTimeout(() => { 81 - const cached = recordCache.get(props.uri); 82 - setRecord(cached || { value: null, loading: true }); 83 - setShow(true); 84 - 85 - // Poll for updates while loading 86 - if (cached?.loading) { 87 - const pollInterval = setInterval(() => { 88 - const updated = recordCache.get(props.uri); 89 - if (updated && !updated.loading) { 90 - setRecord(updated); 91 - clearInterval(pollInterval); 92 - } 93 - }, 100); 94 - 95 - setTimeout(() => clearInterval(pollInterval), 10000); 96 - } 97 - }, 200); 98 - }; 99 - 100 - const handleMouseLeave = () => { 101 - clearTimeout(hoverTimeout); 102 - hideTimeout = setTimeout(() => { 103 - setShow(false); 104 - }, 150); 105 - }; 106 - 107 - onCleanup(() => { 108 - clearTimeout(hoverTimeout); 109 - clearTimeout(hideTimeout); 110 - }); 111 - 112 - return ( 113 - <span 114 - ref={rkeyRef} 115 - class="group/hover-card relative inline" 116 - onMouseEnter={handleMouseEnter} 117 - onMouseLeave={handleMouseLeave} 118 - > 119 - <A 120 - class="text-blue-500 hover:underline active:underline dark:text-blue-400" 121 - href={`/${props.uri}`} 122 - target={props.newTab ? "_blank" : "_self"} 123 - > 124 - {props.uri} 125 - </A> 126 - <Show when={show() && !isTouchDevice}> 127 - <div 128 - ref={previewRef} 129 - class={`dark:bg-dark-300 dark:shadow-dark-700 pointer-events-none absolute left-[50%] z-50 block max-h-80 w-max max-w-sm -translate-x-1/2 overflow-hidden rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-xs whitespace-pre-wrap shadow-md sm:max-h-112 lg:max-w-lg dark:border-neutral-700 ${isOverflowing(previewHeight()) ? "bottom-7" : "top-7"}`} 130 - onMouseEnter={() => clearTimeout(hideTimeout)} 131 - onMouseLeave={handleMouseLeave} 132 - > 133 - <Show when={record()?.loading}> 134 - <div class="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400"> 135 - <span class="iconify lucide--loader-circle animate-spin" /> 136 - Loading... 137 - </div> 138 - </Show> 139 - <Show when={record()?.error}> 140 - <div class="text-sm text-red-500 dark:text-red-400">{record()?.error}</div> 141 - </Show> 142 - <Show when={record()?.value && !record()?.loading}> 143 - <div class="font-mono text-xs whitespace-pre-wrap"> 144 - <JSONValue 145 - data={record()?.value as any} 146 - repo={parsed()?.repo || ""} 147 - truncate 148 - newTab 149 - hideBlobs 150 - /> 151 - </div> 152 - </Show> 153 - </div> 154 - </Show> 155 - </span> 156 - ); 157 - }; 158 - 159 - export default RecordHoverCard;
+30 -42
src/views/car/explore.tsx
··· 17 17 } from "solid-js"; 18 18 import { Button } from "../../components/button.jsx"; 19 19 import { Favicon } from "../../components/favicon.jsx"; 20 + import HoverCard from "../../components/hover-card/base"; 20 21 import { JSONValue } from "../../components/json.jsx"; 21 22 import { TextInput } from "../../components/text-input.jsx"; 22 - import { isTouchDevice } from "../../layout.jsx"; 23 23 import { didDocCache, resolveDidDoc } from "../../utils/api.js"; 24 24 import { localDateFromTimestamp } from "../../utils/date.js"; 25 25 import { createDebouncedValue } from "../../utils/hooks/debounced.js"; ··· 516 516 {(entry) => { 517 517 const isTid = TID.validate(entry.key); 518 518 const timestamp = isTid ? TID.parse(entry.key).timestamp / 1_000 : null; 519 - const [hover, setHover] = createSignal(false); 520 - const [previewHeight, setPreviewHeight] = createSignal(0); 521 - let rkeyRef!: HTMLButtonElement; 522 - let previewRef!: HTMLSpanElement; 523 - 524 - createEffect(() => { 525 - if (hover() && previewRef) setPreviewHeight(previewRef.offsetHeight); 526 - }); 527 - 528 - const isOverflowing = (previewHeight: number) => 529 - rkeyRef.offsetTop - window.scrollY + previewHeight + 32 > window.innerHeight; 530 519 531 520 return ( 532 - <button 533 - onClick={() => { 534 - props.onRoute({ 535 - type: "record", 536 - collection: props.collection, 537 - record: entry, 538 - }); 539 - }} 540 - ref={rkeyRef} 541 - onmouseover={() => !isTouchDevice && setHover(true)} 542 - onmouseleave={() => !isTouchDevice && setHover(false)} 543 - class="relative flex w-full items-baseline gap-1 rounded px-1 py-0.5 text-left hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 544 - > 545 - <span class="shrink-0 text-sm text-blue-500 dark:text-blue-400">{entry.key}</span> 546 - <span class="truncate text-xs text-neutral-500 dark:text-neutral-400" dir="rtl"> 547 - {entry.cid} 548 - </span> 549 - <Show when={timestamp}> 550 - {(ts) => ( 551 - <span class="ml-auto shrink-0 text-xs">{localDateFromTimestamp(ts())}</span> 552 - )} 553 - </Show> 554 - <Show when={hover()}> 555 - <span 556 - ref={previewRef} 557 - class={`dark:bg-dark-300 dark:shadow-dark-700 pointer-events-none absolute left-[50%] z-25 block max-h-80 w-max max-w-sm -translate-x-1/2 overflow-hidden rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-xs whitespace-pre-wrap shadow-md sm:max-h-112 lg:max-w-lg dark:border-neutral-700 ${isOverflowing(previewHeight()) ? "bottom-7" : "top-7"}`} 521 + <HoverCard 522 + class="flex w-full items-baseline gap-1 rounded px-1 py-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 523 + trigger={ 524 + <button 525 + onClick={() => { 526 + props.onRoute({ 527 + type: "record", 528 + collection: props.collection, 529 + record: entry, 530 + }); 531 + }} 532 + class="flex w-full items-baseline gap-1 text-left" 558 533 > 559 - <JSONValue data={entry.record} repo={props.archive.did} truncate hideBlobs /> 560 - </span> 561 - </Show> 562 - </button> 534 + <span class="shrink-0 text-sm text-blue-500 dark:text-blue-400"> 535 + {entry.key} 536 + </span> 537 + <span class="truncate text-xs text-neutral-500 dark:text-neutral-400" dir="rtl"> 538 + {entry.cid} 539 + </span> 540 + <Show when={timestamp}> 541 + {(ts) => ( 542 + <span class="ml-auto shrink-0 text-xs">{localDateFromTimestamp(ts())}</span> 543 + )} 544 + </Show> 545 + </button> 546 + } 547 + previewClass="max-h-80 w-max max-w-sm text-xs whitespace-pre-wrap sm:max-h-112 lg:max-w-lg" 548 + > 549 + <JSONValue data={entry.record} repo={props.archive.did} truncate hideBlobs /> 550 + </HoverCard> 563 551 ); 564 552 }} 565 553 </For>
+25 -52
src/views/collection.tsx
··· 4 4 import * as TID from "@atcute/tid"; 5 5 import { Title } from "@solidjs/meta"; 6 6 import { A, useBeforeLeave, useParams, useSearchParams } from "@solidjs/router"; 7 - import { 8 - createEffect, 9 - createMemo, 10 - createResource, 11 - createSignal, 12 - For, 13 - onMount, 14 - Show, 15 - } from "solid-js"; 7 + import { createMemo, createResource, createSignal, For, onMount, Show } from "solid-js"; 16 8 import { createStore } from "solid-js/store"; 17 9 import { hasUserScope } from "../auth/scope-utils"; 18 10 import { agent } from "../auth/state"; 19 11 import { Button } from "../components/button.jsx"; 12 + import HoverCard from "../components/hover-card/base"; 20 13 import { JSONType, JSONValue } from "../components/json.jsx"; 21 14 import { Modal } from "../components/modal.jsx"; 22 15 import { addNotification, removeNotification } from "../components/notification.jsx"; 23 16 import { StickyOverlay } from "../components/sticky.jsx"; 24 17 import { TextInput } from "../components/text-input.jsx"; 25 18 import Tooltip from "../components/tooltip.jsx"; 26 - import { isTouchDevice } from "../layout.jsx"; 27 19 import { resolvePDS } from "../utils/api.js"; 28 20 import { localDateFromTimestamp } from "../utils/date.js"; 29 21 import { ··· 43 35 const DEFAULT_LIMIT = 100; 44 36 45 37 const RecordLink = (props: { record: AtprotoRecord }) => { 46 - const [hover, setHover] = createSignal(false); 47 - const [previewHeight, setPreviewHeight] = createSignal(0); 48 - let rkeyRef!: HTMLSpanElement; 49 - let previewRef!: HTMLSpanElement; 50 - 51 - createEffect(() => { 52 - if (hover() && previewRef) setPreviewHeight(previewRef.offsetHeight); 53 - }); 54 - 55 - const isOverflowing = (previewHeight: number) => 56 - rkeyRef.offsetTop - window.scrollY + previewHeight + 32 > window.innerHeight; 57 - 58 38 return ( 59 - <span 60 - class="relative flex w-full min-w-0 items-baseline rounded px-1 py-0.5" 61 - ref={rkeyRef} 62 - onmouseover={() => !isTouchDevice && setHover(true)} 63 - onmouseleave={() => !isTouchDevice && setHover(false)} 39 + <HoverCard 40 + class="flex w-full min-w-0 items-baseline rounded px-1 py-0.5" 41 + trigger={ 42 + <> 43 + <span class="shrink-0 text-sm text-blue-500 dark:text-blue-400">{props.record.rkey}</span> 44 + <span class="ml-1 truncate text-xs text-neutral-500 dark:text-neutral-400" dir="rtl"> 45 + {props.record.cid} 46 + </span> 47 + <Show when={props.record.timestamp && props.record.timestamp <= Date.now()}> 48 + <span class="ml-1 shrink-0 text-xs"> 49 + {localDateFromTimestamp(props.record.timestamp!)} 50 + </span> 51 + </Show> 52 + </> 53 + } 54 + previewClass="max-h-80 w-max max-w-sm text-xs whitespace-pre-wrap sm:max-h-112 lg:max-w-lg" 64 55 > 65 - <span class="flex items-baseline truncate"> 66 - <span class="shrink-0 text-sm text-blue-500 dark:text-blue-400">{props.record.rkey}</span> 67 - <span class="ml-1 truncate text-xs text-neutral-500 dark:text-neutral-400" dir="rtl"> 68 - {props.record.cid} 69 - </span> 70 - <Show when={props.record.timestamp && props.record.timestamp <= Date.now()}> 71 - <span class="ml-1 shrink-0 text-xs"> 72 - {localDateFromTimestamp(props.record.timestamp!)} 73 - </span> 74 - </Show> 75 - </span> 76 - <Show when={hover()}> 77 - <span 78 - ref={previewRef} 79 - class={`dark:bg-dark-300 dark:shadow-dark-700 pointer-events-none absolute left-[50%] z-25 block max-h-80 w-max max-w-sm -translate-x-1/2 overflow-hidden rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-xs whitespace-pre-wrap shadow-md sm:max-h-112 lg:max-w-lg dark:border-neutral-700 ${isOverflowing(previewHeight()) ? "bottom-7" : "top-7"}`} 80 - > 81 - <JSONValue 82 - data={props.record.record.value as JSONType} 83 - repo={props.record.record.uri.split("/")[2]} 84 - truncate 85 - hideBlobs 86 - /> 87 - </span> 88 - </Show> 89 - </span> 56 + <JSONValue 57 + data={props.record.record.value as JSONType} 58 + repo={props.record.record.uri.split("/")[2]} 59 + truncate 60 + hideBlobs 61 + /> 62 + </HoverCard> 90 63 ); 91 64 }; 92 65