# @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 } |