refactor: show original content first, then spell tabs below

- Remove flex-1 from main content areas (profile, event, relay)
- Make spell tabs horizontally scrollable with overflow-x-auto
- Add kind icons (up to 3) to the left of spell names using compact badges
- Extract kinds from both published and local spells for display
- Use whitespace-nowrap to prevent tab text wrapping
- Improve overall layout stacking of main component and tabs
This commit is contained in:
Claude
2026-01-22 15:18:19 +00:00
parent e7723cd223
commit 594e2ee937
3 changed files with 120 additions and 33 deletions

View File

@@ -28,6 +28,7 @@ import { useReqTimelineEnhanced } from "@/hooks/useReqTimelineEnhanced";
import { applySpellParameters, decodeSpell } from "@/lib/spell-conversion";
import { parseReqCommand } from "@/lib/req-parser";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { KindBadge } from "./KindBadge";
export interface EventDetailViewerProps {
pointer: EventPointer | AddressPointer;
@@ -372,7 +373,7 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) {
</div>
{/* Rendered Content */}
<div className="flex-1 overflow-y-auto">
<div className="overflow-y-auto">
<EventErrorBoundary event={event}>
<DetailKindRenderer event={event} />
</EventErrorBoundary>
@@ -385,16 +386,44 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) {
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 className="w-full justify-start rounded-none border-b bg-transparent p-0 h-auto flex-shrink-0 overflow-x-auto overflow-y-hidden">
{eventSpells.map((spell) => {
// Extract kinds from spell for display
const spellKinds = (() => {
try {
if (spell.event) {
const decoded = decodeSpell(spell.event);
return decoded.filter.kinds?.slice(0, 3) || [];
}
// For local spells, parse command
const commandWithoutPrefix = spell.command
.replace(/^\s*(req|count)\s+/i, "")
.trim();
const tokens = commandWithoutPrefix.split(/\s+/);
const parsed = parseReqCommand(tokens);
return parsed.filter.kinds?.slice(0, 3) || [];
} catch {
return [];
}
})();
return (
<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 flex items-center gap-2 whitespace-nowrap"
>
{spellKinds.length > 0 && (
<div className="flex items-center gap-1">
{spellKinds.map((kind) => (
<KindBadge key={kind} kind={kind} variant="compact" />
))}
</div>
)}
<span>{spell.name || spell.alias || "Untitled Spell"}</span>
</TabsTrigger>
);
})}
</TabsList>
{/* Spell Tab Contents */}

View File

@@ -42,6 +42,7 @@ import { applySpellParameters, decodeSpell } from "@/lib/spell-conversion";
import { parseReqCommand } from "@/lib/req-parser";
import { useOutboxRelays } from "@/hooks/useOutboxRelays";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { KindBadge } from "./KindBadge";
export interface ProfileViewerProps {
pubkey: string;
@@ -615,7 +616,7 @@ export function ProfileViewer({ pubkey }: ProfileViewerProps) {
</div>
{/* Profile Content */}
<div className="flex-1 overflow-y-auto p-4">
<div className="overflow-y-auto p-4">
{!profile && !profileEvent && <ProfileCardSkeleton variant="full" />}
{!profile && profileEvent && (
@@ -713,16 +714,44 @@ export function ProfileViewer({ pubkey }: ProfileViewerProps) {
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 className="w-full justify-start rounded-none border-b bg-transparent p-0 h-auto flex-shrink-0 overflow-x-auto overflow-y-hidden">
{pubkeySpells.map((spell) => {
// Extract kinds from spell for display
const spellKinds = (() => {
try {
if (spell.event) {
const decoded = decodeSpell(spell.event);
return decoded.filter.kinds?.slice(0, 3) || [];
}
// For local spells, parse command
const commandWithoutPrefix = spell.command
.replace(/^\s*(req|count)\s+/i, "")
.trim();
const tokens = commandWithoutPrefix.split(/\s+/);
const parsed = parseReqCommand(tokens);
return parsed.filter.kinds?.slice(0, 3) || [];
} catch {
return [];
}
})();
return (
<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 flex items-center gap-2 whitespace-nowrap"
>
{spellKinds.length > 0 && (
<div className="flex items-center gap-1">
{spellKinds.map((kind) => (
<KindBadge key={kind} kind={kind} variant="compact" />
))}
</div>
)}
<span>{spell.name || spell.alias || "Untitled Spell"}</span>
</TabsTrigger>
);
})}
</TabsList>
{/* Spell Tab Contents */}

View File

@@ -12,6 +12,7 @@ import { applySpellParameters, decodeSpell } from "@/lib/spell-conversion";
import { parseReqCommand } from "@/lib/req-parser";
import { useMemo } from "react";
import { NIPBadge } from "./NIPBadge";
import { KindBadge } from "./KindBadge";
import { SpellHeader } from "./timeline/SpellHeader";
export interface RelayViewerProps {
@@ -237,7 +238,7 @@ export function RelayViewer({ url }: RelayViewerProps) {
return (
<div className="flex flex-col h-full overflow-hidden">
{/* Relay Info Content */}
<div className="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
<div className="overflow-y-auto p-4 flex flex-col gap-6">
{/* Header */}
<div className="flex items-center gap-4">
<div className="flex-1">
@@ -314,16 +315,44 @@ export function RelayViewer({ url }: RelayViewerProps) {
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 className="w-full justify-start rounded-none border-b bg-transparent p-0 h-auto flex-shrink-0 overflow-x-auto overflow-y-hidden">
{relaySpells.map((spell) => {
// Extract kinds from spell for display
const spellKinds = (() => {
try {
if (spell.event) {
const decoded = decodeSpell(spell.event);
return decoded.filter.kinds?.slice(0, 3) || [];
}
// For local spells, parse command
const commandWithoutPrefix = spell.command
.replace(/^\s*(req|count)\s+/i, "")
.trim();
const tokens = commandWithoutPrefix.split(/\s+/);
const parsed = parseReqCommand(tokens);
return parsed.filter.kinds?.slice(0, 3) || [];
} catch {
return [];
}
})();
return (
<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 flex items-center gap-2 whitespace-nowrap"
>
{spellKinds.length > 0 && (
<div className="flex items-center gap-1">
{spellKinds.map((kind) => (
<KindBadge key={kind} kind={kind} variant="compact" />
))}
</div>
)}
<span>{spell.name || spell.alias || "Untitled Spell"}</span>
</TabsTrigger>
);
})}
</TabsList>
{/* Spell Tab Contents */}