fix: improve spell UI - scrolling, execution control, and parameter markers

Three improvements to spell UI based on user feedback:

1. Fix tabs scroll issue - Change TabsContent overflow-hidden to overflow-auto
   in ProfileViewer, EventDetailViewer, and RelayViewer. This allows scrolling
   to content and clicking tabs when the visible area is small.

2. Prevent execution of parameterized spells without defaults - Add validation
   logic to check if spell has parameters and default values. Non-executable
   spells show as disabled (muted text, cursor-not-allowed, opacity-60) with
   helpful tooltips explaining where to use them (profile/event/relay viewer).

3. Add visual parameter type markers - Display Badge components showing the
   parameter type ($pubkey, $event, $relay) with User icon for $pubkey type.
   Applied to SpellRenderer (compact and detail views) and SpellsViewer cards.

Files modified:
- ProfileViewer.tsx - tabs scroll fix
- EventDetailViewer.tsx - tabs scroll fix
- RelayViewer.tsx - tabs scroll fix
- SpellRenderer.tsx - parameter validation and badges
- SpellsViewer.tsx - parameter validation and badges
This commit is contained in:
Claude
2026-01-23 22:15:56 +00:00
parent 611cc02949
commit 2f8f214f50
5 changed files with 129 additions and 40 deletions

View File

@@ -169,7 +169,7 @@ function SpellTabContent({
return (
<TabsContent
value={spellId}
className="flex-1 overflow-hidden m-0 flex flex-col"
className="flex-1 overflow-auto m-0 flex flex-col"
>
{!appliedFilter ? (
<div className="flex items-center justify-center h-full p-8 text-center text-muted-foreground">

View File

@@ -257,7 +257,7 @@ function SpellTabContent({
return (
<TabsContent
value={spellId}
className="flex-1 overflow-hidden m-0 flex flex-col"
className="flex-1 overflow-auto m-0 flex flex-col"
>
{!appliedFilter ? (
<div className="flex items-center justify-center h-full p-8 text-center text-muted-foreground">

View File

@@ -139,7 +139,7 @@ function SpellTabContent({
return (
<TabsContent
value={spellId}
className="flex-1 overflow-hidden m-0 flex flex-col"
className="flex-1 overflow-auto m-0 flex flex-col"
>
{!appliedFilter ? (
<div className="flex items-center justify-center h-full p-8 text-center text-muted-foreground">

View File

@@ -11,6 +11,7 @@ import {
Archive,
WandSparkles as Wand,
BookUp,
User,
} from "lucide-react";
import { useLiveQuery } from "dexie-react-hooks";
import db from "@/services/db";
@@ -63,6 +64,12 @@ function SpellCard({ spell, onDelete, onPublish }: SpellCardProps) {
}
}, [spell.command]);
// Check if spell has parameters but no defaults
const hasParameters = !!spell.parameterType;
const hasDefault =
spell.parameterDefault && spell.parameterDefault.length > 0;
const canExecute = !hasParameters || hasDefault;
const handlePublish = async () => {
setIsPublishing(true);
try {
@@ -112,6 +119,17 @@ function SpellCard({ spell, onDelete, onPublish }: SpellCardProps) {
>
{displayName}
</CardTitle>
{hasParameters && (
<Badge
variant="outline"
className="text-[10px] gap-1 flex-shrink-0"
>
{spell.parameterType === "$pubkey" && (
<User className="size-3" />
)}
{spell.parameterType}
</Badge>
)}
</div>
{spell.deletedAt ? (
<Badge variant="outline" className="text-muted-foreground">
@@ -139,13 +157,22 @@ function SpellCard({ spell, onDelete, onPublish }: SpellCardProps) {
<CardContent className="p-4 pt-0 flex-1">
<div className="flex flex-col gap-2">
<ExecutableCommand
commandLine={spell.command}
className="text-xs truncate line-clamp-1 text-primary hover:underline cursor-pointer"
spellId={spell.id}
>
{spell.command}
</ExecutableCommand>
{canExecute ? (
<ExecutableCommand
commandLine={spell.command}
className="text-xs truncate line-clamp-1 text-primary hover:underline cursor-pointer"
spellId={spell.id}
>
{spell.command}
</ExecutableCommand>
) : (
<div
className="text-xs truncate line-clamp-1 text-muted-foreground cursor-not-allowed opacity-60"
title={`Requires a ${spell.parameterType} to run. Use from ${spell.parameterType === "$pubkey" ? "profile" : spell.parameterType === "$event" ? "event" : "relay"} viewer.`}
>
{spell.command}
</div>
)}
<div className="flex flex-wrap gap-1.5 mt-1">
{kinds.map((kind) => (
<KindBadge

View File

@@ -152,18 +152,34 @@ export function SpellRenderer({ event }: BaseEventProps) {
try {
const spell = decodeSpell(event as SpellEvent);
// Check if spell has parameters but no defaults
const hasParameters = !!spell.parameter;
const hasDefault =
spell.parameter?.default && spell.parameter.default.length > 0;
const canExecute = !hasParameters || hasDefault;
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
{/* Title */}
{spell.name && (
<ClickableEventTitle
event={event}
className="text-lg font-semibold text-foreground"
>
{spell.name}
</ClickableEventTitle>
)}
{/* Title with parameter marker */}
<div className="flex items-center gap-2 flex-wrap">
{spell.name && (
<ClickableEventTitle
event={event}
className="text-lg font-semibold text-foreground"
>
{spell.name}
</ClickableEventTitle>
)}
{hasParameters && spell.parameter && (
<Badge variant="outline" className="text-[10px] gap-1">
{spell.parameter.type === "$pubkey" && (
<User className="size-3" />
)}
{spell.parameter.type}
</Badge>
)}
</div>
{/* Description */}
{spell.description && (
@@ -172,13 +188,22 @@ export function SpellRenderer({ event }: BaseEventProps) {
</p>
)}
{/* Command Preview */}
<ExecutableCommand
commandLine={spell.command}
className="text-xs font-mono bg-muted/30 p-2 border border-border truncate line-clamp-1 text-primary hover:underline cursor-pointer"
>
{spell.command}
</ExecutableCommand>
{/* Command Preview - only executable if has defaults or no parameters */}
{canExecute ? (
<ExecutableCommand
commandLine={spell.command}
className="text-xs font-mono bg-muted/30 p-2 border border-border truncate line-clamp-1 text-primary hover:underline cursor-pointer"
>
{spell.command}
</ExecutableCommand>
) : (
<div
className="text-xs font-mono bg-muted/30 p-2 border border-border truncate line-clamp-1 text-muted-foreground cursor-not-allowed opacity-60"
title={`Requires a ${spell.parameter?.type} to run. Use from ${spell.parameter?.type === "$pubkey" ? "profile" : spell.parameter?.type === "$event" ? "event" : "relay"} viewer.`}
>
{spell.command}
</div>
)}
{/* Kind Badges */}
{spell.filter.kinds && spell.filter.kinds.length > 0 && (
@@ -220,6 +245,12 @@ export function SpellDetailRenderer({ event }: BaseEventProps) {
try {
const spell = decodeSpell(event as SpellEvent);
// Check if spell has parameters but no defaults
const hasParameters = !!spell.parameter;
const hasDefault =
spell.parameter?.default && spell.parameter.default.length > 0;
const canExecute = !hasParameters || hasDefault;
// Create a display filter that includes since/until even in relative format
const displayFilter = { ...spell.filter };
@@ -237,14 +268,27 @@ export function SpellDetailRenderer({ event }: BaseEventProps) {
return (
<div className="flex flex-col gap-6 p-4">
<div className="flex flex-col gap-2">
{spell.name && (
<ClickableEventTitle
event={event}
className="text-2xl font-bold hover:underline cursor-pointer"
>
{spell.name}
</ClickableEventTitle>
)}
<div className="flex items-center gap-3 flex-wrap">
{spell.name && (
<ClickableEventTitle
event={event}
className="text-2xl font-bold hover:underline cursor-pointer"
>
{spell.name}
</ClickableEventTitle>
)}
{hasParameters && spell.parameter && (
<Badge variant="outline" className="text-xs gap-1.5">
{spell.parameter.type === "$pubkey" && (
<User className="size-3.5" />
)}
{spell.parameter.type}
{hasDefault && (
<span className="text-[10px] opacity-70">(has default)</span>
)}
</Badge>
)}
</div>
{spell.description && (
<p className="text-muted-foreground">{spell.description}</p>
)}
@@ -254,12 +298,30 @@ export function SpellDetailRenderer({ event }: BaseEventProps) {
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Command
</h3>
<ExecutableCommand
commandLine={spell.command}
className="text-sm font-mono p-4 bg-muted/30 border border-border text-primary hover:underline hover:decoration-dotted cursor-crosshair break-words overflow-x-auto"
>
{spell.command}
</ExecutableCommand>
{canExecute ? (
<ExecutableCommand
commandLine={spell.command}
className="text-sm font-mono p-4 bg-muted/30 border border-border text-primary hover:underline hover:decoration-dotted cursor-crosshair break-words overflow-x-auto"
>
{spell.command}
</ExecutableCommand>
) : (
<div className="flex flex-col gap-2">
<div className="text-sm font-mono p-4 bg-muted/30 border border-border text-muted-foreground cursor-not-allowed opacity-60 break-words overflow-x-auto">
{spell.command}
</div>
<p className="text-xs text-muted-foreground">
💡 This spell requires a {spell.parameter?.type} to run. Use it
from a{" "}
{spell.parameter?.type === "$pubkey"
? "profile"
: spell.parameter?.type === "$event"
? "event"
: "relay"}{" "}
viewer to execute.
</p>
</div>
)}
</div>
{spell.filter.kinds && spell.filter.kinds.length > 0 && (