+1
package.json
+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
+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
+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
+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
-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
+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"