diff --git a/src/components/RecordPreview.tsx b/src/components/RecordPreview.tsx
index 729425c..9858d6c 100644
--- a/src/components/RecordPreview.tsx
+++ b/src/components/RecordPreview.tsx
@@ -18,9 +18,22 @@ type RecordPreviewProps = {
collection: string;
handle: string;
rkey: string;
+ /**
+ * When true, suppress the "View Full Record" JSON-modal button and the
+ * "Open in Explorer" cross-link. Used inside the explorer's record view,
+ * which already shows the raw (linkified) JSON inline and is itself the
+ * Explorer destination.
+ */
+ hideExplorerCtas?: boolean;
};
-export default function RecordPreview({ record, collection, handle, rkey }: RecordPreviewProps) {
+export default function RecordPreview({
+ record,
+ collection,
+ handle,
+ rkey,
+ hideExplorerCtas,
+}: RecordPreviewProps) {
const { value, cid } = record;
const [showJsonModal, setShowJsonModal] = useState(false);
@@ -183,39 +196,44 @@ export default function RecordPreview({ record, collection, handle, rkey }: Reco
))}
- {/* View Full Record Button */}
-
+ {/* View Full Record Button — hidden when used inside the explorer,
+ which renders the linkified raw JSON inline immediately below. */}
+ {!hideExplorerCtas && (
+
+ )}
- {/* Cross-product: jump into the same record inside the explorer */}
+ {/* Cross-product: jump into the same record inside the explorer —
+ suppressed when we're already on an explorer page. */}
+ {!hideExplorerCtas && (
Open in Explorer — backlinks, identity, raw JSON →
+ )}
{/* Footer: CID */}
diff --git a/src/components/explore/Breadcrumb.tsx b/src/components/explore/Breadcrumb.tsx
index bfc912d..6767633 100644
--- a/src/components/explore/Breadcrumb.tsx
+++ b/src/components/explore/Breadcrumb.tsx
@@ -4,6 +4,7 @@ import Link from 'next/link';
import { ChevronRight, Server } from 'lucide-react';
import { encodeRepo } from '@/utils/atproto/urls';
import { pdsHostname } from '@/utils/atproto/pdsServer';
+import ShareLinkChip from './ShareLinkChip';
type Props = {
handle: string | null;
@@ -12,6 +13,11 @@ type Props = {
pds?: string;
collection?: string;
rkey?: string;
+ /**
+ * When provided, renders a "Copy link" chip at the end of the breadcrumb
+ * row. Path or full URL; bare paths get aturi.to prepended.
+ */
+ shareUrl?: string;
};
/**
@@ -22,7 +28,14 @@ type Props = {
* Each upstream segment is clickable. The PDS segment is optional so older
* call sites without a resolved PDS still render correctly.
*/
-export default function Breadcrumb({ handle, did, pds, collection, rkey }: Props) {
+export default function Breadcrumb({
+ handle,
+ did,
+ pds,
+ collection,
+ rkey,
+ shareUrl,
+}: Props) {
const repoSegment = encodeRepo(handle || did);
const repoLabel = handle ? `@${handle}` : did;
const pdsHost = pds ? pdsHostname(pds) : null;
@@ -112,6 +125,12 @@ export default function Breadcrumb({ handle, did, pds, collection, rkey }: Props
{rkey}
>
)}
+
+ {shareUrl && (
+
+
+
+ )}
);
}
diff --git a/src/components/explore/CollectionExplorer.tsx b/src/components/explore/CollectionExplorer.tsx
index 25986a2..7502ee2 100644
--- a/src/components/explore/CollectionExplorer.tsx
+++ b/src/components/explore/CollectionExplorer.tsx
@@ -132,6 +132,8 @@ function CollectionList({
did={identity.did}
pds={identity.pds}
collection={collection}
+ // No universal link route for collections — share the explorer URL.
+ shareUrl={`/explore/${repoSeg}/${collection}`}
/>
diff --git a/src/components/explore/PdsExplorer.tsx b/src/components/explore/PdsExplorer.tsx
index 4018c9e..bbe5734 100644
--- a/src/components/explore/PdsExplorer.tsx
+++ b/src/components/explore/PdsExplorer.tsx
@@ -14,6 +14,7 @@ import { describeRepo } from '@/utils/atproto/pdsClient';
import { encodeRepo, shortDid } from '@/utils/atproto/urls';
import AppearIn from './AppearIn';
import CopyButton from './CopyButton';
+import ShareLinkChip from './ShareLinkChip';
type Props = {
host: string;
@@ -190,18 +191,29 @@ function PdsHeader({
>
-
- Personal Data Server
+
+
+ Personal Data Server
+
+
diff --git a/src/components/explore/RepoExplorer.tsx b/src/components/explore/RepoExplorer.tsx
index 292f864..902202c 100644
--- a/src/components/explore/RepoExplorer.tsx
+++ b/src/components/explore/RepoExplorer.tsx
@@ -70,7 +70,14 @@ export default function RepoExplorer({ repo }: { repo: string }) {
on this view — it's the only \"drill up\" affordance at the top
of the explorer hierarchy. */}
-
+
diff --git a/src/components/explore/RichRecordPreview.tsx b/src/components/explore/RichRecordPreview.tsx
index d1fd9b7..b056b2c 100644
--- a/src/components/explore/RichRecordPreview.tsx
+++ b/src/components/explore/RichRecordPreview.tsx
@@ -105,5 +105,13 @@ export default function RichRecordPreview({
return ;
}
- return ;
+ return (
+
+ );
}
diff --git a/src/components/explore/ShareLinkChip.tsx b/src/components/explore/ShareLinkChip.tsx
new file mode 100644
index 0000000..81ee2de
--- /dev/null
+++ b/src/components/explore/ShareLinkChip.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import { useState } from 'react';
+import { Check, Link2 } from 'lucide-react';
+
+type Props = {
+ /** Path or full URL — `/`-prefixed paths get the production origin prepended. */
+ url: string;
+ /** Override the button text. Defaults to "Copy link". */
+ label?: string;
+ /** Visual hint string for the tooltip / aria-label. Defaults to the resolved URL. */
+ title?: string;
+};
+
+const PRODUCTION_ORIGIN = 'https://aturi.to';
+
+async function writeToClipboard(text: string): Promise {
+ try {
+ await navigator.clipboard.writeText(text);
+ return;
+ } catch {
+ /* fall through */
+ }
+ const ta = document.createElement('textarea');
+ ta.value = text;
+ ta.style.position = 'fixed';
+ ta.style.opacity = '0';
+ document.body.appendChild(ta);
+ ta.select();
+ try {
+ document.execCommand('copy');
+ } finally {
+ document.body.removeChild(ta);
+ }
+}
+
+/**
+ * Compact "copy this page's link" button used throughout the explorer.
+ * Accepts a path (gets prefixed with aturi.to) or a full URL.
+ */
+export default function ShareLinkChip({ url, label = 'Copy link', title }: Props) {
+ const [copied, setCopied] = useState(false);
+
+ const resolvedUrl = url.startsWith('http')
+ ? url
+ : `${PRODUCTION_ORIGIN}${url.startsWith('/') ? url : `/${url}`}`;
+
+ async function onClick() {
+ await writeToClipboard(resolvedUrl);
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), 1500);
+ }
+
+ return (
+
+ );
+}