diff --git a/src/components/nostr/CompactEventRow.tsx b/src/components/nostr/CompactEventRow.tsx
new file mode 100644
index 0000000..1f08bdc
--- /dev/null
+++ b/src/components/nostr/CompactEventRow.tsx
@@ -0,0 +1,105 @@
+import { memo, useCallback } from "react";
+import type { NostrEvent } from "@/types/nostr";
+import { useGrimoire } from "@/core/state";
+import { formatTimestamp } from "@/hooks/useLocale";
+import { getTagValue } from "applesauce-core/helpers";
+import { KindBadge } from "@/components/KindBadge";
+import { UserName } from "./UserName";
+import { compactRenderers, DefaultCompactPreview } from "./compact";
+
+// NIP-01 Kind ranges for replaceable events
+const REPLACEABLE_START = 10000;
+const REPLACEABLE_END = 20000;
+const PARAMETERIZED_REPLACEABLE_START = 30000;
+const PARAMETERIZED_REPLACEABLE_END = 40000;
+
+interface CompactEventRowProps {
+ event: NostrEvent;
+}
+
+/**
+ * Compact single-line event representation
+ * Layout: [KindBadge] [Author] [Preview] [Time]
+ */
+export function CompactEventRow({ event }: CompactEventRowProps) {
+ const { addWindow } = useGrimoire();
+ const { locale } = useGrimoire();
+
+ // Get the compact preview renderer for this kind, or use default
+ const PreviewRenderer = compactRenderers[event.kind] || DefaultCompactPreview;
+
+ // Format relative time
+ const relativeTime = formatTimestamp(
+ event.created_at,
+ "relative",
+ locale.locale,
+ );
+
+ // Format absolute time for tooltip
+ const absoluteTime = formatTimestamp(
+ event.created_at,
+ "absolute",
+ locale.locale,
+ );
+
+ // Click handler to open event detail
+ const handleClick = useCallback(() => {
+ // Determine if event is addressable/replaceable
+ const isAddressable =
+ (event.kind >= REPLACEABLE_START && event.kind < REPLACEABLE_END) ||
+ (event.kind >= PARAMETERIZED_REPLACEABLE_START &&
+ event.kind < PARAMETERIZED_REPLACEABLE_END);
+
+ let pointer;
+
+ if (isAddressable) {
+ const dTag = getTagValue(event, "d") || "";
+ pointer = {
+ kind: event.kind,
+ pubkey: event.pubkey,
+ identifier: dTag,
+ };
+ } else {
+ pointer = {
+ id: event.id,
+ };
+ }
+
+ addWindow("open", { pointer });
+ }, [event, addWindow]);
+
+ return (
+
+ {/* Kind badge - compact/icon only */}
+
+
+ {/* Author */}
+
+
+ {/* Kind-specific or default preview */}
+
+
+ {/* Timestamp */}
+
+ {relativeTime}
+
+
+ );
+}
+
+// Memoized version for scroll performance
+export const MemoizedCompactEventRow = memo(
+ CompactEventRow,
+ (prev, next) => prev.event.id === next.event.id,
+);
diff --git a/src/components/nostr/JsonEventRow.tsx b/src/components/nostr/JsonEventRow.tsx
new file mode 100644
index 0000000..360d6a1
--- /dev/null
+++ b/src/components/nostr/JsonEventRow.tsx
@@ -0,0 +1,52 @@
+import { memo } from "react";
+import type { NostrEvent } from "@/types/nostr";
+import { useCopy } from "@/hooks/useCopy";
+import { SyntaxHighlight } from "@/components/SyntaxHighlight";
+import { CodeCopyButton } from "@/components/CodeCopyButton";
+
+interface JsonEventRowProps {
+ event: NostrEvent;
+}
+
+/**
+ * JSON view for a single event
+ * Shows syntax-highlighted, copyable JSON
+ */
+export function JsonEventRow({ event }: JsonEventRowProps) {
+ const { copy, copied } = useCopy();
+ const jsonString = JSON.stringify(event, null, 2);
+
+ return (
+
+ {/* Event ID header for reference */}
+
+
+ {event.id.slice(0, 16)}...
+
+ kind {event.kind}
+
+
+ {/* JSON content */}
+
+
+ {/* Copy button - visible on hover */}
+
+ copy(jsonString)}
+ copied={copied}
+ label="Copy event JSON"
+ />
+
+
+ );
+}
+
+// Memoized version for scroll performance
+export const MemoizedJsonEventRow = memo(
+ JsonEventRow,
+ (prev, next) => prev.event.id === next.event.id,
+);
diff --git a/src/components/nostr/LinkPreview/AudioLink.tsx b/src/components/nostr/LinkPreview/AudioLink.tsx
index 0e81b71..7e00cbf 100644
--- a/src/components/nostr/LinkPreview/AudioLink.tsx
+++ b/src/components/nostr/LinkPreview/AudioLink.tsx
@@ -1,4 +1,4 @@
-import { Music } from "lucide-react";
+import { Mic } from "lucide-react";
interface AudioLinkProps {
url: string;
@@ -11,7 +11,7 @@ export function AudioLink({ url, onClick }: AudioLinkProps) {
onClick={onClick}
className="inline-flex items-baseline gap-1 text-muted-foreground underline decoration-dotted hover:text-foreground cursor-crosshair break-all line-clamp-1"
>
-
+
{url}
);
diff --git a/src/components/nostr/MediaEmbed.tsx b/src/components/nostr/MediaEmbed.tsx
index b39b787..f7291ae 100644
--- a/src/components/nostr/MediaEmbed.tsx
+++ b/src/components/nostr/MediaEmbed.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import Zoom from "react-medium-image-zoom";
import "react-medium-image-zoom/dist/styles.css";
-import { Music, AlertCircle, Play, RotateCw } from "lucide-react";
+import { Mic, AlertCircle, Play, RotateCw } from "lucide-react";
import {
isImageURL,
isVideoURL,
@@ -362,14 +362,13 @@ export function MediaEmbed({
return (
-
+
{!onAudioClick ? (