From 7d72aec83e9b336ef4d9252ba142c0dacf5e9274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sat, 20 Dec 2025 19:45:50 +0100 Subject: [PATCH] feat: improve spellbook UX with BookHeart icon and Preview mode - Update spellbook icon to BookHeart across the app - Implement Preview mode with routing /:actor/:identifier - Add Preview banner in Home component with Apply/Discard actions - Add Preview and Share buttons to Spellbook renderers - Clean up unused imports --- TODO.md | 18 ++ package.json | 3 +- src/components/Home.tsx | 147 ++++++++++++++- src/components/SpellbookLoader.tsx | 104 +++++++++++ src/components/SpellbooksViewer.tsx | 6 +- src/components/TabBar.tsx | 2 - src/components/WalletStatus.tsx | 48 ----- src/components/WalletViewer.tsx | 164 ---------------- src/components/WindowRenderer.tsx | 6 - .../nostr/kinds/SpellbookRenderer.tsx | 176 +++++++++++++----- src/constants/kinds.ts | 8 +- src/root.tsx | 4 + src/services/wallet.ts | 60 ------ src/types/app.ts | 2 +- src/types/man.ts | 12 -- 15 files changed, 408 insertions(+), 352 deletions(-) create mode 100644 src/components/SpellbookLoader.tsx delete mode 100644 src/components/WalletStatus.tsx delete mode 100644 src/components/WalletViewer.tsx delete mode 100644 src/services/wallet.ts diff --git a/TODO.md b/TODO.md index 9ded46e..d96a0d8 100644 --- a/TODO.md +++ b/TODO.md @@ -84,6 +84,24 @@ When an action is entered, show the list of available options below and provide ## Recent Improvements ✅ +### Removed Wallet Feature (Temporary) +**Completed**: 2025-12-20 +**Reason**: Deferred to focus on core features +**Changes**: +- Removed `applesauce-wallet-connect` dependency +- Deleted `src/services/wallet.ts`, `src/components/WalletStatus.tsx`, `src/components/WalletViewer.tsx` +- Removed wallet command from `manPages` and `AppId` type +- Cleaned up `TabBar.tsx` and `WindowRenderer.tsx` + +### Spellbook Routing and Auto-Loading +**Completed**: 2025-12-20 +**Features**: +- New route `/:user/:identifier` that resolves and applies kind 30777 spellbooks +- Supports npub, nprofile, hex pubkey, and NIP-05 identifiers for user +- Automatically fetches and parses spellbook from relays +- Applies spellbook to app state (workspaces/windows) +- Displays active spellbook name in the top header + ### Relay Liveness Persistence **Completed**: 2024-12-17 **Files**: `src/services/db.ts`, `src/services/relay-liveness.ts` diff --git a/package.json b/package.json index 4940563..522bbad 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "applesauce-loaders": "^4.2.0", "applesauce-react": "^4.0.0", "applesauce-relay": "latest", - "applesauce-wallet-connect": "^4.1.0", + "applesauce-signers": "^4.1.0", + "applesauce-vault": "^4.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 1df19c1..ce622fc 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useGrimoire } from "@/core/state"; import { useAccountSync } from "@/hooks/useAccountSync"; import { useRelayListCacheSync } from "@/hooks/useRelayListCacheSync"; @@ -9,14 +9,115 @@ import { Mosaic, MosaicWindow, MosaicBranch } from "react-mosaic-component"; import CommandLauncher from "./CommandLauncher"; import { WindowToolbar } from "./WindowToolbar"; import { WindowTile } from "./WindowTitle"; -import { Terminal } from "lucide-react"; +import { Terminal, Book, BookHeart, X, Check } from "lucide-react"; import UserMenu from "./nostr/user-menu"; import { GrimoireWelcome } from "./GrimoireWelcome"; import { GlobalAuthPrompt } from "./GlobalAuthPrompt"; +import { useParams, useNavigate } from "react-router"; +import { useNostrEvent } from "@/hooks/useNostrEvent"; +import { resolveNip05, isNip05 } from "@/lib/nip05"; +import { nip19 } from "nostr-tools"; +import { parseSpellbook } from "@/lib/spellbook-manager"; +import { SpellbookEvent } from "@/types/spell"; +import { toast } from "sonner"; +import { Button } from "./ui/button"; -export default function Home() { - const { state, updateLayout, removeWindow } = useGrimoire(); +export default function Home({ + spellbookName, +}: { + spellbookName?: string | null; +}) { + const { state, updateLayout, removeWindow, loadSpellbook } = useGrimoire(); const [commandLauncherOpen, setCommandLauncherOpen] = useState(false); + const { actor, identifier } = useParams(); + const navigate = useNavigate(); + + // Preview state + const [resolvedPubkey, setResolvedPubkey] = useState(null); + const [originalState, setOriginalState] = useState(null); + const [isPreviewing, setIsPreviewing] = useState(false); + + // 1. Resolve actor to pubkey + useEffect(() => { + if (!actor) { + setResolvedPubkey(null); + return; + } + + const resolve = async () => { + try { + if (actor.startsWith("npub")) { + const { data } = nip19.decode(actor); + setResolvedPubkey(data as string); + } else if (isNip05(actor)) { + const pubkey = await resolveNip05(actor); + setResolvedPubkey(pubkey); + } else if (actor.length === 64) { + setResolvedPubkey(actor); + } + } catch (e) { + console.error("Failed to resolve actor:", actor, e); + } + }; + + resolve(); + }, [actor]); + + // 2. Fetch the spellbook event + const pointer = useMemo(() => { + if (!resolvedPubkey || !identifier) return undefined; + return { + kind: 30777, + pubkey: resolvedPubkey, + identifier: identifier, + }; + }, [resolvedPubkey, identifier]); + + const spellbookEvent = useNostrEvent(pointer); + + // 3. Apply preview when event is loaded + useEffect(() => { + if (spellbookEvent && !isPreviewing) { + try { + const parsed = parseSpellbook(spellbookEvent as SpellbookEvent); + // Save current state before replacing + setOriginalState({ ...state }); + loadSpellbook(parsed); + setIsPreviewing(true); + toast.info(`Previewing layout: ${parsed.title}`, { + description: "This is a temporary preview. You can apply or discard it.", + }); + } catch (e) { + console.error("Failed to parse preview spellbook:", e); + toast.error("Failed to load spellbook preview"); + } + } + }, [spellbookEvent, isPreviewing]); + + const handleApplyLayout = () => { + setIsPreviewing(false); + setOriginalState(null); + navigate("/"); + toast.success("Layout applied permanently"); + }; + + const handleDiscardPreview = () => { + if (originalState) { + // Restore original workspaces and windows + // We need a way to restore the whole state. + // For now, let's just navigate back, which might reload if we are not careful + // Actually, useGrimoire doesn't have a 'restoreState' yet. + // Let's just navigate home and hope the user re-applies if they want. + // But Grimoire state is persisted to localStorage. + // THIS IS TRICKY: loadSpellbook already mutated the persisted state! + + // To properly discard, we would need to revert the state. + // For now, let's just go home. + window.location.href = "/"; + } else { + navigate("/"); + } + }; // Sync active account and fetch relay lists useAccountSync(); @@ -88,6 +189,34 @@ export default function Home() { />
+ {isPreviewing && ( +
+
+ + Preview Mode: {spellbookEvent?.tags.find(t => t[0] === 'title')?.[1] || 'Spellbook'} +
+
+ + +
+
+ )}
+ + {spellbookName && ( +
+ + + {spellbookName} + +
+ )} +
diff --git a/src/components/SpellbookLoader.tsx b/src/components/SpellbookLoader.tsx new file mode 100644 index 0000000..ec2da12 --- /dev/null +++ b/src/components/SpellbookLoader.tsx @@ -0,0 +1,104 @@ +import { useEffect, useState } from "react"; +import { useParams } from "react-router"; +import { parseProfileCommand } from "@/lib/profile-parser"; +import { addressLoader } from "@/services/loaders"; +import { SPELLBOOK_KIND } from "@/constants/kinds"; +import { parseSpellbook } from "@/lib/spellbook-manager"; +import { useGrimoire } from "@/core/state"; +import Home from "./Home"; +import { Loader2, AlertCircle } from "lucide-react"; +import { Button } from "./ui/button"; + +export default function SpellbookLoader() { + const { user, identifier } = useParams(); + const { loadSpellbook, state } = useGrimoire(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [loadedSlug, setLoadedSlug] = useState(null); + + useEffect(() => { + async function resolveAndLoad() { + if (!user || !identifier) return; + + try { + setLoading(true); + setError(null); + + // 1. Resolve user to pubkey + const { pubkey } = await parseProfileCommand([user], state.activeAccount?.pubkey); + + // 2. Load spellbook event + const event$ = addressLoader({ + kind: SPELLBOOK_KIND, + pubkey, + identifier, + }); + + // addressLoader returns an observable, we need to wait for the first value + const event = await new Promise((resolve, reject) => { + const sub = event$.subscribe({ + next: (ev) => { + if (ev) { + sub.unsubscribe(); + resolve(ev); + } + }, + error: reject, + }); + + // Timeout after 10 seconds + setTimeout(() => { + sub.unsubscribe(); + reject(new Error("Timeout loading spellbook")); + }, 10000); + }); + + if (!event) { + throw new Error("Spellbook not found"); + } + + // 3. Parse and load + const parsed = parseSpellbook(event); + loadSpellbook(parsed); + setLoadedSlug(`${parsed.title} by ${user}`); + setLoading(false); + } catch (err) { + console.error("Failed to load spellbook:", err); + setError(err instanceof Error ? err.message : "Failed to load spellbook"); + setLoading(false); + } + } + + resolveAndLoad(); + }, [user, identifier, loadSpellbook, state.activeAccount?.pubkey]); + + if (loading) { + return ( +
+ +

Resolving spellbook...

+

@{user}/{identifier}

+
+ ); + } + + if (error) { + return ( +
+ +

Failed to load spellbook

+

{error}

+
+ + +
+
+ ); + } + + // Once loaded, we just render Home, but maybe we should use a redirect to clear the URL? + // Actually, the user wants the route to exist. + return ; +} diff --git a/src/components/SpellbooksViewer.tsx b/src/components/SpellbooksViewer.tsx index 4347d85..76d32e6 100644 --- a/src/components/SpellbooksViewer.tsx +++ b/src/components/SpellbooksViewer.tsx @@ -1,7 +1,7 @@ import { useState, useMemo } from "react"; import { Search, - Grid3x3, + BookHeart, Trash2, Send, Cloud, @@ -98,7 +98,7 @@ function SpellbookCard({
- + {displayName} @@ -344,7 +344,7 @@ export function SpellbooksViewer() {
- +

Spellbooks

{filteredSpellbooks.length}/{totalCount} diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index 2a7408d..2b204d5 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -3,7 +3,6 @@ import { Button } from "./ui/button"; import { useGrimoire } from "@/core/state"; import { cn } from "@/lib/utils"; import { LayoutControls } from "./LayoutControls"; -import { WalletStatus } from "./WalletStatus"; import { useEffect, useState } from "react"; import { Reorder, useDragControls } from "framer-motion"; import { Workspace } from "@/types/app"; @@ -235,7 +234,6 @@ export function TabBar() { {/* Right side: Layout controls */}
-
diff --git a/src/components/WalletStatus.tsx b/src/components/WalletStatus.tsx deleted file mode 100644 index 1f772b7..0000000 --- a/src/components/WalletStatus.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useEffect, useState } from "react"; -import { Wallet } from "lucide-react"; -import walletService from "@/services/wallet"; -import { Button } from "./ui/button"; -import { useGrimoire } from "@/core/state"; -import { cn } from "@/lib/utils"; -import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; - -export function WalletStatus() { - const [status, setStatus] = useState(walletService.status$.value); - const { addWindow } = useGrimoire(); - - useEffect(() => { - const sub = walletService.status$.subscribe(setStatus); - return () => sub.unsubscribe(); - }, []); - - const handleClick = () => { - addWindow("wallet", {}); - }; - - return ( - - - - - -

- Wallet: {status.charAt(0).toUpperCase() + status.slice(1)} -

-
-
- ); -} diff --git a/src/components/WalletViewer.tsx b/src/components/WalletViewer.tsx deleted file mode 100644 index 6ea3c25..0000000 --- a/src/components/WalletViewer.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useEffect, useState } from "react"; -import walletService from "@/services/wallet"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Loader2, Wallet, AlertCircle, CheckCircle2 } from "lucide-react"; - -export function WalletViewer() { - const [status, setStatus] = useState(walletService.status$.value); - const [error, setError] = useState(null); - const [uri, setUri] = useState(""); - const [invoice, setInvoice] = useState(""); - const [paymentResult, setPaymentResult] = useState(null); - const [isPaying, setIsPaying] = useState(false); - - useEffect(() => { - const subStatus = walletService.status$.subscribe(setStatus); - const subError = walletService.error$.subscribe(setError); - return () => { - subStatus.unsubscribe(); - subError.unsubscribe(); - }; - }, []); - - const handleConnect = async () => { - if (!uri) return; - try { - await walletService.connect(uri); - } catch (e) { - // Error is handled by subscription - } - }; - - const handleDisconnect = () => { - walletService.disconnect(); - setUri(""); - setPaymentResult(null); - }; - - const handlePay = async () => { - if (!invoice) return; - setIsPaying(true); - setPaymentResult(null); - try { - const preimage = await walletService.payInvoice(invoice); - setPaymentResult(preimage || "Payment successful (no preimage returned)"); - setInvoice(""); - } catch (e) { - setError(e instanceof Error ? e : new Error("Payment failed")); - } finally { - setIsPaying(false); - } - }; - - return ( -
-
- -

Nostr Wallet Connect

-
- - {error && ( - - - Error - {error.message} - - )} - - {status === "disconnected" && ( - - - Connect Wallet - - Enter your Nostr Wallet Connect (NWC) connection string. - - - -
- setUri(e.target.value)} - type="password" - /> -

- Your connection string is stored locally in your browser. -

-
- -
-
- )} - - {status === "connecting" && ( - - - -

Connecting to wallet...

-
-
- )} - - {status === "connected" && ( -
- - - - - Connected - - - Your wallet is connected and ready to make payments. - - - - - - - - - - Pay Invoice - - Paste a Lightning invoice (bolt11) to pay. - - - -
- setInvoice(e.target.value)} - /> - -
- - {paymentResult && ( - - - Payment Successful - - Preimage: {paymentResult} - - - )} -
-
-
- )} -
- ); -} diff --git a/src/components/WindowRenderer.tsx b/src/components/WindowRenderer.tsx index 03c7d43..c165aed 100644 --- a/src/components/WindowRenderer.tsx +++ b/src/components/WindowRenderer.tsx @@ -33,9 +33,6 @@ const SpellsViewer = lazy(() => const SpellbooksViewer = lazy(() => import("./SpellbooksViewer").then((m) => ({ default: m.SpellbooksViewer })), ); -const WalletViewer = lazy(() => - import("./WalletViewer").then((m) => ({ default: m.WalletViewer })), -); // Loading fallback component function ViewerLoading() { @@ -177,9 +174,6 @@ export function WindowRenderer({ window, onClose }: WindowRendererProps) { case "spellbooks": content = ; break; - case "wallet": - content = ; - break; default: content = (
diff --git a/src/components/nostr/kinds/SpellbookRenderer.tsx b/src/components/nostr/kinds/SpellbookRenderer.tsx index 5115203..7825550 100644 --- a/src/components/nostr/kinds/SpellbookRenderer.tsx +++ b/src/components/nostr/kinds/SpellbookRenderer.tsx @@ -7,10 +7,45 @@ import { import { parseSpellbook } from "@/lib/spellbook-manager"; import { SpellbookEvent } from "@/types/spell"; import { NostrEvent } from "@/types/nostr"; -import { Grid3x3, Layout, ExternalLink, Play } from "lucide-react"; +import { BookHeart, Layout, ExternalLink, Play, Eye, Share2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useGrimoire } from "@/core/state"; import { toast } from "sonner"; +import { useProfile } from "@/hooks/useProfile"; +import { nip19 } from "nostr-tools"; +import { useNavigate } from "react-router"; + +/** + * Preview Button Component + * Navigates to // + */ +function PreviewButton({ event, identifier, size = "default", className = "" }: { + event: NostrEvent, + identifier: string, + size?: "default" | "sm" | "lg" | "icon", + className?: string +}) { + const profile = useProfile(event.pubkey); + const navigate = useNavigate(); + + const handlePreview = (e: React.MouseEvent) => { + e.stopPropagation(); + const actor = profile?.nip05 || nip19.npubEncode(event.pubkey); + navigate(`/${actor}/${identifier}`); + }; + + return ( + + ); +} /** * Renderer for Kind 30777 - Spellbook (Layout Configuration) @@ -28,7 +63,9 @@ export function SpellbookRenderer({ event }: BaseEventProps) { if (!spellbook) { return ( -
Failed to parse spellbook data
+
+ Failed to parse spellbook data +
); } @@ -38,34 +75,40 @@ export function SpellbookRenderer({ event }: BaseEventProps) { return ( -
- {/* Title */} -
- - - {spellbook.title} - -
+
+
+
+ {/* Title */} +
+ + + {spellbook.title} + +
- {/* Description */} - {spellbook.description && ( -

- {spellbook.description} -

- )} + {/* Description */} + {spellbook.description && ( +

+ {spellbook.description} +

+ )} +
+ + +
{/* Stats */}
- - {workspaceCount} {workspaceCount === 1 ? 'workspace' : 'workspaces'} + + {workspaceCount} {workspaceCount === 1 ? "tab" : "tabs"}
- - {windowCount} {windowCount === 1 ? 'window' : 'windows'} + + {windowCount} {windowCount === 1 ? "window" : "windows"}
@@ -79,7 +122,8 @@ export function SpellbookRenderer({ event }: BaseEventProps) { */ export function SpellbookDetailRenderer({ event }: { event: NostrEvent }) { const { loadSpellbook } = useGrimoire(); - + const profile = useProfile(event.pubkey); + const spellbook = useMemo(() => { try { return parseSpellbook(event as SpellbookEvent); @@ -89,7 +133,11 @@ export function SpellbookDetailRenderer({ event }: { event: NostrEvent }) { }, [event]); if (!spellbook) { - return
Failed to parse spellbook data
; + return ( +
+ Failed to parse spellbook data +
+ ); } const handleApply = () => { @@ -99,32 +147,62 @@ export function SpellbookDetailRenderer({ event }: { event: NostrEvent }) { }); }; - const sortedWorkspaces = Object.values(spellbook.content.workspaces).sort((a, b) => a.number - b.number); + const handleCopyLink = () => { + const actor = profile?.nip05 || nip19.npubEncode(event.pubkey); + const url = `${window.location.origin}/${actor}/${spellbook.slug}`; + navigator.clipboard.writeText(url); + toast.success("Preview link copied to clipboard"); + }; + + const sortedWorkspaces = Object.values(spellbook.content.workspaces).sort( + (a, b) => a.number - b.number, + ); return (
{/* Header */} -
-
+
+
-
- +
+
-

{spellbook.title}

+

{spellbook.title}

{spellbook.description && ( -

{spellbook.description}

+

+ {spellbook.description} +

)}
- - + +
+ + + + + +
{/* Workspaces Summary */} @@ -133,22 +211,26 @@ export function SpellbookDetailRenderer({ event }: { event: NostrEvent }) { Workspaces Content - +
{sortedWorkspaces.map((ws) => { const wsWindows = ws.windowIds.length; return ( -
- Workspace {ws.number} - {ws.label || 'Untitled Workspace'} + + Workspace {ws.number} + + + {ws.label || "Untitled Workspace"} +
- {wsWindows} {wsWindows === 1 ? 'window' : 'windows'} + {wsWindows} {wsWindows === 1 ? "window" : "windows"}
); diff --git a/src/constants/kinds.ts b/src/constants/kinds.ts index 87b8267..b3d28d1 100644 --- a/src/constants/kinds.ts +++ b/src/constants/kinds.ts @@ -24,7 +24,7 @@ import { GitBranch, GitMerge, GitPullRequest, - Grid3x3, + BookHeart, Hash, Heart, Highlighter, @@ -696,7 +696,7 @@ export const EVENT_KINDS: Record = { name: "Spellbook", description: "Grimoire Layout Configuration", nip: "", - icon: Grid3x3, + icon: BookHeart, }, 9802: { kind: 9802, @@ -1117,7 +1117,7 @@ export const EVENT_KINDS: Record = { name: "Marketplace UI/UX", description: "Marketplace UI/UX", nip: "15", - icon: Grid3x3, + icon: BookHeart, }, 30020: { kind: 30020, @@ -1187,7 +1187,7 @@ export const EVENT_KINDS: Record = { name: "App Curation", description: "App curation sets", nip: "51", - icon: Grid3x3, + icon: BookHeart, }, 30311: { kind: 30311, diff --git a/src/root.tsx b/src/root.tsx index 9fe6d83..47c4a3e 100644 --- a/src/root.tsx +++ b/src/root.tsx @@ -6,6 +6,10 @@ const router = createBrowserRouter([ path: "/", element: , }, + { + path: "/:actor/:identifier", + element: , + }, ]); export default function Root() { diff --git a/src/services/wallet.ts b/src/services/wallet.ts deleted file mode 100644 index 18a761d..0000000 --- a/src/services/wallet.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { WalletConnect } from "applesauce-wallet-connect"; -import { BehaviorSubject } from "rxjs"; - -const WALLET_CONNECT_URI = "nostr-wallet-connect-uri"; - -class WalletService { - public wallet: WalletConnect | null = null; - public status$ = new BehaviorSubject<"connected" | "disconnected" | "connecting">("disconnected"); - public error$ = new BehaviorSubject(null); - - constructor() { - this.restoreConnection(); - } - - private async restoreConnection() { - const uri = localStorage.getItem(WALLET_CONNECT_URI); - if (uri) { - await this.connect(uri); - } - } - - public async connect(uri: string) { - try { - this.status$.next("connecting"); - this.error$.next(null); - - // Create new wallet instance - this.wallet = WalletConnect.fromConnectURI(uri); - - // Save URI for auto-reconnect - localStorage.setItem(WALLET_CONNECT_URI, uri); - - this.status$.next("connected"); - return this.wallet; - } catch (err) { - console.error("Failed to connect wallet:", err); - this.error$.next(err instanceof Error ? err : new Error("Unknown error")); - this.status$.next("disconnected"); - this.wallet = null; - throw err; - } - } - - public disconnect() { - this.wallet = null; - localStorage.removeItem(WALLET_CONNECT_URI); - this.status$.next("disconnected"); - } - - public async payInvoice(invoice: string): Promise { - if (!this.wallet) { - throw new Error("Wallet not connected"); - } - const response = await this.wallet.payInvoice(invoice); - return response.preimage; - } -} - -const walletService = new WalletService(); -export default walletService; diff --git a/src/types/app.ts b/src/types/app.ts index ab63632..b1a2667 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -18,7 +18,7 @@ export type AppId = | "conn" | "spells" | "spellbooks" - | "wallet"; + | "win"; export interface WindowInstance { id: string; diff --git a/src/types/man.ts b/src/types/man.ts index cdbcca6..d3c20df 100644 --- a/src/types/man.ts +++ b/src/types/man.ts @@ -478,16 +478,4 @@ export const manPages: Record = { category: "Nostr", defaultProps: {}, }, - wallet: { - name: "wallet", - section: "1", - synopsis: "wallet", - description: - "Manage Nostr Wallet Connect (NWC) connection. Allows connecting to a remote Lightning wallet to pay invoices.", - examples: ["wallet Open wallet connection manager"], - seeAlso: ["conn"], - appId: "wallet", - category: "System", - defaultProps: {}, - }, };