From 7e6175755eb99a506aab0e9f86c1fe3f8e3e5e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sat, 20 Dec 2025 20:02:03 +0100 Subject: [PATCH] style: refine SpellbookDropdown UI and deduplicate spells --- src/components/SpellbookDropdown.tsx | 117 ++++++++++++++++++--------- 1 file changed, 78 insertions(+), 39 deletions(-) diff --git a/src/components/SpellbookDropdown.tsx b/src/components/SpellbookDropdown.tsx index 4637e40..e130d0c 100644 --- a/src/components/SpellbookDropdown.tsx +++ b/src/components/SpellbookDropdown.tsx @@ -1,12 +1,17 @@ import { useMemo } from "react"; -import { BookHeart, ChevronDown, Layout, Loader2, WandSparkles } from "lucide-react"; +import { BookHeart, ChevronDown, Loader2, WandSparkles } from "lucide-react"; import { useLiveQuery } from "dexie-react-hooks"; import db from "@/services/db"; import { useGrimoire } from "@/core/state"; import { useReqTimeline } from "@/hooks/useReqTimeline"; import { parseSpellbook } from "@/lib/spellbook-manager"; import { decodeSpell } from "@/lib/spell-conversion"; -import type { SpellbookEvent, ParsedSpellbook, SpellEvent, ParsedSpell } from "@/types/spell"; +import type { + SpellbookEvent, + ParsedSpellbook, + SpellEvent, + ParsedSpell, +} from "@/types/spell"; import { SPELLBOOK_KIND, SPELL_KIND } from "@/constants/kinds"; import { Button } from "./ui/button"; import { @@ -19,6 +24,7 @@ import { } from "./ui/dropdown-menu"; import { toast } from "sonner"; import { manPages } from "@/types/man"; +import { cn } from "@/lib/utils"; export function SpellbookDropdown() { const { state, loadSpellbook, addWindow } = useGrimoire(); @@ -26,10 +32,10 @@ export function SpellbookDropdown() { // 1. Load Local Data const localSpellbooks = useLiveQuery(() => - db.spellbooks.toArray().then(books => books.filter(b => !b.deletedAt)), + db.spellbooks.toArray().then((books) => books.filter((b) => !b.deletedAt)), ); const localSpells = useLiveQuery(() => - db.spells.toArray().then(spells => spells.filter(s => !s.deletedAt)), + db.spells.toArray().then((spells) => spells.filter((s) => !s.deletedAt)), ); // 2. Fetch Network Data @@ -58,17 +64,25 @@ export function SpellbookDropdown() { }); } - for (const event of networkEvents.filter(e => e.kind === SPELLBOOK_KIND)) { + for (const event of networkEvents.filter((e) => e.kind === SPELLBOOK_KIND)) { const slug = event.tags.find((t) => t[0] === "d")?.[1] || ""; if (!slug) continue; const existing = allMap.get(slug); - if (existing && event.created_at * 1000 <= (existing.event?.created_at || 0) * 1000) continue; + if ( + existing && + event.created_at * 1000 <= (existing.event?.created_at || 0) * 1000 + ) + continue; try { allMap.set(slug, parseSpellbook(event as SpellbookEvent)); - } catch (e) {} + } catch (e) { + // ignore + } } - return Array.from(allMap.values()).sort((a, b) => a.title.localeCompare(b.title)); + return Array.from(allMap.values()).sort((a, b) => + a.title.localeCompare(b.title), + ); }, [localSpellbooks, networkEvents, activeAccount]); // 4. Process Spells @@ -77,30 +91,37 @@ export function SpellbookDropdown() { const allMap = new Map(); for (const s of localSpells || []) { - allMap.set(s.id, { + // Use eventId if available, otherwise fallback to local id for deduplication + const key = s.eventId || s.id; + allMap.set(key, { name: s.name || s.alias, command: s.command, description: s.description, event: s.event as SpellEvent, - filter: {}, // Not needed for dropdown + filter: {}, topics: [], - closeOnEose: false + closeOnEose: false, }); } - for (const event of networkEvents.filter(e => e.kind === SPELL_KIND)) { + for (const event of networkEvents.filter((e) => e.kind === SPELL_KIND)) { if (allMap.has(event.id)) continue; try { allMap.set(event.id, decodeSpell(event as SpellEvent)); - } catch (e) {} + } catch (e) { + // ignore + } } - return Array.from(allMap.values()).sort((a, b) => - (a.name || "Untitled").localeCompare(b.name || "Untitled") + return Array.from(allMap.values()).sort((a, b) => + (a.name || "Untitled").localeCompare(b.name || "Untitled"), ); }, [localSpells, networkEvents, activeAccount]); - if (!activeAccount || (spellbooks.length === 0 && spells.length === 0 && !networkLoading)) { + if ( + !activeAccount || + (spellbooks.length === 0 && spells.length === 0 && !networkLoading) + ) { return null; } @@ -121,27 +142,33 @@ export function SpellbookDropdown() { ? await Promise.resolve(command.argParser(cmdArgs)) : command.defaultProps || {}; addWindow(command.appId, cmdProps, spell.command); - toast.success(`Ran spell: ${spell.name || 'Untitled'}`); + toast.success(`Ran spell: ${spell.name || "Untitled"}`); } } catch (e) { toast.error("Failed to run spell"); } }; + const itemClass = + "cursor-pointer py-2 hover:bg-muted focus:bg-muted transition-colors"; + return ( - + {/* Spellbooks Section */} {spellbooks.length > 0 && ( <> @@ -152,17 +179,26 @@ export function SpellbookDropdown() { handleApplySpellbook(sb)} - className="cursor-pointer py-2" + className={itemClass} > - +
- {sb.title} + + {sb.title} + {Object.keys(sb.content.workspaces).length} tabs
))} + addWindow("spellbooks", {})} + className={cn(itemClass, "text-xs opacity-70")} + > + + Manage Library + )} @@ -177,36 +213,39 @@ export function SpellbookDropdown() { handleRunSpell(s)} - className="cursor-pointer py-2" + className={itemClass} > - +
- {s.name || "Untitled Spell"} + + {s.name || "Untitled Spell"} + {s.command}
))} + addWindow("spells", {})} + className={cn(itemClass, "text-xs opacity-70")} + > + + Manage Spells + )} {networkLoading && ( -
- - Syncing... -
+ <> + +
+ + Syncing... +
+ )} - - - addWindow("spellbooks", {})} - className="cursor-crosshair py-2" - > - - Manage Library -
); -} \ No newline at end of file +}