mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-16 17:48:34 +02:00
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:
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user