mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-15 17:19:27 +02:00
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:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user