"use client"; /** * Floating hover toolbar rendered in the row's left gutter: grip drag * handle (dnd-kit listeners passed in from the sortable row), a "type" * button (opens the TypeMenu turn-into grid) and a "more" menu (delete, * duplicate — actions are dispatched via the editor store / direct API). */ import { useState } from "react"; import { MoreHorizontal, Type as TypeIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import { useEditorStore } from "@/stores/editor-store"; import { toast, useUiStore } from "@/stores/ui-store"; import { blockApi } from "@/lib/api"; import { useUpdateBlock } from "@/hooks/use-blocks"; import { AskAiSubmenu } from "@/components/ai/ask-ai-submenu"; import { TypeMenu } from "./type-menu"; import type { WritingAction } from "@/lib/ai"; import type { Block, BlockType } from "@/lib/types"; interface BlockToolbarProps { block: Block; /** dnd-kit useSortable attributes + listeners for the grip handle. */ gripProps?: { attributes?: Record; listeners?: Record; }; /** TASK-0123 — a writing action from the "Ask AI" submenu (the parent * opens the WritingDialog for this single block). */ onWritingAction?: (action: WritingAction, blockIds: string[], language?: string) => void; } export function BlockToolbar({ block, gripProps, onWritingAction }: BlockToolbarProps) { const [typeOpen, setTypeOpen] = useState(false); const [moreOpen, setMoreOpen] = useState(false); const setAiPanel = useUiStore((s) => s.setAiPanel); const { updateBlock } = useUpdateBlock(block.pageId); const turnInto = async (type: BlockType) => { setTypeOpen(false); if (type === block.type) return; try { // useUpdateBlock INVALIDATES the page's block query on success — the // row re-renders with the new type immediately (the old fire-and-forget // `blockApi.update` never invalidated, so "Turn into" appeared dead). // The API preserves content and clears type-specific properties on a // type change. await updateBlock(block.id, { type }, true); } catch (e) { toast("error", "Turn into failed", e instanceof Error ? e.message : undefined); } }; const duplicate = async () => { setMoreOpen(false); try { const dup = await blockApi.duplicate(block.id, { afterBlockId: block.id }); useEditorStore.getState().setSelection(dup.id); } catch (e) { toast("error", "Duplicate failed", e instanceof Error ? e.message : undefined); } }; const remove = () => { setMoreOpen(false); void blockApi.remove(block.id); useEditorStore.getState().setSelection(null); }; return (
{gripProps ? ( ) : null}
{typeOpen ? : null}
{moreOpen ? (
{ setMoreOpen(false); setAiPanel(true, { pageId: block.pageId, blockId: block.id }); }} onWritingAction={(action, blockIds, language) => { setMoreOpen(false); onWritingAction?.(action, blockIds, language); }} />
{/* "Turn into" is also reachable from the ⋮ menu (QA issue #4: the three-dots menu should offer converting the block). It opens the same TypeMenu the "Aa" button does. */}
) : null}
); }