feat: clickable spell and spellbook links

This commit is contained in:
Alejandro Gómez
2025-12-21 22:54:44 +01:00
parent de063c8bfe
commit e7a7bb05a4
3 changed files with 58 additions and 4 deletions

View File

@@ -63,6 +63,7 @@ function SpellbookCard({
showAuthor = false,
isOwner = true,
}: SpellbookCardProps) {
const { addWindow } = useGrimoire();
const [isPublishing, setIsPublishing] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const displayName = spellbook.title || "Untitled Spellbook";
@@ -108,6 +109,26 @@ function SpellbookCard({
onApply(parsed);
};
const handleOpenEvent = () => {
const id = spellbook.eventId || (spellbook.event?.id as string);
if (id && id.length === 64) {
addWindow("open", { pointer: { id } }, `open ${id}`);
} else if (spellbook.isPublished && spellbook.slug && authorPubkey) {
// For addressable events (kind 30003)
addWindow(
"open",
{
pointer: {
kind: SPELLBOOK_KIND,
pubkey: authorPubkey,
identifier: spellbook.slug,
},
},
`open ${SPELLBOOK_KIND}:${authorPubkey}:${spellbook.slug}`,
);
}
};
return (
<Card
className={cn(
@@ -119,7 +140,19 @@ function SpellbookCard({
<div className="flex items-center flex-wrap justify-between gap-2">
<div className="flex items-center gap-2 flex-1 overflow-hidden">
<BookHeart className="size-4 flex-shrink-0 text-muted-foreground mt-0.5" />
<CardTitle className="text-xl truncate" title={displayName}>
<CardTitle
className={cn(
"text-xl truncate",
(spellbook.eventId || spellbook.isPublished) &&
"cursor-pointer hover:underline text-primary",
)}
title={displayName}
onClick={
spellbook.eventId || spellbook.isPublished
? handleOpenEvent
: undefined
}
>
{displayName}
</CardTitle>
</div>

View File

@@ -47,6 +47,7 @@ interface SpellCardProps {
}
function SpellCard({ spell, onDelete, onPublish }: SpellCardProps) {
const { addWindow } = useGrimoire();
const [isPublishing, setIsPublishing] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const displayName = spell.name || spell.alias || "Untitled Spell";
@@ -80,6 +81,13 @@ function SpellCard({ spell, onDelete, onPublish }: SpellCardProps) {
}
};
const handleOpenEvent = () => {
const id = spell.eventId || (spell.event?.id as string);
if (id && id.length === 64) {
addWindow("open", { pointer: { id } }, `open ${id}`);
}
};
return (
<Card
className={cn(
@@ -91,7 +99,17 @@ function SpellCard({ spell, onDelete, onPublish }: SpellCardProps) {
<div className="flex items-center flex-wrap justify-between gap-2">
<div className="flex items-center gap-2 flex-1 overflow-hidden">
<WandSparkles className="size-4 flex-shrink-0 text-muted-foreground mt-0.5" />
<CardTitle className="text-xl truncate" title={displayName}>
<CardTitle
className={cn(
"text-xl truncate",
(spell.eventId || spell.event) &&
"cursor-pointer hover:underline text-primary",
)}
title={displayName}
onClick={
spell.eventId || spell.event ? handleOpenEvent : undefined
}
>
{displayName}
</CardTitle>
</div>

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, ReactNode } from "react";
import { Terminal } from "lucide-react";
import { useAccountSync } from "@/hooks/useAccountSync";
import { useRelayListCacheSync } from "@/hooks/useRelayListCacheSync";
import { useRelayState } from "@/hooks/useRelayState";
@@ -59,10 +60,12 @@ export function AppShell({ children }: AppShellProps) {
<header className="flex flex-row items-center justify-between px-1 border-b border-border">
<button
onClick={() => setCommandLauncherOpen(true)}
className="p-1 text-muted-foreground hover:text-accent transition-colors cursor-crosshair"
className="p-1.5 text-muted-foreground hover:text-accent transition-colors cursor-crosshair flex items-center gap-2"
title="Launch command (Cmd+K)"
aria-label="Launch command palette"
></button>
>
<Terminal className="size-4" />
</button>
<div className="flex items-center gap-2">
<SpellbookDropdown />