mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-15 09:08:43 +02:00
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:
@@ -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 */}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user