From e47fde91587f59849f2ab171908c3ae5dc1f09ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sun, 21 Dec 2025 12:57:14 +0100 Subject: [PATCH] feat: better layout rendering --- src/components/Home.tsx | 54 ++++--- src/components/LayoutControls.tsx | 56 ++----- src/components/SaveSpellbookDialog.tsx | 13 +- src/components/SpellbookDropdown.tsx | 149 +++++++++++------- .../nostr/kinds/SpellbookRenderer.tsx | 65 ++++++-- 5 files changed, 191 insertions(+), 146 deletions(-) diff --git a/src/components/Home.tsx b/src/components/Home.tsx index e8db4db..cfc7403 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -24,14 +24,14 @@ import { toast } from "sonner"; import { Button } from "./ui/button"; export default function Home() { - const { - state, - updateLayout, - removeWindow, - switchToTemporary, - applyTemporaryToPersistent, + const { + state, + updateLayout, + removeWindow, + switchToTemporary, + applyTemporaryToPersistent, discardTemporary, - isTemporary + isTemporary, } = useGrimoire(); const [commandLauncherOpen, setCommandLauncherOpen] = useState(false); const { actor, identifier } = useParams(); @@ -94,18 +94,15 @@ export default function Home() { if (spellbookEvent && !hasLoadedSpellbook) { try { const parsed = parseSpellbook(spellbookEvent as SpellbookEvent); - + // Use the new temporary state system switchToTemporary(parsed); setHasLoadedSpellbook(true); if (isPreviewPath) { toast.info(`Previewing layout: ${parsed.title}`, { - description: "You are in a temporary session. Apply to keep this layout permanently.", - }); - } else if (isDirectPath) { - toast.success(`Loaded temporary layout: ${parsed.title}`, { - description: "Visit / to return to your permanent dashboard, or click Apply Layout.", + description: + "You are in a temporary session. Apply to keep this layout.", }); } } catch (e) { @@ -113,12 +110,18 @@ export default function Home() { toast.error("Failed to load spellbook"); } } - }, [spellbookEvent, hasLoadedSpellbook, isPreviewPath, isDirectPath, switchToTemporary]); + }, [ + spellbookEvent, + hasLoadedSpellbook, + isPreviewPath, + isDirectPath, + switchToTemporary, + ]); const handleApplyLayout = () => { applyTemporaryToPersistent(); navigate("/", { replace: true }); - toast.success("Layout applied to your dashboard permanently"); + toast.success("Layout applied to your dashboard"); }; const handleDiscardPreview = () => { @@ -201,22 +204,24 @@ export default function Home() {
- {isPreviewPath ? "Preview Mode" : "Temporary Layout"}: {spellbookEvent?.tags.find(t => t[0] === 'title')?.[1] || 'Spellbook'} + {isPreviewPath ? "Preview Mode" : "Temporary Layout"}:{" "} + {spellbookEvent?.tags.find((t) => t[0] === "title")?.[1] || + "Spellbook"}
- - - + > +
diff --git a/src/components/LayoutControls.tsx b/src/components/LayoutControls.tsx index d22e0e1..f5fef91 100644 --- a/src/components/LayoutControls.tsx +++ b/src/components/LayoutControls.tsx @@ -6,8 +6,6 @@ import { Sparkles, SplitSquareHorizontal, SplitSquareVertical, - Save, - BookOpen, } from "lucide-react"; import { Button } from "./ui/button"; import { Slider } from "./ui/slider"; @@ -23,17 +21,15 @@ import { import { toast } from "sonner"; import type { LayoutConfig } from "@/types/app"; import { useState } from "react"; -import { SaveSpellbookDialog } from "./SaveSpellbookDialog"; export function LayoutControls() { - const { state, applyPresetLayout, updateLayoutConfig, addWindow } = useGrimoire(); + const { state, applyPresetLayout, updateLayoutConfig } = useGrimoire(); const { workspaces, activeWorkspaceId, layoutConfig } = state; // Local state for immediate slider feedback (debounced persistence) const [localSplitPercentage, setLocalSplitPercentage] = useState< number | null >(null); - const [saveDialogOpen, setSaveDialogOpen] = useState(false); const activeWorkspace = workspaces[activeWorkspaceId]; const windowCount = activeWorkspace?.windowIds.length || 0; @@ -109,42 +105,19 @@ export function LayoutControls() { localSplitPercentage ?? layoutConfig.splitPercentage; return ( - <> - - - - - - - {/* Spellbooks Section */} -
- Spellbooks -
- setSaveDialogOpen(true)} - className="flex items-center gap-3 cursor-pointer" - > - -
Save Layout
-
- addWindow("spellbooks", {})} - className="flex items-center gap-3 cursor-pointer" - > - -
Open Spellbooks
-
- - - - {/* Layouts Section */} + + + + + + {/* Layouts Section */}
Layout Presets
@@ -225,6 +198,5 @@ export function LayoutControls() {
- ); } diff --git a/src/components/SaveSpellbookDialog.tsx b/src/components/SaveSpellbookDialog.tsx index db9b950..0d239f4 100644 --- a/src/components/SaveSpellbookDialog.tsx +++ b/src/components/SaveSpellbookDialog.tsx @@ -37,7 +37,7 @@ export function SaveSpellbookDialog({ onOpenChange, existingSpellbook, }: SaveSpellbookDialogProps) { - const { state } = useGrimoire(); + const { state, loadSpellbook } = useGrimoire(); const isUpdateMode = !!existingSpellbook; const [title, setTitle] = useState(existingSpellbook?.title || ""); @@ -123,6 +123,17 @@ export function SaveSpellbookDialog({ ); } + // 5. Set as active spellbook + const parsedSpellbook = { + slug, + title, + description: description || undefined, + content: localSpellbook.content, + referencedSpells: [], + event: localSpellbook.event as any, // Event might not exist for locally-only spellbooks + }; + loadSpellbook(parsedSpellbook); + onOpenChange(false); // Reset form only if creating new if (!isUpdateMode) { diff --git a/src/components/SpellbookDropdown.tsx b/src/components/SpellbookDropdown.tsx index 8e55780..98613a2 100644 --- a/src/components/SpellbookDropdown.tsx +++ b/src/components/SpellbookDropdown.tsx @@ -1,6 +1,7 @@ import { useMemo, useState } from "react"; import { BookHeart, ChevronDown, Plus, Save, Settings, X } from "lucide-react"; import { useLiveQuery } from "dexie-react-hooks"; +import { useLocation } from "react-router"; import db from "@/services/db"; import { useGrimoire } from "@/core/state"; import { useReqTimeline } from "@/hooks/useReqTimeline"; @@ -20,19 +21,32 @@ import { cn } from "@/lib/utils"; import { SaveSpellbookDialog } from "./SaveSpellbookDialog"; export function SpellbookDropdown() { - const { state, loadSpellbook, addWindow, clearActiveSpellbook, applyTemporaryToPersistent, isTemporary } = - useGrimoire(); + const { + state, + loadSpellbook, + addWindow, + clearActiveSpellbook, + applyTemporaryToPersistent, + isTemporary, + } = useGrimoire(); + const location = useLocation(); const activeAccount = state.activeAccount; const activeSpellbook = state.activeSpellbook; const [saveDialogOpen, setSaveDialogOpen] = useState(false); - const [dialogSpellbook, setDialogSpellbook] = useState<{ - slug: string; - title: string; - description?: string; - workspaceIds?: string[]; - localId?: string; - pubkey?: string; - } | undefined>(undefined); + const [dialogSpellbook, setDialogSpellbook] = useState< + | { + slug: string; + title: string; + description?: string; + workspaceIds?: string[]; + localId?: string; + pubkey?: string; + } + | undefined + >(undefined); + + // Check if we're in preview mode + const isPreviewMode = location.pathname.startsWith("/preview/"); // 1. Load Local Data const localSpellbooks = useLiveQuery(() => @@ -89,10 +103,11 @@ export function SpellbookDropdown() { // Check if active spellbook is in local library const isActiveLocal = useMemo(() => { if (!activeSpellbook) return false; - return (localSpellbooks || []).some(s => s.slug === activeSpellbook.slug); + return (localSpellbooks || []).some((s) => s.slug === activeSpellbook.slug); }, [activeSpellbook, localSpellbooks]); - if (!activeAccount || (spellbooks.length === 0 && !activeSpellbook)) { + // Show dropdown if: in preview mode, has active account, or has active spellbook + if (!isPreviewMode && !activeAccount && !activeSpellbook) { return null; } @@ -176,7 +191,8 @@ export function SpellbookDropdown() { )} - {isActiveLocal && activeSpellbook.pubkey === activeAccount.pubkey ? ( + {isActiveLocal && activeAccount && + activeSpellbook.pubkey === activeAccount.pubkey ? ( )} - {/* Spellbooks Section */} - - My Layouts - - - {spellbooks.length === 0 ? ( -
- No layouts saved yet. -
- ) : ( - spellbooks.map((sb) => { - const isActive = activeSpellbook?.slug === sb.slug; - return ( + {/* Spellbooks Section - only show if user is logged in */} + {activeAccount && ( + <> + + My Layouts + + + {spellbooks.length === 0 ? ( +
+ No layouts saved yet. +
+ ) : ( + spellbooks.map((sb) => { + const isActive = activeSpellbook?.slug === sb.slug; + return ( + handleApplySpellbook(sb)} + className={cn(itemClass, isActive && "bg-muted font-bold")} + > + +
+ + {sb.title} + +
+
+ ); + }) + )} + + + + {!activeSpellbook && ( handleApplySpellbook(sb)} - className={cn(itemClass, isActive && "bg-muted font-bold")} + onClick={handleNewSpellbook} + className={itemClass} > - -
- - {sb.title} - -
+ + Save Spellbook
- ); - }) + )} + + addWindow("spellbooks", {})} + className={cn(itemClass, "text-xs opacity-70")} + > + + Manage Library + + )} - - - {!activeSpellbook && ( - - - Save current as Layout - + {/* Show message for non-logged-in users in preview mode */} + {!activeAccount && isPreviewMode && ( +
+ Log in to save and manage layouts +
)} - - addWindow("spellbooks", {})} - className={cn(itemClass, "text-xs opacity-70")} - > - - Manage Library - diff --git a/src/components/nostr/kinds/SpellbookRenderer.tsx b/src/components/nostr/kinds/SpellbookRenderer.tsx index d63969b..0bec6bd 100644 --- a/src/components/nostr/kinds/SpellbookRenderer.tsx +++ b/src/components/nostr/kinds/SpellbookRenderer.tsx @@ -83,6 +83,41 @@ function LayoutVisualizer({ if (typeof node === "string") { const window = windows[node]; const appId = window?.appId || "unknown"; + + // For req windows, show kind badges if available + if (appId === "req" && window?.props?.filter?.kinds) { + const kinds = window.props.filter.kinds; + return ( +
+ {kinds.map((kind: number) => ( + + ))} +
+ ); + } + + // Default: show appId as text return (
- {renderLayout(node.first)} - {renderLayout(node.second)} +
+ {renderLayout(node.first)} +
+
+ {renderLayout(node.second)} +
); } @@ -325,26 +366,16 @@ export function SpellbookDetailRenderer({ event }: { event: NostrEvent }) {
{sortedWorkspaces.map((ws) => { - const wsWindows = ws.windowIds.length; return (
-
-
- - Tab {ws.number} - - - {ws.label || "Untitled Tab"} - -
-
- - {wsWindows} {wsWindows === 1 ? "window" : "windows"} -
-
+ {ws.label && ( + + {ws.label} + + )} {ws.layout && (