# @blinksgg/canvas - LLM API Reference (v3.0) > A batteries-included canvas library for node-based editors with drag, resize, pan/zoom, selection, undo/redo, and Supabase sync. Uses **React 19.2+** with the **React Compiler** — zero manual `useCallback`/`useMemo` in the codebase. ## Quick Start ```tsx import { Canvas, CanvasProvider, CanvasDbProvider, registerNodeTypes, useDeleteNode, useCreateNode, } from '@blinksgg/canvas'; // 1. Register your node type components registerNodeTypes({ 'my-node': MyNodeComponent, }); // 2. Wrap your app with providers ``` --- ## CRUD Operations (Database Hooks) These hooks handle database operations with optimistic updates: | Hook | Purpose | Usage | |------|---------|-------| | `useCreateNode` | Create a new node | `const { mutate } = useCreateNode()` | | `useUpdateNode` | Update node properties | `const { mutate } = useUpdateNode()` | | `useDeleteNode` | Delete a node | `const { mutate } = useDeleteNode()` | | `useCreateEdge` | Create an edge between nodes | `const { mutate } = useCreateEdge()` | | `useUpdateEdge` | Update edge properties | `const { mutate } = useUpdateEdge()` | | `useDeleteEdge` | Delete an edge | `const { mutate } = useDeleteEdge()` | | `useGraphNodes` | Fetch all nodes for a graph | `const { data } = useGraphNodes(graphId)` | | `useGraphEdges` | Fetch all edges for a graph | `const { data } = useGraphEdges(graphId)` | ### Example: Delete a Node ```tsx import { useDeleteNode } from '@blinksgg/canvas'; function MyComponent() { const deleteNode = useDeleteNode(); const handleDelete = (nodeId: string, graphId: string) => { deleteNode.mutate({ nodeId, graphId }); }; } ``` ### Example: Create a Node ```tsx import { useCreateNode } from '@blinksgg/canvas'; function MyComponent() { const createNode = useCreateNode(); const handleCreate = () => { createNode.mutate({ graphId: 'my-graph-id', node: { label: 'New Node', node_type: 'my-node', ui_properties: { x: 100, y: 100, width: 200, height: 150 }, }, }); }; } ``` --- ## Selection Management ### Atoms (for Jotai) | Atom | Purpose | |------|---------| | `selectedNodeIdsAtom` | Set of selected node IDs | | `selectedEdgeIdAtom` | Currently selected edge ID | | `hasSelectionAtom` | Boolean: any nodes selected? | | `selectedNodesCountAtom` | Number of selected nodes | ### Actions | Atom | Purpose | |------|---------| | `selectSingleNodeAtom` | Select only one node | | `toggleNodeInSelectionAtom` | Toggle node in/out of selection | | `addNodesToSelectionAtom` | Add nodes to selection | | `removeNodesFromSelectionAtom` | Remove nodes from selection | | `clearSelectionAtom` | Clear all selection | | `selectEdgeAtom` | Select an edge | | `clearEdgeSelectionAtom` | Clear edge selection | ### Hook ```tsx import { useCanvasSelection } from '@blinksgg/canvas'; function MyComponent() { const { selectedNodeIds, count, hasSelection, hasEdgeSelection } = useCanvasSelection(); } ``` --- ## Virtualization (Performance) Canvas uses viewport virtualization to only render nodes and edges visible on screen. This dramatically improves performance for large graphs (100+ nodes). ### How It Works - Only nodes within the viewport bounds (plus 200px buffer) are rendered - Edges are only rendered if both source and target nodes are visible - Virtualization is enabled by default and persisted to localStorage ### Settings Integration Virtualization is controlled through the canvas settings system, which persists user preferences: ```tsx import { useCanvasSettings } from '@blinksgg/canvas'; function VirtualizationToggle() { const settings = useCanvasSettings(); // Access virtualizationEnabled from settings } ``` ### Atoms | Atom | Purpose | |------|---------| | `virtualizationEnabledAtom` | Whether virtualization is on (persisted) | | `setVirtualizationEnabledAtom` | Set virtualization enabled/disabled | | `toggleVirtualizationAtom` | Toggle virtualization on/off | | `visibleBoundsAtom` | Current visible area in world coords | | `visibleNodeKeysAtom` | Node IDs visible in viewport | | `visibleEdgeKeysAtom` | Edge keys visible in viewport | | `virtualizationMetricsAtom` | Debug metrics | ### Hook ```tsx import { useVirtualization } from '@blinksgg/canvas'; function PerformanceMonitor() { const { enabled, // Is virtualization on? totalNodes, // Total nodes in graph visibleNodes, // Nodes being rendered culledNodes, // Nodes not rendered (off-screen) totalEdges, visibleEdges, culledEdges, bounds, // Current visible bounds { minX, minY, maxX, maxY } enable, // Enable virtualization disable, // Disable (render all) toggle, // Toggle on/off } = useVirtualization(); return (
Rendering {visibleNodes}/{totalNodes} nodes ({culledNodes} culled)
); } ``` ### Disable Virtualization For debugging or small graphs: ```tsx import { useSetAtom } from 'jotai'; import { setVirtualizationEnabledAtom, toggleVirtualizationAtom } from '@blinksgg/canvas'; // Set directly const setEnabled = useSetAtom(setVirtualizationEnabledAtom); setEnabled(false); // Render all nodes // Or toggle const toggle = useSetAtom(toggleVirtualizationAtom); toggle(); // Toggle on/off ``` --- ## Viewport (Pan/Zoom) ### Atoms | Atom | Purpose | |------|---------| | `zoomAtom` | Current zoom level (0.1 to 3) | | `panAtom` | Current pan offset { x, y } | | `viewportRectAtom` | Viewport dimensions | | `screenToWorldAtom` | Convert screen coords to world | | `worldToScreenAtom` | Convert world coords to screen | ### Actions | Atom | Purpose | |------|---------| | `setZoomAtom` | Set zoom level | | `resetViewportAtom` | Reset to default view | ### Hook ```tsx import { useCanvasViewport } from '@blinksgg/canvas'; function MyComponent() { const { zoom, pan, screenToWorld, worldToScreen } = useCanvasViewport(); } ``` --- ## Layout (Fit-to-View, Force Layout, Tree, Grid) ### Layout Hooks | Hook | Purpose | |------|---------| | `useLayout` | Combined layout utilities (fitToBounds, bounds) | | `useFitToBounds` | Fit viewport to show nodes | | `useGetGraphBounds` | Calculate bounding rect of all nodes | | `useSelectionBounds` | Calculate bounding rect of selected nodes | | `useForceLayout` | Apply force-directed layout to arrange nodes | | `useTreeLayout` | Hierarchical tree layout (top-down or left-right) | | `useGridLayout` | Uniform grid layout (auto columns, spatial sort) | | `useAnimatedLayout` | Shared animated position interpolation hook | ### Fit to Bounds Automatically zoom and pan to show all nodes or selection: ```tsx import { useFitToBounds, FitToBoundsMode } from '@blinksgg/canvas/hooks'; function MyComponent() { const { fitToBounds } = useFitToBounds(); // Fit all nodes in view with 50px padding const handleFitAll = () => { fitToBounds(FitToBoundsMode.Graph, 50); }; // Fit only selected nodes const handleFitSelection = () => { fitToBounds(FitToBoundsMode.Selection, 20); }; } ``` ### Graph Bounds Get the bounding rectangle of all nodes: ```tsx import { useGetGraphBounds } from '@blinksgg/canvas/hooks'; function MyComponent() { const { bounds, nodes } = useGetGraphBounds(); console.log(`Graph spans ${bounds.width}x${bounds.height}`); console.log(`Top-left at (${bounds.x}, ${bounds.y})`); } ``` ### Force Layout Apply D3 force-directed layout to arrange nodes without overlap: ```tsx import { useForceLayout } from '@blinksgg/canvas/hooks'; function MyComponent() { // Basic usage (no persistence) const { applyForceLayout } = useForceLayout(); const handleArrange = async () => { await applyForceLayout(); // Nodes are now arranged without overlap }; } ``` With database persistence callback: ```tsx import { useForceLayout, type NodePositionUpdate } from '@blinksgg/canvas/hooks'; function MyComponent() { const { applyForceLayout } = useForceLayout({ onPositionsChanged: async (updates: NodePositionUpdate[]) => { // Save all positions to your database for (const { nodeId, position } of updates) { await savePosition(nodeId, position); } }, maxIterations: 1000, // Default: 1000 chargeStrength: -6000, // Default: -6000 (more negative = stronger repulsion) linkStrength: 0.03, // Default: 0.03 }); } ``` ### Layout Utilities (Pure Functions) Available from `@blinksgg/canvas/utils`: | Function | Purpose | |----------|---------| | `calculateBounds(nodes)` | Calculate bounding rect from array of Rect | | `checkNodesOverlap(node1, node2)` | Check if two nodes overlap | | `getNodeCenter(node)` | Get center point of a node | | `setNodeCenter(node, x, y)` | Set node position by center | | `getNodeBounds(node)` | Get full bounds with edges | | `areNodesClose(node1, node2, threshold)` | Check if nodes are within distance | ```tsx import { calculateBounds, checkNodesOverlap, type Rect } from '@blinksgg/canvas/utils'; const nodes: Rect[] = [ { x: 0, y: 0, width: 100, height: 100 }, { x: 200, y: 150, width: 100, height: 100 }, ]; const bounds = calculateBounds(nodes); // bounds = { x: 0, y: 0, width: 300, height: 250 } const overlaps = checkNodesOverlap(nodes[0], nodes[1]); // overlaps = false ``` --- ## Undo/Redo (History) ### Atoms | Atom | Purpose | |------|---------| | `canUndoAtom` | Boolean: can undo? | | `canRedoAtom` | Boolean: can redo? | | `undoCountAtom` | Number of undo steps available | | `redoCountAtom` | Number of redo steps available | | `historyLabelsAtom` | Labels for history entries | ### Actions | Atom | Purpose | |------|---------| | `pushHistoryAtom` | Push current state to history | | `undoAtom` | Undo last action | | `redoAtom` | Redo last undone action | | `clearHistoryAtom` | Clear history stack | ### Hook ```tsx import { useCanvasHistory } from '@blinksgg/canvas'; function MyComponent() { const { undo, redo, canUndo, canRedo, pushHistory } = useCanvasHistory(); // Before making changes, save state pushHistory('Move nodes'); // User can then undo/redo if (canUndo) undo(); if (canRedo) redo(); } ``` --- ## Node Drag & Resize ### Hooks ```tsx import { useNodeDrag, useNodeResize } from '@blinksgg/canvas'; // For drag functionality const { bind, isDragging } = useNodeDrag(nodeId, nodeData); // For resize functionality const { localWidth, localHeight, isResizing, createResizeStart } = useNodeResize({ id: nodeId, nodeData, updateNodePositions, }); ``` --- ## Node Type Registry Register custom components for different node types: ```tsx import { registerNodeType, registerNodeTypes, getNodeTypeComponent } from '@blinksgg/canvas'; // Register single type registerNodeType('note', NoteNodeComponent); // Register multiple types registerNodeTypes({ 'note': NoteNodeComponent, 'image': ImageNodeComponent, 'code': CodeNodeComponent, }); // Check if type exists const hasType = hasNodeTypeComponent('note'); // Get all registered types const types = getRegisteredNodeTypes(); // ['note', 'image', 'code'] ``` --- ## Bundled Node Components ### NoteNode A rich text editor node using BlockNote: ```tsx import { NoteNode, type NoteNodeStorage } from '@blinksgg/canvas'; // Implement storage adapter const storage: NoteNodeStorage = { content: htmlContent, onChange: (html) => saveToBackend(html), subscribe: (callback) => subscribeToChanges(callback), isLoading: false, }; ``` --- ## Components | Component | Purpose | |-----------|---------| | `Canvas` | Main canvas container | | `CanvasProvider` | Provides canvas context | | `CanvasDbProvider` | Provides Supabase client | | `CanvasStyleProvider` | Theming and styles | | `InputProvider` / `GestureProvider` | Gesture system v2 context | | `Viewport` | Pan/zoom container | | `NodeRenderer` | Renders all nodes | | `Node` | Single node wrapper | | `GroupNode` | Collapsible group container | | `ConnectedNode` | Node with connections | | `EdgeRenderer` | Renders all edges | | `Grid` | Background grid | | `Minimap` | Canvas overview with draggable viewport | | `SelectionOverlay` | Lasso/rect selection | | `ViewportControls` | Zoom +/- buttons | | `ResizeHandle` | Node resize handles | | `NodeContextMenu` | Right-click menu | | `NodeErrorBoundary` | Error boundary for nodes | | `LockedNodeOverlay` | Full-screen node view | | `EdgeOverlay` | Edge creation overlay | | `EdgeLabelEditor` | Inline edge label editing | | `AlignmentGuides` | Snap alignment lines | | `CanvasAnimations` | CSS animation injection | | `NodeTypeCombobox` | Combobox to select and add nodes | | `CommandLine` | Command palette UI | | `CommandFeedbackOverlay` | Command input feedback | --- ## Graph State (Core Atoms) | Atom | Purpose | |------|---------| | `graphAtom` | Graphology graph instance | | `currentGraphIdAtom` | Active graph ID | | `uiNodesAtom` | All nodes as UINodeState[] | | `nodeKeysAtom` | Array of node IDs | | `edgeKeysAtom` | Array of edge keys | ### Local Graph Mutations (Optimistic) | Atom | Purpose | |------|---------| | `addNodeToLocalGraphAtom` | Add node to local graph | | `addEdgeToLocalGraphAtom` | Add edge to local graph | | `removeEdgeFromLocalGraphAtom` | Remove edge from local graph | | `optimisticDeleteNodeAtom` | Optimistically delete node | | `optimisticDeleteEdgeAtom` | Optimistically delete edge | --- ## Sync Status | Atom | Purpose | |------|---------| | `syncStatusAtom` | 'synced' | 'syncing' | 'error' | 'offline' | | `isOnlineAtom` | Boolean: is online? | | `pendingMutationsCountAtom` | Count of pending mutations | | `lastSyncErrorAtom` | Last sync error message | | `mutationQueueAtom` | Queue of pending mutations | --- ## Locked Node (Full-screen View) | Atom | Purpose | |------|---------| | `lockedNodeIdAtom` | ID of locked node | | `lockedNodeDataAtom` | Data of locked node | | `hasLockedNodeAtom` | Boolean: is a node locked? | | `lockNodeAtom` | Lock a node (full-screen) | | `unlockNodeAtom` | Unlock the node | --- ## Direct Query Functions For advanced use cases, direct Supabase queries: ```tsx import { fetchGraphNodes, fetchGraphNode, createGraphNode, updateGraphNode, deleteGraphNode, fetchGraphEdges, createGraphEdge, updateGraphEdge, deleteGraphEdge, } from '@blinksgg/canvas'; // Use with your own Supabase client const nodes = await fetchGraphNodes(supabase, graphId); await deleteGraphNode(supabase, nodeId); ``` --- ## Types Key TypeScript types: ```tsx import type { // Node types DBGraphNode, UINodeState, NodeId, GraphNodeAttributes, NodePosition, // Edge types DBGraphEdge, UIEdgeState, EdgeKey, GraphEdgeAttributes, // Component props NodeTypeComponentProps, NodeRenderProps, CanvasProps, // Storage adapters NoteNodeStorage, NoteNodeProps, } from '@blinksgg/canvas'; ``` --- ## Import Paths ```tsx // Main entry (everything) import { ... } from '@blinksgg/canvas'; // Specific subpaths import { ... } from '@blinksgg/canvas/core'; // State atoms only import { ... } from '@blinksgg/canvas/db'; // Database layer import { ... } from '@blinksgg/canvas/hooks'; // React hooks import { ... } from '@blinksgg/canvas/nodes'; // Bundled node components import { ... } from '@blinksgg/canvas/utils'; // Layout utilities, geometry functions import { ... } from '@blinksgg/canvas/gestures'; // Gesture system v2 pipeline import { ... } from '@blinksgg/canvas/commands'; // Command palette system ``` ### Peer Dependencies | Package | Version | |---------|---------| | `react` / `react-dom` | ^19.2 | | `jotai` | ^2.6 | | `d3-force` | ^3.0 (optional) | | `@tanstack/react-query` | ^5.17 (optional) | > **React Compiler**: The codebase has zero manual `useCallback`/`useMemo`/`React.memo`. The React Compiler (`babel-plugin-react-compiler`) auto-memoizes everything at build time. --- ## Common Patterns ### Delete Selected Nodes ```tsx import { useDeleteNode, useCanvasSelection } from '@blinksgg/canvas'; import { useAtomValue } from 'jotai'; import { currentGraphIdAtom } from '@blinksgg/canvas'; function DeleteButton() { const deleteNode = useDeleteNode(); const { selectedNodeIds } = useCanvasSelection(); const graphId = useAtomValue(currentGraphIdAtom); const handleDeleteSelected = () => { selectedNodeIds.forEach(nodeId => { deleteNode.mutate({ nodeId, graphId }); }); }; return ; } ``` ### Create Node at Click Position ```tsx import { useCreateNode, screenToWorldAtom } from '@blinksgg/canvas'; import { useAtomValue } from 'jotai'; function handleCanvasClick(e: React.MouseEvent, graphId: string) { const screenToWorld = useAtomValue(screenToWorldAtom); const createNode = useCreateNode(); const worldPos = screenToWorld({ x: e.clientX, y: e.clientY }); createNode.mutate({ graphId, node: { label: 'New Node', node_type: 'default', ui_properties: { x: worldPos.x, y: worldPos.y, width: 200, height: 150 }, }, }); } ``` ### Undo/Redo with Keyboard ```tsx import { useCanvasHistory } from '@blinksgg/canvas'; import { useEffect } from 'react'; function UndoRedoKeyboard() { const { undo, redo, canUndo, canRedo } = useCanvasHistory(); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.metaKey || e.ctrlKey) { if (e.key === 'z' && !e.shiftKey && canUndo) { e.preventDefault(); undo(); } if ((e.key === 'z' && e.shiftKey || e.key === 'y') && canRedo) { e.preventDefault(); redo(); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }); // React Compiler auto-tracks dependencies — no dep array needed return null; } ``` --- ## Plugin System Register canvas plugins that provide custom node types, actions, or behaviors: ```tsx import { usePlugin, usePlugins } from '@blinksgg/canvas'; import type { CanvasPlugin } from '@blinksgg/canvas'; const myPlugin: CanvasPlugin = { id: 'my-plugin', name: 'My Plugin', nodeTypes: { 'custom': CustomNode }, }; // Single plugin function App() { usePlugin(myPlugin); return ; } // Multiple plugins (dependencies should come first) function App() { usePlugins([basePlugin, extensionPlugin]); return ; } ``` --- ## Delete Operations Delete nodes and edges using either database hooks (persisted) or optimistic atoms (local-only). ### Database Hooks (Persisted Delete) | Hook | Purpose | |------|---------| | `useDeleteNode` | Delete node from database | | `useDeleteEdge` | Delete edge from database | ```tsx import { useDeleteNode, useDeleteEdge } from '@blinksgg/canvas'; function MyComponent() { const deleteNode = useDeleteNode(); const deleteEdge = useDeleteEdge(); // Delete a node (also removes connected edges) deleteNode.mutate({ nodeId: 'node-123', graphId: 'graph-id' }); // Delete an edge deleteEdge.mutate({ edgeId: 'edge-456', graphId: 'graph-id' }); } ``` ### Optimistic Atoms (Local-Only Delete) For local-only operations without database sync: | Atom | Purpose | |------|---------| | `optimisticDeleteNodeAtom` | Remove node from local graph instantly | | `optimisticDeleteEdgeAtom` | Remove edge from local graph instantly | ```tsx import { useSetAtom } from 'jotai'; import { optimisticDeleteNodeAtom, optimisticDeleteEdgeAtom } from '@blinksgg/canvas'; function MyComponent() { const deleteNode = useSetAtom(optimisticDeleteNodeAtom); const deleteEdge = useSetAtom(optimisticDeleteEdgeAtom); // Delete node from local graph (no database sync) deleteNode('node-123'); // Delete edge from local graph (no database sync) deleteEdge('source-id', 'target-id'); } ``` ### Delete with Selection ```tsx import { useDeleteNode, useCanvasSelection, currentGraphIdAtom } from '@blinksgg/canvas'; import { useAtomValue } from 'jotai'; function DeleteSelectedButton() { const deleteNode = useDeleteNode(); const { selectedNodeIds } = useCanvasSelection(); const graphId = useAtomValue(currentGraphIdAtom); const handleDelete = () => { selectedNodeIds.forEach(nodeId => { deleteNode.mutate({ nodeId, graphId }); }); }; return ; } ``` ### Keyboard Shortcut for Delete ```tsx import { useDeleteNode, useCanvasSelection, currentGraphIdAtom } from '@blinksgg/canvas'; import { useAtomValue } from 'jotai'; import { useEffect } from 'react'; function DeleteKeyHandler() { const deleteNode = useDeleteNode(); const { selectedNodeIds } = useCanvasSelection(); const graphId = useAtomValue(currentGraphIdAtom); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.key === 'Delete' || e.key === 'Backspace') && selectedNodeIds.length > 0) { e.preventDefault(); selectedNodeIds.forEach(nodeId => { deleteNode.mutate({ nodeId, graphId }); }); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [deleteNode, selectedNodeIds, graphId]); return null; } ``` --- ## NodeTypeCombobox (Add Node UI) A searchable combobox for selecting and adding nodes to the canvas. ### Basic Usage ```tsx import { NodeTypeCombobox } from '@blinksgg/canvas'; // Uses all registered node types automatically console.log('Created:', id, type)} /> ``` ### With Custom Node Types ```tsx import { NodeTypeCombobox, type NodeTypeOption } from '@blinksgg/canvas'; const nodeTypes: NodeTypeOption[] = [ { type: 'note', label: 'Note', icon: '📝', description: 'Rich text note' }, { type: 'image', label: 'Image', icon: '🖼️', description: 'Image container' }, { type: 'code', label: 'Code', icon: '💻', defaultWidth: 400, defaultHeight: 300 }, ]; selectNode(id)} /> ``` ### Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `nodeTypes` | `NodeTypeOption[]` | registered types | Custom list of node types | | `onNodeCreated` | `(id, type) => void` | - | Callback after creation | | `createPosition` | `'center' \| 'random' \| {x,y}` | `'center'` | Where to place new node | | `placeholder` | `string` | `'Add node...'` | Input placeholder | | `clearOnCreate` | `boolean` | `true` | Clear search after create | | `disabled` | `boolean` | `false` | Disable the combobox | ### NodeTypeOption Interface ```tsx interface NodeTypeOption { type: string; // Node type identifier label?: string; // Display label icon?: string; // Emoji or icon description?: string; // Shown in dropdown defaultWidth?: number; // Default: 200 defaultHeight?: number; // Default: 150 } ``` --- ## Settings Panel (Event-Action Mappings) Configure what actions trigger on canvas events (double-click node → fit to view, etc.). ### Basic Usage ```tsx import { SettingsPanel, useCanvasSettings } from '@blinksgg/canvas'; function MyApp() { const { isPanelOpen, togglePanel } = useCanvasSettings(); return ( <> {isPanelOpen && } ); } ``` ### Canvas Events | Event | Description | |-------|-------------| | `node:double-click` | Double-click on a node | | `node:triple-click` | Triple-click on a node | | `node:right-click` | Right-click on a node | | `node:long-press` | Long-press on a node (touch) | | `background:click` | Click on canvas background | | `background:double-click` | Double-click on background | | `background:right-click` | Right-click on background | | `background:long-press` | Long-press on background (touch) | ### Built-in Actions | Action ID | Description | |-----------|-------------| | `none` | Do nothing | | `select-node` | Select the node | | `add-to-selection` | Add node to selection | | `clear-selection` | Clear all selection | | `delete-selected` | Delete selected nodes | | `fit-to-view` | Fit node in viewport | | `fit-all-to-view` | Fit all nodes | | `center-on-node` | Center viewport on node | | `lock-node` | Lock the node | | `unlock-node` | Unlock the node | | `toggle-lock` | Toggle node lock | | `open-context-menu` | Open context menu | | `apply-force-layout` | Apply D3 force layout | | `undo` | Undo last action | | `redo` | Redo last action | | `create-node` | Create a new node | ### useCanvasSettings Hook ```tsx import { useCanvasSettings } from '@blinksgg/canvas'; function SettingsManager() { const { mappings, // Current event-action mappings activePresetId, // ID of active preset (null if modified) allPresets, // Built-in + custom presets hasUnsavedChanges, // True if mappings differ from preset isPanelOpen, // Panel visibility state setEventMapping, // Set action for an event applyPreset, // Apply a preset saveAsPreset, // Save current mappings as preset deletePreset, // Delete a custom preset resetSettings, // Reset to defaults togglePanel, // Toggle panel visibility } = useCanvasSettings(); } ``` ### useActionExecutor Hook Execute actions programmatically with access to canvas state: ```tsx import { useActionExecutor, CanvasEventType } from '@blinksgg/canvas'; function MyCanvas() { const { executeEventAction, helpers } = useActionExecutor({ onDeleteNode: async (nodeId) => await deleteNode({ nodeId, graphId }), onOpenContextMenu: (pos, nodeId) => openMenu(pos, nodeId), onCreateNode: async (pos) => await createNode(pos), onApplyForceLayout: async () => await applyLayout(), }); const handleDoubleClick = (e: React.MouseEvent, nodeId: string) => { // Execute the action mapped to double-click executeEventAction(CanvasEventType.NodeDoubleClick, { eventType: CanvasEventType.NodeDoubleClick, nodeId, worldPosition: { x: 100, y: 100 }, screenPosition: { x: e.clientX, y: e.clientY }, modifiers: { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey, meta: e.metaKey }, }); }; } ``` ### Register Custom Actions ```tsx import { registerAction, ActionCategory } from '@blinksgg/canvas'; registerAction({ id: 'my-custom-action', label: 'My Action', description: 'Does something custom', category: ActionCategory.Custom, icon: 'star', handler: async (context, helpers) => { // context has: eventType, nodeId, nodeData, worldPosition, screenPosition, modifiers // helpers has: selectNode, clearSelection, fitToBounds, lockNode, undo, redo, etc. if (context.nodeId) { helpers.selectNode(context.nodeId); } }, }); ``` ### Built-in Presets | Preset | Description | |--------|-------------| | `default` | Standard interactions (current behavior) | | `minimal` | Only selection and context menu | | `power-user` | Quick actions (double-click lock, triple-click delete) | ### Wiring Node/Viewport Callbacks ```tsx import { Node, Viewport, useActionExecutor, CanvasEventType } from '@blinksgg/canvas'; function MyCanvas() { const { executeEventAction, helpers } = useActionExecutor({ onDeleteNode: (id) => deleteMutation.mutate({ nodeId: id, graphId }), onOpenContextMenu: (pos, nodeId) => setMenu({ pos, nodeId }), }); return ( { executeEventAction(CanvasEventType.BackgroundDoubleClick, { eventType: CanvasEventType.BackgroundDoubleClick, worldPosition: worldPos, screenPosition: worldPos, // Approximate modifiers: { shift: false, ctrl: false, alt: false, meta: false }, }); }} > ( { executeEventAction(CanvasEventType.NodeDoubleClick, { eventType: CanvasEventType.NodeDoubleClick, nodeId: id, nodeData: data, worldPosition: { x: data.x, y: data.y }, screenPosition: { x: 0, y: 0 }, modifiers: { shift: false, ctrl: false, alt: false, meta: false }, }); }} onRightClick={(id, data, e) => { executeEventAction(CanvasEventType.NodeRightClick, { eventType: CanvasEventType.NodeRightClick, nodeId: id, worldPosition: { x: data.x, y: data.y }, screenPosition: { x: e.clientX, y: e.clientY }, modifiers: { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey, meta: e.metaKey }, }); }} /> )} /> ); } ``` --- ## Settings Panel Canvas provides a customizable settings system for mapping user interactions (events) to actions. ### useCanvasSettings Hook ```tsx import { useCanvasSettings } from '@blinksgg/canvas'; function SettingsUI() { const { // Read-only state mappings, // Current event-action mappings activePresetId, // ID of active preset (null if modified) activePreset, // Active preset object allPresets, // All available presets (built-in + custom) hasUnsavedChanges, // Whether mappings differ from preset isPanelOpen, // Whether settings panel is visible // Actions setEventMapping, // Set action for a specific event applyPreset, // Apply a preset by ID saveAsPreset, // Save current mappings as new preset updatePreset, // Update existing preset with current mappings deletePreset, // Delete a custom preset resetSettings, // Reset to default settings togglePanel, // Toggle settings panel visibility setPanelOpen, // Set panel open/closed directly // Utilities getActionForEvent, // Get action ID for an event } = useCanvasSettings(); } ``` ### Settings Panel Component ```tsx import { SettingsPanel, useCanvasSettings } from '@blinksgg/canvas'; function MyApp() { const { isPanelOpen, togglePanel } = useCanvasSettings(); return ( <> togglePanel()} /> ); } ``` ### Event Types Events that can be mapped to actions: | Event Type | Description | |------------|-------------| | `NodeDoubleClick` | Double-click on a node | | `NodeTripleClick` | Triple-click on a node | | `NodeRightClick` | Right-click on a node | | `NodeLongPress` | Long-press on a node (touch) | | `BackgroundClick` | Click on canvas background | | `BackgroundDoubleClick` | Double-click on background | | `BackgroundRightClick` | Right-click on background | | `BackgroundLongPress` | Long-press on background (touch) | ### Built-in Actions | Action ID | Description | |-----------|-------------| | `none` | No action (disabled) | | `select-node` | Select the clicked node | | `add-to-selection` | Add node to selection | | `clear-selection` | Clear all selection | | `delete-selected` | Delete selected nodes | | `fit-to-view` | Fit node to viewport | | `fit-all-to-view` | Fit entire graph to viewport | | `center-on-node` | Center viewport on node | | `reset-viewport` | Reset viewport to default | | `lock-node` | Lock node (full-screen view) | | `unlock-node` | Unlock node | | `toggle-lock` | Toggle node lock state | | `open-context-menu` | Open right-click menu | | `apply-force-layout` | Apply force-directed layout | | `undo` | Undo last action | | `redo` | Redo last undone action | | `create-node` | Create a new node | ### Built-in Presets | Preset | Description | |--------|-------------| | `default` | Standard canvas behavior | | `productivity` | Optimized for quick editing | | `presentation` | Clean presentation mode | ### Custom Presets ```tsx const { saveAsPreset, applyPreset, deletePreset } = useCanvasSettings(); // Save current mappings as a new preset const newPresetId = saveAsPreset('My Custom Preset', 'Optional description'); // Apply a preset applyPreset('productivity'); applyPreset(newPresetId); // Delete a custom preset (built-in presets cannot be deleted) deletePreset(newPresetId); ``` ### Settings Atoms (for Jotai) | Atom | Purpose | |------|---------| | `canvasSettingsAtom` | Full settings state (persisted) | | `eventMappingsAtom` | Current event-action mappings | | `activePresetIdAtom` | Currently active preset ID | | `allPresetsAtom` | All presets (built-in + custom) | | `isPanelOpenAtom` | Settings panel visibility | | `hasUnsavedChangesAtom` | Whether mappings differ from preset | ### Action Atoms | Atom | Purpose | |------|---------| | `setEventMappingAtom` | Set a single event-action mapping | | `applyPresetAtom` | Apply a preset by ID | | `saveAsPresetAtom` | Save current mappings as new preset | | `updatePresetAtom` | Update existing preset | | `deletePresetAtom` | Delete a custom preset | | `resetSettingsAtom` | Reset to default settings | | `togglePanelAtom` | Toggle settings panel | | `setPanelOpenAtom` | Set panel open state | --- ## Command Console An extensible command palette with fuzzy search, sequential input collection, and keyboard shortcuts. ### Import Path ```tsx import { ... } from '@blinksgg/canvas/commands'; ``` ### Quick Start ```tsx import { CommandProvider, CommandLine, CommandFeedbackOverlay, registerBuiltinCommands, registerCommand, useCommandLine, } from '@blinksgg/canvas/commands'; // 1. Register built-in commands once registerBuiltinCommands(); // 2. Register custom commands registerCommand({ name: 'createWidget', description: 'Create a new widget', category: 'custom', inputs: [{ type: 'text', name: 'name', prompt: 'Widget name' }], execute: async (inputs, ctx) => { await ctx.mutations.createNode({ type: 'widget', label: inputs.name }); }, }); // 3. Wrap your app with CommandProvider function App() { return ( ); } ``` ### Built-in Commands | Command | Aliases | Description | |---------|---------|-------------| | `fitToView` | `fit`, `fitAll` | Fit all nodes in viewport | | `fitSelection` | `fitSel` | Fit selected nodes in viewport | | `resetViewport` | `reset`, `home` | Reset zoom and pan | | `zoomIn` | `+`, `in` | Zoom in | | `zoomOut` | `-`, `out` | Zoom out | | `selectAll` | `all` | Select all nodes | | `clearSelection` | `deselect`, `clear` | Clear selection | | `invertSelection` | `invert` | Invert selection | | `undo` | `z` | Undo last action | | `redo` | `y` | Redo last action | | `forceLayout` | `force`, `autoLayout` | Apply force-directed layout | ### Keyboard Shortcuts | Shortcut | Action | |----------|--------| | `/` | Toggle command line | | `Cmd+K` / `Ctrl+K` | Open command line | | `ESC` | Cancel current command / return to search | | `↑` / `↓` | Navigate suggestions | | `Enter` | Select command / provide input | | `Tab` | Autocomplete command name | ### useCommandLine Hook ```tsx import { useCommandLine } from '@blinksgg/canvas/hooks'; function CommandToggle() { const { visible, // Command line visibility state, // Current state (searching, collecting, executing, error) history, // Command history progress, // Input collection progress { current, total } open, // Open command line close, // Close command line isSearching, // Helper: state.phase === 'searching' isCollecting, // Helper: state.phase === 'collecting' isExecuting, // Helper: state.phase === 'executing' hasError, // Helper: state.phase === 'error' } = useCommandLine(); return ; } ``` ### Register Custom Commands ```tsx import { registerCommand, type CommandDefinition } from '@blinksgg/canvas/commands'; const myCommand: CommandDefinition = { name: 'myCommand', description: 'Does something cool', aliases: ['mc'], // Optional shortcuts category: 'custom', // 'nodes' | 'edges' | 'viewport' | 'selection' | 'history' | 'layout' | 'custom' inputs: [ { type: 'text', name: 'title', prompt: 'Enter title' }, { type: 'number', name: 'count', prompt: 'How many?', default: 1 }, { type: 'select', name: 'color', prompt: 'Choose color', options: [ { value: 'red', label: 'Red', shortcut: 'r' }, { value: 'blue', label: 'Blue', shortcut: 'b' }, ], }, { type: 'point', name: 'position', prompt: 'Click location' }, { type: 'node', name: 'target', prompt: 'Select target node' }, { type: 'boolean', name: 'confirm', prompt: 'Are you sure?' }, ], execute: async (inputs, ctx) => { // inputs: { title: string, count: number, color: string, position: {x,y}, target: string, confirm: boolean } // ctx: CommandContext with mutations, layout, history, viewport, selectedNodeIds await ctx.mutations.createNode({ graph_id: ctx.currentGraphId, node_type: 'custom', label: inputs.title, ui_properties: { x: inputs.position.x, y: inputs.position.y }, }); }, }; registerCommand(myCommand); ``` ### Input Types | Type | Description | Value | |------|-------------|-------| | `text` | Text input | `string` | | `number` | Numeric input | `number` | | `boolean` | Yes/No choice | `boolean` | | `select` | Dropdown options | `string` | | `point` | Click on canvas | `{ x: number, y: number }` | | `node` | Click on a node | `string` (nodeId) | | `nodes` | Select multiple nodes | `string[]` (nodeIds) | ### CommandProvider Props ```tsx interface CommandProviderProps { children: React.ReactNode; // Node mutations (app provides implementations) onCreateNode?: (payload: CreateNodePayload) => Promise; onUpdateNode?: (nodeId: string, updates: Record) => Promise; onDeleteNode?: (nodeId: string) => Promise; // Edge mutations onCreateEdge?: (payload: CreateEdgePayload) => Promise; onDeleteEdge?: (edgeId: string) => Promise; // Force layout persistence onForceLayoutPersist?: (positions: Array<{ nodeId: string; x: number; y: number }>) => Promise; } ``` ### CommandContext (in execute function) ```tsx interface CommandContext { get: JotaiStore['get']; // Read atoms directly set: JotaiStore['set']; // Write atoms directly currentGraphId: string | null; // Active graph ID selectedNodeIds: Set; // Currently selected nodes viewport: { zoom: number; pan: { x: number; y: number } }; mutations: { createNode: (payload: CreateNodePayload) => Promise; updateNode: (nodeId: string, updates: Record) => Promise; deleteNode: (nodeId: string) => Promise; createEdge: (payload: CreateEdgePayload) => Promise; deleteEdge: (edgeId: string) => Promise; }; layout: { fitToBounds: (mode: 'graph' | 'selection', padding?: number) => void; applyForceLayout: () => Promise; }; history: { undo: () => void; redo: () => void; }; } ``` ### Command Line UI Components | Component | Purpose | |-----------|---------| | `CommandLine` | Main command bar (appears at bottom) | | `CommandSearch` | Fuzzy search autocomplete | | `CommandInputCollector` | Sequential input prompts | | `CommandFeedbackOverlay` | Visual feedback (crosshairs, previews) | ### Store Atoms | Atom | Purpose | |------|---------| | `commandLineVisibleAtom` | Command line visibility | | `commandLineStateAtom` | Full state (phase, suggestions, etc.) | | `commandFeedbackAtom` | Visual feedback state | | `commandHistoryAtom` | Command execution history | | `openCommandLineAtom` | Action: open command line | | `closeCommandLineAtom` | Action: close command line | | `selectCommandAtom` | Action: select a command | | `isCommandActiveAtom` | Boolean: is command line active? | | `currentInputAtom` | Current input being collected | | `commandProgressAtom` | Progress: { current, total } |