Files
grimoire/src/components/WindowToolbar.tsx
Alejandro 7241b3fb5a feat: add copy button for NIP markdown (#26)
* feat: add copy button for NIP markdown

- Add copy button to WindowToolbar for regular NIPs (appId: "nip")
  - Button appears in window toolbar next to edit button
  - Uses Copy/CopyCheck icons from lucide-react
  - Fetches NIP content via useNip hook
  - Shows toast notification on successful copy

- Add copy button to CommunityNIPDetailRenderer for community NIPs (kind 30817)
  - Button appears in header next to title
  - Copies event.content (markdown) to clipboard
  - Uses same Copy/CopyCheck icon pattern
  - Shows toast notification on successful copy

Both implementations use the existing useCopy hook for state management
and maintain consistent styling with other toolbar buttons.

* refactor: use Button component and remove misleading shortcuts

- Replace native button elements with Button component from shadcn/ui
  - Use variant="ghost" and size="icon" for consistent styling
  - Apply h-8 w-8 classes for uniform button sizing
  - Remove manual className styling in favor of component variants

- Remove misleading keyboard shortcut hints from button titles
  - Changed "Edit command (Cmd+E)" to "Edit command"
  - Changed "Close window (Cmd+W)" to "Close window"
  - These shortcuts don't actually work, so they were misleading

- Clean up imports and formatting
  - Format lucide-react imports across multiple lines
  - Add Button component import
  - Run prettier for consistent code style

* refactor: use link variant and remove size class overrides

- Change all Button components from variant="ghost" to variant="link"
- Remove className="h-8 w-8" overrides to use default Button sizing
- Maintains size="icon" for proper icon button behavior
- Applies to WindowToolbar and CommunityNIPDetailRenderer

* style: add muted color to window toolbar icon buttons

- Add className="text-muted-foreground" to all Button components in WindowToolbar
- Improves visual contrast for toolbar buttons
- Applies to Edit, Copy NIP, More actions, and Close window buttons

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-23 00:00:52 +01:00

186 lines
4.9 KiB
TypeScript

import {
X,
Pencil,
MoreVertical,
WandSparkles,
Copy,
CopyCheck,
} from "lucide-react";
import { useSetAtom } from "jotai";
import { useState } from "react";
import { WindowInstance } from "@/types/app";
import { commandLauncherEditModeAtom } from "@/core/command-launcher-state";
import { reconstructCommand } from "@/lib/command-reconstructor";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { SpellDialog } from "@/components/nostr/SpellDialog";
import { reconstructCommand as reconstructReqCommand } from "@/lib/spell-conversion";
import { toast } from "sonner";
import { useCopy } from "@/hooks/useCopy";
import { useNip } from "@/hooks/useNip";
interface WindowToolbarProps {
window?: WindowInstance;
onClose?: () => void;
onEditCommand?: () => void; // Callback to open CommandLauncher
}
export function WindowToolbar({
window,
onClose,
onEditCommand,
}: WindowToolbarProps) {
const setEditMode = useSetAtom(commandLauncherEditModeAtom);
const [showSpellDialog, setShowSpellDialog] = useState(false);
const handleEdit = () => {
if (!window) return;
// Get command string (existing or reconstructed)
const commandString = window.commandString || reconstructCommand(window);
// Set edit mode state
setEditMode({
windowId: window.id,
initialCommand: commandString,
});
// Open CommandLauncher
if (onEditCommand) {
onEditCommand();
}
};
const handleTurnIntoSpell = () => {
if (!window) return;
// Only available for REQ windows
if (window.appId !== "req") {
toast.error("Only REQ windows can be turned into spells");
return;
}
setShowSpellDialog(true);
};
// Copy functionality for NIPs
const { copy, copied } = useCopy();
const isNipWindow = window?.appId === "nip";
// Fetch NIP content for regular NIPs
const { content: nipContent } = useNip(
isNipWindow && window?.props?.number ? window.props.number : "",
);
const handleCopyNip = () => {
if (!window || !nipContent) return;
copy(nipContent);
toast.success("NIP markdown copied to clipboard");
};
// Check if this is a REQ window for spell creation
const isReqWindow = window?.appId === "req";
// Get REQ command for spell dialog
const reqCommand =
isReqWindow && window
? window.commandString ||
reconstructReqCommand(
window.props?.filter || {},
window.props?.relays,
undefined,
undefined,
window.props?.closeOnEose,
)
: "";
return (
<>
{window && (
<>
{/* Edit button */}
<Button
variant="link"
size="icon"
className="text-muted-foreground"
onClick={handleEdit}
title="Edit command"
aria-label="Edit command"
>
<Pencil className="size-4" />
</Button>
{/* Copy button for NIPs */}
{isNipWindow && (
<Button
variant="link"
size="icon"
className="text-muted-foreground"
onClick={handleCopyNip}
title="Copy NIP markdown"
aria-label="Copy NIP markdown"
disabled={!nipContent}
>
{copied ? <CopyCheck /> : <Copy />}
</Button>
)}
{/* More actions menu - only for REQ windows for now */}
{isReqWindow && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="link"
size="icon"
className="text-muted-foreground"
title="More actions"
aria-label="More actions"
>
<MoreVertical className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleTurnIntoSpell}>
<WandSparkles className="size-4 mr-2" />
Save as spell
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{/* Spell Dialog */}
{isReqWindow && (
<SpellDialog
open={showSpellDialog}
onOpenChange={setShowSpellDialog}
mode="create"
initialCommand={reqCommand}
onSuccess={() => {
toast.success("Spell published successfully!");
}}
/>
)}
</>
)}
{onClose && (
<Button
variant="link"
size="icon"
className="text-muted-foreground"
onClick={onClose}
title="Close window"
aria-label="Close window"
>
<X className="size-4" />
</Button>
)}
</>
);
}