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 && (