refactor: show original content first, then spell tabs below

Changed layout in ProfileViewer, EventDetailViewer, and RelayViewer to:
1. Show original content at the top (profile info, event detail, relay info)
2. Display spell tabs below the content (only when parameterized spells exist)

This provides a better UX by:
- Keeping the primary content visible and scrollable
- Adding spell functionality as an enhancement below
- Only showing tabs when relevant spells are available
- Avoiding replacement of the main content with tab navigation

All viewers now follow the same pattern: content first, then optional spell tabs.
This commit is contained in:
Claude
2026-01-22 13:40:29 +00:00
parent 17840c2028
commit ba9b441c75
3 changed files with 246 additions and 254 deletions

View File

@@ -263,46 +263,45 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) {
</div>
</div>
{/* Rendered Content with Tabs */}
<div className="flex-1 overflow-hidden flex flex-col">
<Tabs defaultValue="detail" className="flex flex-col h-full">
<TabsList className="w-full justify-start rounded-none border-b bg-transparent p-0 h-auto">
<TabsTrigger
value="detail"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
Detail
</TabsTrigger>
{eventSpells.map((spell) => (
<TabsTrigger
key={spell.id}
value={spell.id}
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
{spell.name || spell.alias || "Untitled Spell"}
</TabsTrigger>
))}
</TabsList>
{/* Detail Tab Content */}
<TabsContent value="detail" className="flex-1 overflow-y-auto m-0">
<EventErrorBoundary event={event}>
<DetailKindRenderer event={event} />
</EventErrorBoundary>
</TabsContent>
{/* Spell Tab Contents */}
{eventSpells.map((spell) => (
<SpellTabContent
key={spell.id}
spellId={spell.id}
spell={spell}
targetEventId={event.id}
/>
))}
</Tabs>
{/* Rendered Content */}
<div className="flex-1 overflow-y-auto">
<EventErrorBoundary event={event}>
<DetailKindRenderer event={event} />
</EventErrorBoundary>
</div>
{/* Spell Tabs */}
{eventSpells.length > 0 && (
<div className="border-t border-border flex-1 overflow-hidden flex flex-col min-h-0">
<Tabs
defaultValue={eventSpells[0]?.id}
className="flex flex-col h-full"
>
<TabsList className="w-full justify-start rounded-none border-b bg-transparent p-0 h-auto flex-shrink-0">
{eventSpells.map((spell) => (
<TabsTrigger
key={spell.id}
value={spell.id}
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
{spell.name || spell.alias || "Untitled Spell"}
</TabsTrigger>
))}
</TabsList>
{/* Spell Tab Contents */}
{eventSpells.map((spell) => (
<SpellTabContent
key={spell.id}
spellId={spell.id}
spell={spell}
targetEventId={event.id}
/>
))}
</Tabs>
</div>
)}
{/* JSON Viewer Dialog */}
<JsonViewer
data={event}

View File

@@ -469,135 +469,129 @@ export function ProfileViewer({ pubkey }: ProfileViewerProps) {
</div>
</div>
{/* Profile Content with Tabs */}
<div className="flex-1 overflow-hidden flex flex-col">
<Tabs defaultValue="profile" className="flex flex-col h-full">
<TabsList className="w-full justify-start rounded-none border-b bg-transparent p-0 h-auto">
<TabsTrigger
value="profile"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
Profile
</TabsTrigger>
{pubkeySpells.map((spell) => (
<TabsTrigger
key={spell.id}
value={spell.id}
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
{spell.name || spell.alias || "Untitled Spell"}
</TabsTrigger>
))}
</TabsList>
{/* Profile Content */}
<div className="flex-1 overflow-y-auto p-4">
{!profile && !profileEvent && <ProfileCardSkeleton variant="full" />}
{/* Profile Tab Content */}
<TabsContent
value="profile"
className="flex-1 overflow-y-auto p-4 m-0"
>
{!profile && !profileEvent && (
<ProfileCardSkeleton variant="full" />
)}
{!profile && profileEvent && (
<div className="text-center text-muted-foreground text-sm">
No profile metadata found
</div>
)}
{!profile && profileEvent && (
<div className="text-center text-muted-foreground text-sm">
No profile metadata found
</div>
)}
{profile && (
<div className="flex flex-col gap-4 max-w-2xl">
<div className="flex flex-col gap-0">
{/* Display Name */}
<UserName
pubkey={pubkey}
className="text-2xl font-bold pointer-events-none"
/>
{/* NIP-05 */}
{profile.nip05 && (
<div className="text-xs">
<Nip05 pubkey={pubkey} profile={profile} />
</div>
)}
{profile && (
<div className="flex flex-col gap-4 max-w-2xl">
<div className="flex flex-col gap-0">
{/* Display Name */}
<UserName
pubkey={pubkey}
className="text-2xl font-bold pointer-events-none"
/>
{/* NIP-05 */}
{profile.nip05 && (
<div className="text-xs">
<Nip05 pubkey={pubkey} profile={profile} />
</div>
)}
</div>
{/* About/Bio */}
{profile.about && (
<div className="flex flex-col gap-1">
<div className="text-xs text-muted-foreground uppercase tracking-wide">
About
</div>
<RichText
className="text-sm whitespace-pre-wrap break-words"
content={profile.about}
/>
</div>
)}
{/* Website */}
{profile.website && (
<div className="flex flex-col gap-1">
<div className="text-xs text-muted-foreground uppercase tracking-wide">
Website
</div>
<a
href={profile.website}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-accent underline decoration-dotted"
>
{profile.website}
</a>
</div>
)}
{/* Lightning Address */}
{profile.lud16 && (
<div className="flex flex-col gap-1">
<div className="text-xs text-muted-foreground uppercase tracking-wide">
Lightning Address
</div>
<button
onClick={() =>
addWindow("zap", { recipientPubkey: resolvedPubkey })
}
className="flex items-center gap-2 w-full text-left hover:bg-muted/50 rounded px-2 py-1 -mx-2 transition-colors group"
title="Send zap"
>
<Zap className="size-4 text-yellow-500 group-hover:text-yellow-600 transition-colors flex-shrink-0" />
<code className="text-sm font-mono flex-1 min-w-0 truncate">
{profile.lud16}
</code>
</button>
</div>
)}
{/* LUD06 (LNURL) */}
{profile.lud06 && (
<div className="flex flex-col gap-1">
<div className="text-xs text-muted-foreground uppercase tracking-wide">
LNURL
</div>
<code className="text-sm font-mono break-all">
{profile.lud06}
</code>
</div>
)}
{/* About/Bio */}
{profile.about && (
<div className="flex flex-col gap-1">
<div className="text-xs text-muted-foreground uppercase tracking-wide">
About
</div>
<RichText
className="text-sm whitespace-pre-wrap break-words"
content={profile.about}
/>
</div>
)}
</TabsContent>
{/* Spell Tab Contents */}
{pubkeySpells.map((spell) => (
<SpellTabContent
key={spell.id}
spellId={spell.id}
spell={spell}
targetPubkey={resolvedPubkey}
/>
))}
</Tabs>
{/* Website */}
{profile.website && (
<div className="flex flex-col gap-1">
<div className="text-xs text-muted-foreground uppercase tracking-wide">
Website
</div>
<a
href={profile.website}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-accent underline decoration-dotted"
>
{profile.website}
</a>
</div>
)}
{/* Lightning Address */}
{profile.lud16 && (
<div className="flex flex-col gap-1">
<div className="text-xs text-muted-foreground uppercase tracking-wide">
Lightning Address
</div>
<button
onClick={() =>
addWindow("zap", { recipientPubkey: resolvedPubkey })
}
className="flex items-center gap-2 w-full text-left hover:bg-muted/50 rounded px-2 py-1 -mx-2 transition-colors group"
title="Send zap"
>
<Zap className="size-4 text-yellow-500 group-hover:text-yellow-600 transition-colors flex-shrink-0" />
<code className="text-sm font-mono flex-1 min-w-0 truncate">
{profile.lud16}
</code>
</button>
</div>
)}
{/* LUD06 (LNURL) */}
{profile.lud06 && (
<div className="flex flex-col gap-1">
<div className="text-xs text-muted-foreground uppercase tracking-wide">
LNURL
</div>
<code className="text-sm font-mono break-all">
{profile.lud06}
</code>
</div>
)}
</div>
)}
</div>
{/* Spell Tabs */}
{pubkeySpells.length > 0 && (
<div className="border-t border-border flex-1 overflow-hidden flex flex-col min-h-0">
<Tabs
defaultValue={pubkeySpells[0]?.id}
className="flex flex-col h-full"
>
<TabsList className="w-full justify-start rounded-none border-b bg-transparent p-0 h-auto flex-shrink-0">
{pubkeySpells.map((spell) => (
<TabsTrigger
key={spell.id}
value={spell.id}
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
{spell.name || spell.alias || "Untitled Spell"}
</TabsTrigger>
))}
</TabsList>
{/* Spell Tab Contents */}
{pubkeySpells.map((spell) => (
<SpellTabContent
key={spell.id}
spellId={spell.id}
spell={spell}
targetPubkey={resolvedPubkey}
/>
))}
</Tabs>
</div>
)}
</div>
);
}

View File

@@ -147,109 +147,108 @@ export function RelayViewer({ url }: RelayViewerProps) {
return (
<div className="flex flex-col h-full overflow-hidden">
<Tabs defaultValue="info" className="flex flex-col h-full">
<TabsList className="w-full justify-start rounded-none border-b bg-transparent p-0 h-auto">
<TabsTrigger
value="info"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
Info
</TabsTrigger>
{relaySpells.map((spell) => (
<TabsTrigger
key={spell.id}
value={spell.id}
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
{spell.name || spell.alias || "Untitled Spell"}
</TabsTrigger>
))}
</TabsList>
{/* Relay Info Content */}
<div className="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
{/* Header */}
<div className="flex items-center gap-4">
<div className="flex-1">
<h2 className="text-2xl font-bold">
{info?.name || "Unknown Relay"}
</h2>
<div className="flex items-center gap-2 text-xs font-mono text-muted-foreground">
{url}
<Button
variant="link"
size="icon"
className="size-4 text-muted-foreground"
onClick={() => copy(url)}
>
{copied ? (
<CopyCheck className="size-3" />
) : (
<Copy className="size-3" />
)}
</Button>
</div>
{info?.description && (
<p className="text-sm mt-2">{info.description}</p>
)}
</div>
</div>
{/* Info Tab Content */}
<TabsContent
value="info"
className="flex-1 overflow-y-auto p-4 m-0 flex flex-col gap-6"
>
{/* Header */}
<div className="flex items-center gap-4">
<div className="flex-1">
<h2 className="text-2xl font-bold">
{info?.name || "Unknown Relay"}
</h2>
<div className="flex items-center gap-2 text-xs font-mono text-muted-foreground">
{url}
<Button
variant="link"
size="icon"
className="size-4 text-muted-foreground"
onClick={() => copy(url)}
>
{copied ? (
<CopyCheck className="size-3" />
) : (
<Copy className="size-3" />
)}
</Button>
</div>
{info?.description && (
<p className="text-sm mt-2">{info.description}</p>
{/* Operator */}
{(info?.contact || info?.pubkey) && (
<div>
<h3 className="mb-2 font-semibold text-sm">Operator</h3>
<div className="space-y-2 text-sm text-accent">
{info.contact && info.contact.length == 64 && (
<UserName pubkey={info.contact} />
)}
{info.pubkey && info.pubkey.length === 64 && (
<UserName pubkey={info.pubkey} />
)}
</div>
</div>
)}
{/* Operator */}
{(info?.contact || info?.pubkey) && (
<div>
<h3 className="mb-2 font-semibold text-sm">Operator</h3>
<div className="space-y-2 text-sm text-accent">
{info.contact && info.contact.length == 64 && (
<UserName pubkey={info.contact} />
)}
{info.pubkey && info.pubkey.length === 64 && (
<UserName pubkey={info.pubkey} />
)}
</div>
{/* Software */}
{(info?.software || info?.version) && (
<div>
<h3 className="mb-2 font-semibold text-sm">Software</h3>
<span className="text-sm text-muted-foreground">
{info.software || info.version}
</span>
</div>
)}
{/* Supported NIPs */}
{info?.supported_nips && info.supported_nips.length > 0 && (
<div>
<h3 className="mb-3 font-semibold text-sm">Supported NIPs</h3>
<div className="flex flex-wrap gap-2">
{info.supported_nips.map((num: number) => (
<NIPBadge
key={num}
nipNumber={String(num).padStart(2, "0")}
showName={true}
/>
))}
</div>
)}
</div>
)}
</div>
{/* Software */}
{(info?.software || info?.version) && (
<div>
<h3 className="mb-2 font-semibold text-sm">Software</h3>
<span className="text-sm text-muted-foreground">
{info.software || info.version}
</span>
</div>
)}
{/* Spell Tabs */}
{relaySpells.length > 0 && (
<div className="border-t border-border flex-1 overflow-hidden flex flex-col min-h-0">
<Tabs
defaultValue={relaySpells[0]?.id}
className="flex flex-col h-full"
>
<TabsList className="w-full justify-start rounded-none border-b bg-transparent p-0 h-auto flex-shrink-0">
{relaySpells.map((spell) => (
<TabsTrigger
key={spell.id}
value={spell.id}
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
{spell.name || spell.alias || "Untitled Spell"}
</TabsTrigger>
))}
</TabsList>
{/* Supported NIPs */}
{info?.supported_nips && info.supported_nips.length > 0 && (
<div>
<h3 className="mb-3 font-semibold text-sm">Supported NIPs</h3>
<div className="flex flex-wrap gap-2">
{info.supported_nips.map((num: number) => (
<NIPBadge
key={num}
nipNumber={String(num).padStart(2, "0")}
showName={true}
/>
))}
</div>
</div>
)}
</TabsContent>
{/* Spell Tab Contents */}
{relaySpells.map((spell) => (
<SpellTabContent
key={spell.id}
spellId={spell.id}
spell={spell}
targetRelay={url}
/>
))}
</Tabs>
{/* Spell Tab Contents */}
{relaySpells.map((spell) => (
<SpellTabContent
key={spell.id}
spellId={spell.id}
spell={spell}
targetRelay={url}
/>
))}
</Tabs>
</div>
)}
</div>
);
}