@ -23,10 +23,10 @@ import { showPublishingError, showSimplePublishSuccess } from '@/lib/publishing-
@@ -23,10 +23,10 @@ import { showPublishingError, showSimplePublishSuccess } from '@/lib/publishing-
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import { getRelaysForSpellCatalogSync } from '@/services/spell.service'
import { Minus , Plus , X } from 'lucide-react'
import { Info , Minus , Plus , X } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import type { Event as NostrEvent } from 'nostr-tools'
import { useCallback , useEffect , useRef , useState } from 'react'
import { useCallback , useEffect , useRef , useState , type ReactNode } from 'react'
import logger from '@/lib/logger'
/** Arrow keys should control the control, not the dialog scroll */
@ -140,6 +140,44 @@ function DynamicStringListField({
@@ -140,6 +140,44 @@ function DynamicStringListField({
)
}
/** Bottom-of-form panel: name, description, catalog topics — not part of NIP-A7 REQ filter. */
function SpellMetadataSection ( {
title ,
badge ,
hint ,
children
} : {
title : string
badge : string
hint : string
children : ReactNode
} ) {
return (
< div
className = "rounded-xl border-2 border-dashed border-muted-foreground/35 bg-muted/25"
role = "region"
aria - labelledby = "spell-form-metadata-title"
>
< div className = "space-y-1.5 border-b border-border/80 bg-muted/40 px-4 py-3" >
< div className = "flex flex-wrap items-center gap-2" >
< Info className = "size-4 shrink-0 text-muted-foreground" aria - hidden / >
< h3 id = "spell-form-metadata-title" className = "text-sm font-semibold tracking-tight" >
{ title }
< / h3 >
< span
className = "rounded-md border border-muted-foreground/45 bg-background/80 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground"
title = { hint }
>
{ badge }
< / span >
< / div >
< p className = "ps-6 text-xs leading-relaxed text-muted-foreground" > { hint } < / p >
< / div >
< div className = "grid gap-4 p-4" > { children } < / div >
< / div >
)
}
function TagFiltersEditor ( {
tagFilters ,
onChange
@ -156,7 +194,7 @@ function TagFiltersEditor({
@@ -156,7 +194,7 @@ function TagFiltersEditor({
}
return (
< div className = "grid gap-2" >
< Label > { t ( 'REQ tag filters ' ) } < / Label >
< Label > { t ( 'spellFormTagFiltersLabel ' ) } < / Label >
< p className = "text-xs text-muted-foreground" > { t ( 'spellTagFiltersHint' ) } < / p >
{ tagFilters . length === 0 ? (
< p className = "text-xs text-muted-foreground" > { t ( 'spellTagFiltersEmpty' ) } < / p >
@ -405,9 +443,7 @@ export default function CreateSpellDialog({
@@ -405,9 +443,7 @@ export default function CreateSpellDialog({
< p className = "mt-2 text-sm text-muted-foreground" >
{ spellToClone
? t ( 'Clone spell intro' )
: t (
'Spells are saved relay filters (NIP-A7). Fill in the filter fields below. Use $me for your pubkey and $contacts for your follow list when executing.'
) }
: t ( 'spellCreateIntro' ) }
< / p >
< / div >
@ -458,6 +494,12 @@ export default function CreateSpellDialog({
@@ -458,6 +494,12 @@ export default function CreateSpellDialog({
) : null }
< / div >
< div className = "space-y-4 border-t border-border pt-4" >
< div className = "space-y-1" >
< h3 className = "text-sm font-semibold text-foreground" > { t ( 'spellFormSectionQueryTitle' ) } < / h3 >
< p className = "text-xs text-muted-foreground" > { t ( 'spellFormSectionQueryHint' ) } < / p >
< / div >
< div className = "grid gap-2" >
< Label > { t ( 'Command' ) } < / Label >
< select
@ -476,25 +518,6 @@ export default function CreateSpellDialog({
@@ -476,25 +518,6 @@ export default function CreateSpellDialog({
< p className = "text-xs text-muted-foreground" > { t ( 'REQ returns a feed; COUNT returns a number.' ) } < / p >
< / div >
< div className = "grid gap-2" >
< Label > { t ( 'Name' ) } < / Label >
< Input
value = { form . name }
onChange = { ( e ) = > setForm ( ( f ) = > ( { . . . f , name : e.target.value } ) ) }
placeholder = { t ( 'Human-readable spell name' ) }
/ >
< / div >
< div className = "grid gap-2" >
< Label > { t ( 'Description (content)' ) } < / Label >
< Textarea
value = { form . content }
onChange = { ( e ) = > setForm ( ( f ) = > ( { . . . f , content : e.target.value } ) ) }
placeholder = { t ( 'Plain text description of the query' ) }
rows = { 2 }
/ >
< / div >
< DynamicStringListField
label = { t ( 'Kinds' ) }
hint = { t ( 'One kind number per row (e.g. 1 for notes).' ) }
@ -520,6 +543,11 @@ export default function CreateSpellDialog({
@@ -520,6 +543,11 @@ export default function CreateSpellDialog({
onChange = { ( ids ) = > setForm ( ( f ) = > ( { . . . f , ids } ) ) }
/ >
< TagFiltersEditor
tagFilters = { form . tagFilters }
onChange = { ( tagFilters ) = > setForm ( ( f ) = > ( { . . . f , tagFilters } ) ) }
/ >
< div className = "grid gap-2" >
< Label > { t ( 'Limit' ) } < / Label >
< Input
@ -568,19 +596,6 @@ export default function CreateSpellDialog({
@@ -568,19 +596,6 @@ export default function CreateSpellDialog({
onChange = { ( relays ) = > setForm ( ( f ) = > ( { . . . f , relays } ) ) }
/ >
< DynamicStringListField
label = { t ( 'Topics (t tags for categorization)' ) }
hint = { t ( 'One topic per row.' ) }
placeholder = { t ( 'topic' ) }
values = { form . topics }
onChange = { ( topics ) = > setForm ( ( f ) = > ( { . . . f , topics } ) ) }
/ >
< TagFiltersEditor
tagFilters = { form . tagFilters }
onChange = { ( tagFilters ) = > setForm ( ( f ) = > ( { . . . f , tagFilters } ) ) }
/ >
{ form . cmd === 'REQ' ? (
< div className = "flex flex-col gap-1.5" >
< Label > { t ( 'Mode' ) } < / Label >
@ -606,6 +621,40 @@ export default function CreateSpellDialog({
@@ -606,6 +621,40 @@ export default function CreateSpellDialog({
< / div >
) : null }
< / div >
< SpellMetadataSection
title = { t ( 'spellFormSectionMetadataTitle' ) }
badge = { t ( 'spellFormSectionMetadataBadge' ) }
hint = { t ( 'spellFormSectionMetadataHint' ) }
>
< div className = "grid gap-2" >
< Label > { t ( 'Name' ) } < / Label >
< Input
value = { form . name }
onChange = { ( e ) = > setForm ( ( f ) = > ( { . . . f , name : e.target.value } ) ) }
placeholder = { t ( 'Human-readable spell name' ) }
/ >
< / div >
< div className = "grid gap-2" >
< Label > { t ( 'Description (content)' ) } < / Label >
< Textarea
value = { form . content }
onChange = { ( e ) = > setForm ( ( f ) = > ( { . . . f , content : e.target.value } ) ) }
placeholder = { t ( 'Plain text description of the query' ) }
rows = { 2 }
/ >
< / div >
< DynamicStringListField
label = { t ( 'spellFormCatalogTopicsLabel' ) }
hint = { t ( 'spellTopicsMetadataHint' ) }
placeholder = { t ( 'topic' ) }
values = { form . topics }
onChange = { ( topics ) = > setForm ( ( f ) = > ( { . . . f , topics } ) ) }
/ >
< / SpellMetadataSection >
< / div >
< / div >
< div className = "flex shrink-0 flex-wrap justify-end gap-2 border-t px-6 py-4" >