canvas/dist/hooks/index.d.mts
2026-03-11 18:42:08 -07:00

1766 lines
50 KiB
TypeScript

import * as _use_gesture_react_dist_declarations_src_types from '@use-gesture/react/dist/declarations/src/types';
import { Getter, Setter } from 'jotai';
import React$1 from 'react';
/**
* Core types for @blinksgg/canvas
*/
/**
* JSON type compatible with Supabase's generated types.
* This allows the package to work with database JSONB fields.
*/
type Json = string | number | boolean | null | {
[key: string]: Json | undefined;
} | Json[];
type NodeId = string;
type EdgeKey = string;
interface NodePosition {
x: number;
y: number;
}
/**
* Database node record - the shape of node data from your database.
* Uses Json type for JSONB fields to be compatible with Supabase's generated types.
*/
interface DBGraphNode {
id: string;
graph_id: string;
label: string | null;
node_type: string | null;
configuration: Json | null;
ui_properties: Json | null;
/** App-specific customization data */
data?: Json | null;
created_at: string;
updated_at: string;
}
/**
* UI node properties for persist callbacks.
* Typed version of the JSONB payload sent to database updates.
* Known fields have explicit types; additional app-specific fields are allowed via index signature.
*/
interface UINodeProperties {
x?: number;
y?: number;
width?: number;
height?: number;
size?: number;
color?: string;
zIndex?: number;
[key: string]: Json | undefined;
}
/**
* Graph node attributes stored in Graphology
*/
interface GraphNodeAttributes {
x: number;
y: number;
size: number;
width: number;
height: number;
color: string;
label?: string;
zIndex: number;
dbData: DBGraphNode;
/** Parent group node ID (for nested grouping) */
parentId?: string;
}
/**
* Complete UI node state for rendering
*/
interface UINodeState extends GraphNodeAttributes {
id: NodeId;
position: NodePosition;
isDragging: boolean;
}
/**
* Database edge record.
* Uses Json type for JSONB fields to be compatible with Supabase's generated types.
*/
interface DBGraphEdge {
id: string;
graph_id: string;
source_node_id: string;
target_node_id: string;
edge_type: string | null;
filter_condition: Json | null;
ui_properties: Json | null;
/** App-specific customization data */
data?: Json | null;
created_at: string;
updated_at: string;
}
/**
* Graph edge attributes stored in Graphology
*/
interface GraphEdgeAttributes {
weight?: number;
type?: 'solid' | 'dashed';
color?: string;
label?: string;
dbData: DBGraphEdge;
}
/**
* Complete UI edge state for rendering
*/
interface UIEdgeState extends GraphEdgeAttributes {
key: EdgeKey;
sourceId: string;
targetId: string;
sourcePosition: NodePosition;
targetPosition: NodePosition;
sourceNodeSize: number;
targetNodeSize: number;
sourceNodeWidth: number;
sourceNodeHeight: number;
targetNodeWidth: number;
targetNodeHeight: number;
/** Source port ID (if using port-based connections) */
sourcePort?: string;
/** Target port ID (if using port-based connections) */
targetPort?: string;
/** Calculated source endpoint position (when port is specified) */
sourceEndpoint?: NodePosition;
/** Calculated target endpoint position (when port is specified) */
targetEndpoint?: NodePosition;
}
interface PanOffset {
x: number;
y: number;
}
/**
* Hook for managing node selection state
*/
/**
* Hook to manage selection for a specific node
*
* @param nodeId - The ID of the node
* @returns Selection state and actions for the node
*/
declare function useNodeSelection(nodeId: NodeId): {
/** Whether this node is currently selected */
isSelected: boolean;
/** Select only this node (clears other selections) */
selectNode: () => void;
/** Toggle this node's selection state */
toggleNode: () => void;
};
/**
* Input Classifier
*
* Classifies pointer events into input sources (finger, pencil, mouse)
* and extracts stylus-specific data (pressure, tilt).
*
* This is the foundation of the touch-first input architecture:
* - Pencil draws/selects
* - Fingers navigate (pan/zoom)
* - Mouse does both
*/
/** Input device type */
type InputSource = 'finger' | 'pencil' | 'mouse';
/**
* A selectable option for 'select' type inputs.
*/
type SelectOption$1 = {
/** Display label for the option */
label: string;
/** Value to use when selected */
value: string;
/** Single key shortcut (e.g., 'w' for widget) */
shortcut: string;
/** Optional description shown on hover */
description?: string;
};
/**
* Input mode - controls how canvas interactions are routed.
* When not 'normal', canvas clicks are intercepted for special handling.
*/
type InputMode = {
type: 'normal';
} | {
type: 'pickPoint';
prompt: string;
snapToGrid?: boolean;
} | {
type: 'pickNode';
prompt: string;
filter?: (node: UINodeState) => boolean;
} | {
type: 'pickNodes';
prompt: string;
filter?: (node: UINodeState) => boolean;
} | {
type: 'text';
prompt: string;
} | {
type: 'select';
prompt: string;
options: SelectOption$1[];
};
type KeyboardInteractionMode = 'navigate' | 'manipulate';
/**
* Gesture System v2 — Core Types
*
* Single source of truth for every type in the gesture pipeline.
* No runtime code — pure type definitions.
*/
type Button = 0 | 1 | 2;
interface Modifiers {
shift: boolean;
ctrl: boolean;
alt: boolean;
meta: boolean;
/** Non-keyboard modifier flags (touch toolbars, stylus buttons, etc.). */
custom?: Record<string, boolean>;
}
interface HeldKeysState {
byKey: Readonly<Record<string, boolean>>;
byCode: Readonly<Record<string, boolean>>;
}
type PointerGestureType = 'tap' | 'double-tap' | 'triple-tap' | 'long-press' | 'drag' | 'pinch' | 'scroll';
type PointerGesturePhase = 'start' | 'move' | 'end' | 'instant' | 'cancel';
type KeyInputPhase = 'down' | 'up';
type InputPhase = PointerGesturePhase | KeyInputPhase;
type GestureSubject = {
kind: 'background';
} | {
kind: 'node';
nodeId: string;
} | {
kind: 'edge';
edgeId: string;
} | {
kind: 'port';
nodeId: string;
portId: string;
};
type SubjectKind = GestureSubject['kind'];
interface Vec2 {
x: number;
y: number;
}
interface PointerGestureEvent {
kind?: 'pointer';
type: PointerGestureType;
phase: PointerGesturePhase;
subject: GestureSubject;
source: InputSource;
button: Button;
modifiers: Modifiers;
heldKeys: HeldKeysState;
pointerId?: number;
screenPosition: Vec2;
worldPosition: Vec2;
delta?: Vec2;
velocity?: Vec2;
scale?: number;
originalEvent?: Event;
}
interface KeyInputEvent {
kind: 'key';
phase: KeyInputPhase;
key: string;
code: string;
repeat: boolean;
modifiers: Modifiers;
heldKeys: HeldKeysState;
subject?: GestureSubject;
screenPosition?: Vec2;
worldPosition?: Vec2;
originalEvent?: Event;
}
type InputEvent = PointerGestureEvent | KeyInputEvent;
interface HeldKeysPattern {
byKey?: Record<string, boolean>;
byCode?: Record<string, boolean>;
}
interface InputPattern {
kind?: 'pointer' | 'key';
type?: PointerGestureType;
subjectKind?: SubjectKind;
source?: InputSource;
button?: Button;
phase?: InputPhase;
modifiers?: Partial<Modifiers>;
key?: string;
code?: string;
heldKeys?: HeldKeysPattern;
}
interface Binding {
id: string;
pattern: InputPattern;
actionId: string;
consumeInput?: boolean;
when?: (ctx: GuardContext) => boolean;
}
interface MappingContext {
id: string;
priority: number;
enabled: boolean;
bindings: Binding[];
}
interface GuardContext {
isStylusActive: boolean;
fingerCount: number;
isDragging: boolean;
isResizing: boolean;
isSplitting: boolean;
inputMode: InputMode;
keyboardInteractionMode: KeyboardInteractionMode;
selectedNodeIds: ReadonlySet<string>;
focusedNodeId: string | null;
isSearchActive: boolean;
commandLineVisible: boolean;
heldKeys: HeldKeysState;
custom: Record<string, unknown>;
}
type BivariantCallback<Args extends unknown[], Result = void> = {
bivarianceHack: (...args: Args) => Result;
}['bivarianceHack'];
interface PhaseHandler {
onStart?: BivariantCallback<[PointerGestureEvent]>;
onMove?: BivariantCallback<[PointerGestureEvent]>;
onEnd?: BivariantCallback<[PointerGestureEvent]>;
onInstant?: BivariantCallback<[PointerGestureEvent]>;
onCancel?: BivariantCallback<[PointerGestureEvent]>;
onDown?: BivariantCallback<[KeyInputEvent]>;
onUp?: BivariantCallback<[KeyInputEvent]>;
}
/**
* Action handler. Simple function form fires only on 'start' and 'instant'.
* Object form routes each phase to a dedicated method.
*/
type ActionHandler = BivariantCallback<[InputEvent]> | PhaseHandler;
/**
* Drag Types
*
* Type definitions extracted from useNodeDrag.ts for reuse.
*/
/**
* Options for the useNodeDrag hook.
*/
interface UseNodeDragOptions {
/** Callback to persist node position to database */
onPersist?: (nodeId: NodeId, graphId: string, uiProperties: UINodeProperties) => Promise<void>;
/** Callback when persist fails */
onPersistError?: (nodeId: NodeId, error: Error) => void;
/** Debounce time for query invalidation (ms) */
invalidationDebounceMs?: number;
/** Snapshot of currently held keyboard keys */
heldKeys?: HeldKeysState;
}
/**
* Hook for node drag behavior with multi-select support.
* Always treats drag as move-node — no gesture resolution.
*/
declare function useNodeDrag(id: NodeId, options?: UseNodeDragOptions): {
bind: (...args: any[]) => _use_gesture_react_dist_declarations_src_types.ReactDOMAttributes;
updateNodePositions: (updates: {
id: NodeId;
pos: NodePosition;
}[]) => void;
};
/**
* Hook for managing node resize behavior
*/
type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
interface UseNodeResizeOptions {
/** Callback to persist node size/position to database */
onPersist?: (nodeId: NodeId, graphId: string, uiProperties: UINodeProperties) => Promise<void>;
/** Callback when persist fails */
onPersistError?: (nodeId: NodeId, error: Error) => void;
/** Minimum node width */
minWidth?: number;
/** Minimum node height */
minHeight?: number;
}
interface UseNodeResizeProps {
id: NodeId;
nodeData: {
width?: number;
height?: number;
};
updateNodePositions: (updates: {
id: NodeId;
pos: NodePosition;
}[]) => void;
options?: UseNodeResizeOptions;
}
/**
* Hook for node resize behavior
*/
declare function useNodeResize({ id, nodeData, updateNodePositions, options }: UseNodeResizeProps): {
localWidth: number;
localHeight: number;
isResizing: boolean;
createResizeStart: (direction: ResizeDirection) => (e: React.PointerEvent) => void;
handleResizeMove: (e: React.PointerEvent) => void;
handleResizeEnd: (e: React.PointerEvent) => void;
};
/**
* useCanvasHistory Hook
*
* Provides undo/redo functionality for canvas operations.
*/
interface UseCanvasHistoryOptions {
/** Deprecated: keyboard shortcuts are owned by InputProvider. */
enableKeyboardShortcuts?: boolean;
}
/**
* Hook for undo/redo functionality.
*
* @example
* ```tsx
* function CanvasToolbar() {
* const { undo, redo, canUndo, canRedo, recordSnapshot } = useCanvasHistory();
*
* // Before making changes, record a snapshot:
* const handleAddNode = () => {
* recordSnapshot('Add node');
* addNode({ ... });
* };
*
* return (
* <>
* <button onClick={undo} disabled={!canUndo}>Undo</button>
* <button onClick={redo} disabled={!canRedo}>Redo</button>
* </>
* );
* }
* ```
*/
declare function useCanvasHistory(options?: UseCanvasHistoryOptions): {
/** Undo the last operation */
undo: () => boolean;
/** Redo the last undone operation */
redo: () => boolean;
/** Whether undo is available */
canUndo: boolean;
/** Whether redo is available */
canRedo: boolean;
/** Number of undo steps available */
undoCount: number;
/** Number of redo steps available */
redoCount: number;
/** Labels for history entries (for display) */
historyLabels: {
past: string[];
future: string[];
};
/** Record a snapshot before making changes */
recordSnapshot: (label?: string) => void;
/** Clear all history */
clear: () => void;
};
/**
* Canvas Selection Subscription Hook
*
* Provides reactive access to canvas selection state.
* Use this to respond to selection changes in your components.
*
* @since 0.2.0
* @example
* ```tsx
* function SelectionInfo() {
* const { selectedNodeIds, selectedEdgeId, hasSelection } = useCanvasSelection();
* return <div>Selected: {selectedNodeIds.size} nodes</div>;
* }
* ```
*/
interface CanvasSelectionState {
/** Set of currently selected node IDs (reactive) */
selectedNodeIds: Set<NodeId>;
/** Currently selected edge ID (null if no edge selected) */
selectedEdgeId: string | null;
/** Number of selected nodes (derived atom for performance) */
count: number;
/** Whether any nodes are selected */
hasSelection: boolean;
/** Whether an edge is selected */
hasEdgeSelection: boolean;
}
/**
* Subscribe to canvas selection state.
*
* This hook provides a convenient way to access selection state
* without needing to know which atoms to import. All returned values
* are reactive and will trigger re-renders when selection changes.
*/
declare function useCanvasSelection(): CanvasSelectionState;
/**
* Canvas Viewport Subscription Hook
*
* Provides reactive access to canvas viewport state (pan, zoom).
* Use this to respond to viewport changes in your components.
*
* @example
* ```tsx
* function ZoomIndicator() {
* const { zoom, pan } = useCanvasViewport();
* return <div>Zoom: {Math.round(zoom * 100)}%</div>;
* }
* ```
*/
interface CanvasViewportState {
/** Current zoom level (1 = 100%) */
zoom: number;
/** Current pan offset in pixels */
pan: PanOffset;
/** Viewport DOM rect (null if not mounted) */
viewportRect: DOMRect | null;
/** Convert screen coordinates to world coordinates */
screenToWorld: (screenX: number, screenY: number) => {
x: number;
y: number;
};
/** Convert world coordinates to screen coordinates */
worldToScreen: (worldX: number, worldY: number) => {
x: number;
y: number;
};
/** Node ID being zoomed into (for transitions) */
zoomFocusNodeId: string | null;
/** Zoom transition progress (0-1) */
zoomTransitionProgress: number;
/** Whether a zoom transition is in progress */
isZoomTransitioning: boolean;
/** Zoom level that triggers transition */
zoomTransitionThreshold: number;
/** Zoom level that exits transition */
zoomExitThreshold: number;
}
/**
* Subscribe to canvas viewport state.
*
* This hook provides a convenient way to access viewport state
* without needing to know which atoms to import.
*/
declare function useCanvasViewport(): CanvasViewportState;
/**
* Canvas Drag Subscription Hook
*
* Provides reactive access to canvas drag state.
* Use this to respond to drag operations in your components.
*
* @example
* ```tsx
* function DragIndicator() {
* const { isDragging, draggingNodeId } = useCanvasDrag();
* if (!isDragging) return null;
* return <div>Dragging: {draggingNodeId}</div>;
* }
* ```
*/
interface CanvasDragState {
/** ID of the node currently being dragged (null if not dragging) */
draggingNodeId: string | null;
/** Whether any node is currently being dragged */
isDragging: boolean;
}
/**
* Subscribe to canvas drag state.
*
* This hook provides a convenient way to access drag state
* without needing to know which atoms to import.
*/
declare function useCanvasDrag(): CanvasDragState;
/**
* Layout utilities for canvas operations.
*
* Provides geometry calculations, bounds computation, and node overlap detection.
*/
/**
* Rectangle with position and dimensions
*/
interface Rect {
x: number;
y: number;
width: number;
height: number;
}
/**
* Mode for fitToBounds operation
*/
declare enum FitToBoundsMode {
/** Fit all nodes in the graph */
Graph = "graph",
/** Fit only selected nodes */
Selection = "selection"
}
/**
* Layout hooks for viewport manipulation.
*
* Provides fit-to-bounds, graph bounds calculation, and layout utilities.
* The core fitToBounds logic lives in viewport-store.ts (headless atom).
*/
declare const useGetGraphBounds: () => {
bounds: Rect;
nodes: Rect[];
};
declare const useSelectionBounds: () => Rect;
/**
* Hook to fit the viewport to show specific bounds.
* Delegates to fitToBoundsAtom (headless, no React required).
*/
declare const useFitToBounds: () => {
fitToBounds: (mode: FitToBoundsMode | "graph" | "selection", padding?: number) => void;
};
/**
* Combined layout hook providing all layout utilities.
*
* @returns Object with fitToBounds and graph bounds utilities
*
* @example
* const { fitToBounds, graphBounds, selectionBounds } = useLayout();
*
* // After modifying nodes:
* fitToBounds(FitToBoundsMode.Graph, 50);
*/
declare const useLayout: () => {
fitToBounds: (mode: FitToBoundsMode | "graph" | "selection", padding?: number) => void;
graphBounds: Rect;
graphNodes: Rect[];
selectionBounds: Rect;
};
/**
* Force-directed layout hook.
*
* Uses D3 force simulation to arrange nodes without overlap.
* Storage-agnostic: accepts optional persistence callback.
*/
/**
* Position update for a single node (used in persistence callback)
*/
interface NodePositionUpdate {
nodeId: string;
position: NodePosition;
}
/**
* Options for the force layout
*/
interface UseForceLayoutOptions {
/**
* Optional callback to persist positions after layout.
* Called with all updated positions when layout completes.
*/
onPositionsChanged?: (updates: NodePositionUpdate[]) => void | Promise<void>;
/**
* Maximum iterations before stopping (default: 1000)
*/
maxIterations?: number;
/**
* Charge strength for node repulsion (default: -6000)
* More negative = stronger repulsion
*/
chargeStrength?: number;
/**
* Link strength for mesh connections (default: 0.03)
*/
linkStrength?: number;
}
/**
* Hook to apply force-directed layout to canvas nodes.
*
* @param options - Configuration options including persistence callback
* @returns Object with applyForceLayout function and isRunning state
*
* @example
* // Basic usage (no persistence)
* const { applyForceLayout } = useForceLayout();
* await applyForceLayout();
*
* @example
* // With database persistence
* const { applyForceLayout } = useForceLayout({
* onPositionsChanged: async (updates) => {
* for (const { nodeId, position } of updates) {
* await saveNodePosition(nodeId, position);
* }
* }
* });
*/
declare const useForceLayout: (options?: UseForceLayoutOptions) => {
applyForceLayout: () => Promise<void>;
isRunning: boolean;
};
/**
* Canvas Event Types
*
* Type definitions for configurable canvas events.
*
* Extracted from settings-types.ts in v1.9.0.
*
* @since 1.9.0
*/
/**
* All configurable canvas events that can trigger actions.
*/
declare enum CanvasEventType {
NodeClick = "node:click",
NodeDoubleClick = "node:double-click",
NodeTripleClick = "node:triple-click",
NodeRightClick = "node:right-click",
NodeLongPress = "node:long-press",
EdgeClick = "edge:click",
EdgeDoubleClick = "edge:double-click",
EdgeRightClick = "edge:right-click",
BackgroundClick = "background:click",
BackgroundDoubleClick = "background:double-click",
BackgroundRightClick = "background:right-click",
BackgroundLongPress = "background:long-press"
}
/**
* Gesture Resolver — Types
*
* Gesture intent vocabulary and context types for the canvas.
*
* Core principle: Pencil draws/selects, fingers navigate, mouse does both.
*
* @see ./gesture-rules.ts for the composable rule-based resolution system.
*/
/** Context about where a gesture occurred */
type GestureTarget = 'node' | 'edge' | 'port' | 'resize-handle' | 'background';
/** Gesture type description */
type GestureType = 'tap' | 'double-tap' | 'triple-tap' | 'drag' | 'long-press' | 'right-click' | 'pinch' | 'scroll';
/**
* Gesture Rules — Type Definitions
*
* All type/interface definitions for the composable gesture rule system.
* Split from gesture-rules.ts for modularity.
*/
/** Mouse button */
type GestureButton = 0 | 1 | 2;
/** Modifier key combination */
interface ModifierFlags {
shift?: boolean;
ctrl?: boolean;
alt?: boolean;
meta?: boolean;
}
/** Drag phase for compound gesture events */
type GesturePhase = 'start' | 'move' | 'end' | 'instant';
/**
* Full description of a gesture event — the "query" passed to the resolver.
* Every field is populated from the actual pointer/gesture event.
*/
interface GestureDescriptor {
/** What type of gesture is being performed */
gesture: GestureType;
/** What the gesture is targeting */
target: GestureTarget;
/** What input device is being used */
source: InputSource;
/** Which mouse button initiated the gesture (default: 0 = left) */
button?: GestureButton;
/** Modifier keys held during the gesture */
modifiers?: ModifierFlags;
/** Phase for drag gestures */
phase?: GesturePhase;
/** Whether a stylus is currently active (for palm rejection) */
isStylusActive?: boolean;
}
/**
* Pattern that matches gesture descriptors.
* Omitted fields are wildcards — they match any value.
* This is the "left-hand side" of a rule.
*/
interface GesturePattern {
gesture?: GestureType;
target?: GestureTarget;
source?: InputSource;
button?: GestureButton;
modifiers?: ModifierFlags;
}
/**
* A single gesture → action mapping rule.
*/
interface GestureRule {
/** Unique identifier for this rule (for UI editing/removal) */
id: string;
/** Pattern to match against */
pattern: GesturePattern;
/** Action ID to execute (from action-registry) */
actionId: string;
/** Higher priority wins when two rules match with equal specificity */
priority?: number;
/** Human-readable label (auto-generated from pattern if omitted) */
label?: string;
}
/**
* Result of gesture resolution.
*/
interface ResolvedGesture {
actionId: string;
rule: GestureRule;
/** Effective score: specificity * 1000 + priority. Higher = better match. */
score: number;
}
/**
* Canvas Action Types
*
* Action category enum, built-in action IDs, and action definition interfaces.
*
* Extracted from settings-types.ts in v1.9.0.
*
* @since 1.9.0
*/
/**
* Categories for grouping actions in the UI.
*/
declare enum ActionCategory {
/** No action / disabled */
None = "none",
/** Selection-related actions */
Selection = "selection",
/** Viewport/navigation actions */
Viewport = "viewport",
/** Node manipulation actions */
Node = "node",
/** Layout and arrangement actions */
Layout = "layout",
/** History actions (undo/redo) */
History = "history",
/** User-defined custom actions */
Custom = "custom"
}
/**
* Context passed to action handlers when executing.
*/
interface ActionContext {
eventType: CanvasEventType;
gesture?: GestureDescriptor;
nodeId?: string;
nodeData?: UINodeState;
edgeId?: string;
edgeData?: UIEdgeState;
worldPosition: {
x: number;
y: number;
};
screenPosition: {
x: number;
y: number;
};
selectedNodeIds?: string[];
modifiers: {
shift: boolean;
ctrl: boolean;
alt: boolean;
meta: boolean;
};
}
/**
* Helper functions provided to action handlers.
*/
interface ActionHelpers {
selectNode: (nodeId: string) => void;
addToSelection: (nodeId: string) => void;
clearSelection: () => void;
getSelectedNodeIds: () => string[];
fitToBounds: (mode: 'graph' | 'selection', padding?: number) => void;
centerOnNode: (nodeId: string) => void;
resetViewport: () => void;
lockNode: (nodeId: string) => void;
unlockNode: (nodeId: string) => void;
toggleLock: (nodeId: string) => void;
deleteNode: (nodeId: string) => Promise<void>;
isNodeLocked: (nodeId: string) => boolean;
applyForceLayout: () => Promise<void>;
undo: () => void;
redo: () => void;
canUndo: () => boolean;
canRedo: () => boolean;
selectEdge: (edgeId: string) => void;
clearEdgeSelection: () => void;
openContextMenu?: (position: {
x: number;
y: number;
}, nodeId?: string) => void;
createNode?: (position: {
x: number;
y: number;
}) => Promise<void>;
splitNode?: (nodeId: string) => Promise<void>;
groupNodes?: (nodeIds: string[]) => Promise<void>;
mergeNodes?: (nodeIds: string[]) => Promise<void>;
}
/**
* Full definition of an action that can be triggered by events.
*/
interface ActionDefinition {
id: string;
label: string;
description: string;
category: ActionCategory;
icon?: string;
requiresNode?: boolean;
isBuiltIn?: boolean;
handler: (context: ActionContext, helpers: ActionHelpers) => void | Promise<void>;
}
/**
* Canvas Settings State Types
*
* Event-action mappings, presets, persisted state, and default mappings.
*
* Extracted from settings-types.ts in v1.9.0.
*
* @since 1.9.0
*/
/**
* Mapping of events to action IDs.
*/
type EventActionMapping = {
[K in CanvasEventType]: string;
};
/**
* A settings preset with a name and full event-action mappings.
*/
interface SettingsPreset {
id: string;
name: string;
description?: string;
mappings: EventActionMapping;
isBuiltIn: boolean;
}
/**
* useCanvasSettings Hook
*
* Main hook for managing canvas settings (event-action mappings and presets).
* Provides both read access to settings and methods to modify them.
*/
interface UseCanvasSettingsReturn {
/** Current event-action mappings */
mappings: EventActionMapping;
/** ID of the currently active preset (null if modified) */
activePresetId: string | null;
/** The currently active preset object (null if modified) */
activePreset: SettingsPreset | null;
/** All available presets (built-in + custom) */
allPresets: SettingsPreset[];
/** Whether current mappings differ from the active preset */
hasUnsavedChanges: boolean;
/** Whether the settings panel is open */
isPanelOpen: boolean;
/** Set the action for a specific event */
setEventMapping: (event: CanvasEventType, actionId: string) => void;
/** Apply a preset (copies its mappings) */
applyPreset: (presetId: string) => void;
/** Save current mappings as a new custom preset */
saveAsPreset: (name: string, description?: string) => string;
/** Update an existing custom preset with current mappings */
updatePreset: (presetId: string) => void;
/** Delete a custom preset */
deletePreset: (presetId: string) => void;
/** Reset to default settings */
resetSettings: () => void;
/** Toggle the settings panel */
togglePanel: () => void;
/** Set panel open state directly */
setPanelOpen: (isOpen: boolean) => void;
/** Get the action ID for a specific event */
getActionForEvent: (event: CanvasEventType) => string;
}
/**
* Hook for managing canvas event-action settings.
*
* @example
* ```tsx
* function CanvasSettingsButton() {
* const {
* isPanelOpen,
* togglePanel,
* activePreset,
* hasUnsavedChanges,
* } = useCanvasSettings();
*
* return (
* <button onClick={togglePanel}>
* Settings
* {hasUnsavedChanges && '*'}
* </button>
* );
* }
* ```
*
* @example
* ```tsx
* function PresetSelector() {
* const { allPresets, activePresetId, applyPreset } = useCanvasSettings();
*
* return (
* <select
* value={activePresetId || ''}
* onChange={(e) => applyPreset(e.target.value)}
* >
* {allPresets.map((preset) => (
* <option key={preset.id} value={preset.id}>
* {preset.name}
* </option>
* ))}
* </select>
* );
* }
* ```
*/
declare function useCanvasSettings(): UseCanvasSettingsReturn;
/**
* Action Executor
*
* Executes actions by ID with the provided context and helpers.
* This is the core execution logic, separate from React hooks.
*/
interface ActionExecutionResult {
success: boolean;
actionId: string;
error?: Error;
}
/**
* useActionExecutor Hook
*
* React wrapper around buildActionHelpers for convenient hook-based usage.
* The core logic lives in action-executor.ts (headless, no React).
*/
interface UseActionExecutorOptions {
/**
* Callback when a node needs to be deleted.
* Apps must provide this since deletion logic is app-specific (database, etc).
*/
onDeleteNode?: (nodeId: string) => Promise<void>;
/**
* Callback to open a context menu.
* Apps provide their own context menu implementation.
*/
onOpenContextMenu?: (position: {
x: number;
y: number;
}, nodeId?: string) => void;
/**
* Callback to create a node at a position.
* Apps provide their own node creation logic.
*/
onCreateNode?: (position: {
x: number;
y: number;
}) => Promise<void>;
/**
* Callback for force layout.
* Apps may want to wrap this with persistence logic.
*/
onApplyForceLayout?: () => Promise<void>;
}
interface UseActionExecutorReturn {
executeActionById: (actionId: string, context: ActionContext) => Promise<ActionExecutionResult>;
executeEventAction: (event: CanvasEventType, context: ActionContext) => Promise<ActionExecutionResult>;
getActionForEvent: (event: CanvasEventType) => string;
mappings: EventActionMapping;
helpers: ActionHelpers;
}
declare function useActionExecutor(options?: UseActionExecutorOptions): UseActionExecutorReturn;
/**
* useGestureResolver Hook
*
* Convenience hook that provides a bound `resolve(descriptor)` function
* using the gesture rules and palm rejection settings from the store.
*/
/**
* Returns a `resolve` function that resolves gesture descriptors against
* the current rule set, with palm rejection and stylus awareness built in.
*
* Usage:
* ```tsx
* const resolve = useGestureResolver();
* const result = resolve({ gesture: 'drag', target: 'node', source: 'mouse' });
* if (result?.actionId === 'move-node') { ... }
* ```
*/
declare function useGestureResolver(): (desc: Omit<GestureDescriptor, "isStylusActive"> & {
isStylusActive?: boolean;
}) => ResolvedGesture | null;
/**
* Command System Types
*
* Type definitions for the command palette system.
* The command system provides a way to execute commands with
* sequential input collection and visual feedback.
*/
/**
* Input types supported by the command system.
* Each type determines how the input is collected from the user.
*/
type InputType = 'text' | 'number' | 'point' | 'node' | 'nodes' | 'edge' | 'select' | 'boolean' | 'color';
/**
* A selectable option for 'select' type inputs.
*/
type SelectOption = {
/** Display label for the option */
label: string;
/** Value to use when selected */
value: string;
/** Single key shortcut (e.g., 'w' for widget) */
shortcut: string;
/** Optional description shown on hover */
description?: string;
};
/**
* Definition for a single input prompt in a command.
*/
type InputDefinition = {
/** Parameter name (used as key in collected inputs) */
name: string;
/** Type of input to collect */
type: InputType;
/** Displayed prompt text */
prompt: string;
/** Whether this input is required (default: true) */
required?: boolean;
/** Default value if skipped or not provided */
default?: unknown;
/** Options for 'select' type */
options?: SelectOption[];
/** Min value for 'number' type */
min?: number;
/** Max value for 'number' type */
max?: number;
/** Snap to grid for 'point' type */
snapToGrid?: boolean;
/** Validation function - returns true or error message */
validate?: (value: unknown, collected: Record<string, unknown>) => boolean | string;
/** Filter function for node/edge picking */
filter?: (item: UINodeState, collected: Record<string, unknown>) => boolean;
};
type Point = {
x: number;
y: number;
};
/**
* Visual feedback to show on the canvas during input collection.
*/
type CommandFeedback = {
/** Node to highlight */
highlightNodeId?: string;
/** Preview edge to draw */
previewEdge?: {
from: Point;
toCursor: boolean;
} | {
from: Point;
to: Point;
};
/** Ghost node following cursor */
ghostNode?: {
position: Point;
opacity: number;
};
/** Show crosshair at cursor */
crosshair?: boolean;
};
/**
* Payload for creating a new node.
*/
type CreateNodePayload = {
graph_id: string;
node_type: string;
label?: string;
configuration?: Record<string, unknown>;
ui_properties?: Record<string, unknown>;
};
/**
* Payload for creating a new edge.
*/
type CreateEdgePayload = {
graph_id: string;
source_node_id: string;
target_node_id: string;
edge_type?: string;
ui_properties?: Record<string, unknown>;
};
/**
* Context passed to command execute() function.
* Provides access to state, mutations, and current graph context.
*/
type CommandContext = {
/** Jotai getter for reading atoms */
get: Getter;
/** Jotai setter for writing atoms */
set: Setter;
/** Current graph ID */
currentGraphId: string | null;
/** Set of currently selected node IDs */
selectedNodeIds: Set<string>;
/** Current viewport state */
viewport: {
zoom: number;
pan: {
x: number;
y: number;
};
};
/** Pre-bound mutation functions (provided by app) */
mutations: {
createNode: (payload: CreateNodePayload) => Promise<unknown>;
updateNode: (nodeId: string, updates: Record<string, unknown>) => Promise<void>;
deleteNode: (nodeId: string) => Promise<void>;
createEdge: (payload: CreateEdgePayload) => Promise<unknown>;
deleteEdge: (edgeId: string) => Promise<void>;
};
/** Layout functions */
layout: {
fitToBounds: (mode: 'graph' | 'selection', padding?: number) => void;
applyForceLayout: () => Promise<void>;
applyTreeLayout: (opts?: {
direction?: 'top-down' | 'left-right';
}) => Promise<void>;
applyGridLayout: (opts?: {
columns?: number;
}) => Promise<void>;
};
/** History functions */
history: {
undo: () => void;
redo: () => void;
};
};
/**
* Category for organizing commands.
*/
type CommandCategory = 'nodes' | 'edges' | 'viewport' | 'selection' | 'history' | 'layout' | 'custom';
/**
* Full command definition.
*/
type CommandDefinition = {
/** Unique command name */
name: string;
/** Alternative names/shortcuts (e.g., ['cn'] for createNode) */
aliases?: string[];
/** Human-readable description */
description: string;
/** Category for organization and filtering */
category: CommandCategory;
/** Sequential input definitions */
inputs: InputDefinition[];
/** Execute the command with collected inputs */
execute: (inputs: Record<string, unknown>, ctx: CommandContext) => Promise<void>;
/**
* Optional: Generate visual feedback during input collection.
* Called after each input is collected.
*/
feedback?: (collected: Record<string, unknown>, currentInput: InputDefinition) => CommandFeedback | null;
};
/**
* Command line state machine phases.
*/
type CommandLineState = {
phase: 'idle';
} | {
phase: 'searching';
query: string;
suggestions: CommandDefinition[];
} | {
phase: 'collecting';
command: CommandDefinition;
inputIndex: number;
collected: Record<string, unknown>;
} | {
phase: 'executing';
command: CommandDefinition;
} | {
phase: 'error';
message: string;
};
/**
* useCommandLine Hook
*
* Main hook for managing command line state and actions.
* Provides access to visibility, state, history, and actions.
*/
/**
* Hook to manage command line state and actions.
*
* @example
* ```tsx
* function CommandLineToggle() {
* const { visible, open, close } = useCommandLine();
*
* return (
* <button onClick={visible ? close : open}>
* {visible ? 'Close' : 'Open'} Command Line
* </button>
* );
* }
* ```
*/
declare function useCommandLine(): {
visible: boolean;
state: CommandLineState;
history: string[];
currentInput: InputDefinition | null;
progress: {
current: number;
total: number;
} | null;
open: () => void;
close: () => void;
updateQuery: (query: string) => void;
selectCommand: (command: CommandDefinition) => void;
isSearching: boolean;
isCollecting: boolean;
isExecuting: boolean;
hasError: boolean;
errorMessage: string | null;
allCommands: CommandDefinition[];
searchCommands: (query: string) => CommandDefinition[];
};
interface VisibleBounds {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
/**
* useVirtualization Hook
*
* Provides virtualization metrics and controls for debugging and monitoring.
* Useful for performance tuning and understanding what's being rendered.
*/
interface VirtualizationMetrics {
/** Whether virtualization is currently enabled */
enabled: boolean;
/** Total number of nodes in the graph */
totalNodes: number;
/** Total number of edges in the graph */
totalEdges: number;
/** Number of nodes currently being rendered */
visibleNodes: number;
/** Number of edges currently being rendered */
visibleEdges: number;
/** Number of nodes culled (not rendered) */
culledNodes: number;
/** Number of edges culled (not rendered) */
culledEdges: number;
/** Current visible bounds in world coordinates, or null if unavailable */
bounds: VisibleBounds | null;
}
interface UseVirtualizationReturn extends VirtualizationMetrics {
/** Enable virtualization */
enable: () => void;
/** Disable virtualization (render all nodes/edges) */
disable: () => void;
/** Toggle virtualization on/off */
toggle: () => void;
}
/**
* Hook for accessing virtualization state and controls.
*
* @example
* ```tsx
* function PerformanceMonitor() {
* const {
* enabled,
* visibleNodes,
* totalNodes,
* culledNodes,
* toggle
* } = useVirtualization();
*
* return (
* <div>
* <p>Rendering {visibleNodes}/{totalNodes} nodes ({culledNodes} culled)</p>
* <button onClick={toggle}>
* {enabled ? 'Disable' : 'Enable'} Virtualization
* </button>
* </div>
* );
* }
* ```
*/
declare function useVirtualization(): UseVirtualizationReturn;
/**
* Multi-tap Gesture Hook
*
* Cross-input multi-tap detection (works with touch, pencil, and mouse).
* Replaces the mouse-only onDoubleClick/onTripleClick handlers.
*
* Usage:
* const handleTap = useTapGesture({
* onSingleTap: () => selectNode(id),
* onDoubleTap: () => lockNode(id),
* onTripleTap: () => expandNode(id),
* });
*
* // In your gesture handler:
* onPointerUp = (e) => handleTap(e);
*/
interface UseTapGestureOptions {
/** Called on single tap (after delay to disambiguate from double-tap) */
onSingleTap?: (event: PointerEvent) => void;
/** Called on double tap */
onDoubleTap?: (event: PointerEvent) => void;
/** Called on triple tap */
onTripleTap?: (event: PointerEvent) => void;
/** Max time between taps to count as multi-tap (ms). Default: 300 */
tapDelay?: number;
/** Max distance between taps to count as multi-tap (px). Default: 25 */
tapDistance?: number;
}
/**
* Hook for detecting single, double, and triple taps across all input types.
*
* Returns a function to call from onPointerUp (or tap handler).
* Uses a timer to disambiguate between single and multi-taps.
*/
declare function useTapGesture(options?: UseTapGestureOptions): {
handleTap: (event: PointerEvent) => void;
cleanup: () => void;
};
/**
* Legacy hook surface.
*
* Arrow-key navigation now lives in the shared input runtime under
* `InputProvider`. This hook remains as a read-only helper so callers
* can still observe focused node state without mounting another listener.
*/
declare function useArrowKeyNavigation(): {
focusedNodeId: string | null;
};
/**
* Canvas Graph Hook
*
* Convenience hook for reading graph structure (node/edge counts and keys).
*/
interface UseCanvasGraphReturn {
/** Number of nodes in the graph */
nodeCount: number;
/** Number of edges in the graph */
edgeCount: number;
/** All node IDs */
nodeKeys: string[];
/** All edge IDs */
edgeKeys: string[];
/** Get a node's attributes by ID (returns undefined if not found) */
getNode: (id: string) => GraphNodeAttributes | undefined;
/** Get an edge's attributes by ID (returns undefined if not found) */
getEdge: (id: string) => GraphEdgeAttributes | undefined;
}
/**
* Hook to access graph structure information.
*
* @example
* ```tsx
* function GraphInfo() {
* const { nodeCount, edgeCount, getNode } = useCanvasGraph();
* return <div>{nodeCount} nodes, {edgeCount} edges</div>;
* }
* ```
*/
declare function useCanvasGraph(): UseCanvasGraphReturn;
/**
* Zoom Transition Hook
*
* Drives the requestAnimationFrame loop for smooth zoom/pan animations.
* Reads the animation target from zoomAnimationTargetAtom and interpolates
* zoom/pan values with cubic ease-in-out.
*/
interface UseZoomTransitionReturn {
/** Whether an animation is currently running */
isAnimating: boolean;
/** Current transition progress (0-1) */
progress: number;
/** Cancel any running animation */
cancel: () => void;
}
/**
* Hook that drives zoom/pan animations via requestAnimationFrame.
*
* @example
* ```tsx
* function Canvas() {
* const { isAnimating, progress } = useZoomTransition();
* return <div style={{ opacity: isAnimating ? 1 - progress * 0.3 : 1 }}>...</div>;
* }
* ```
*/
declare function useZoomTransition(): UseZoomTransitionReturn;
/**
* Hook for detecting two-finger split gesture on a node.
*
* Tracks 2 finger pointers on the node element. When they diverge
* past SPLIT_THRESHOLD (80px screen distance increase), the node is
* split into two copies with all edges duplicated.
*
* Uses raw PointerEvents (not @use-gesture pinch) so it doesn't
* conflict with the Viewport's pinch-to-zoom.
*/
interface UseSplitGestureReturn {
onPointerDown: (e: React.PointerEvent) => void;
onPointerMove: (e: React.PointerEvent) => void;
onPointerUp: (e: React.PointerEvent) => void;
}
declare function useSplitGesture(nodeId: NodeId): UseSplitGestureReturn;
/**
* Animated Layout Transition Hook
*
* Shared hook that smoothly interpolates nodes from current positions
* to target positions using requestAnimationFrame with cubic easing.
* Used by tree/grid layout hooks.
*/
interface UseAnimatedLayoutOptions {
/** Callback to persist positions after animation completes */
onPositionsChanged?: (updates: NodePositionUpdate[]) => void | Promise<void>;
/** Animation duration in ms. Default: 400 */
duration?: number;
}
interface UseAnimatedLayoutReturn {
/** Animate nodes to target positions */
animate: (targets: Map<string, NodePosition>, label?: string) => Promise<void>;
/** Whether an animation is currently running */
isAnimating: boolean;
}
declare function useAnimatedLayout(options?: UseAnimatedLayoutOptions): UseAnimatedLayoutReturn;
/**
* Tree Layout Hook
*
* Arranges nodes in a hierarchical tree (top-down or left-to-right).
* Roots are nodes with no incoming edges. Uses BFS for depth assignment
* and distributes children horizontally at each level.
*/
interface UseTreeLayoutOptions extends UseAnimatedLayoutOptions {
/** Layout direction. Default: 'top-down' */
direction?: 'top-down' | 'left-right';
/** Vertical gap between levels. Default: 200 */
levelGap?: number;
/** Horizontal gap between siblings. Default: 100 */
nodeGap?: number;
}
declare function useTreeLayout(options?: UseTreeLayoutOptions): {
applyLayout: () => Promise<void>;
isRunning: boolean;
};
/**
* Grid Layout Hook
*
* Arranges nodes in a uniform grid. Sorts by existing position
* (top-left to bottom-right) to preserve rough spatial order.
*/
interface UseGridLayoutOptions extends UseAnimatedLayoutOptions {
/** Number of columns. Default: auto (sqrt of node count) */
columns?: number;
/** Gap between cells. Default: 80 */
gap?: number;
}
declare function useGridLayout(options?: UseGridLayoutOptions): {
applyLayout: () => Promise<void>;
isRunning: boolean;
};
/**
* Node Type Registry
*
* Maps node types to their UI components.
* Apps register their custom node components here.
*/
/**
* Props passed to node type components.
*/
interface NodeTypeComponentProps {
/** Full node state */
nodeData: UINodeState;
/** Whether the node is being resized */
isResizing?: boolean;
}
/**
* A component that renders a specific node type.
*/
type NodeTypeComponent = React$1.ComponentType<NodeTypeComponentProps>;
/**
* Edge Path Calculators
*
* Configurable edge path calculation strategies.
* Allows customization of how edges are drawn between nodes.
*/
interface EdgePathInput {
/** Source point (center of source node) */
x1: number;
y1: number;
/** Target point (center of target node) */
x2: number;
y2: number;
/** Source node dimensions */
sourceWidth: number;
sourceHeight: number;
/** Target node dimensions */
targetWidth: number;
targetHeight: number;
}
interface EdgePathResult {
/** SVG path d attribute */
path: string;
/** Midpoint for label placement */
labelX: number;
labelY: number;
}
/**
* Edge path calculator function type
*/
type EdgePathCalculator = (input: EdgePathInput) => EdgePathResult;
/**
* Plugin Types
*
* Type definitions for the canvas plugin system.
* A plugin is a declarative manifest that bundles registrations
* across all canvas subsystems (node types, commands, actions,
* gesture contexts, edge calculators).
*/
/**
* A canvas plugin bundles registrations across all subsystems.
*
* @example
* ```ts
* const myPlugin: CanvasPlugin = {
* id: 'my-plugin',
* name: 'My Plugin',
* nodeTypes: { 'custom-node': CustomNodeComponent },
* commands: [myCommand],
* gestureContexts: [myGestureContext],
* };
* ```
*/
interface CanvasPlugin {
/** Unique plugin identifier */
id: string;
/** Human-readable name */
name: string;
/** Semver version string */
version?: string;
/** Plugin IDs this plugin depends on (must be registered first) */
dependencies?: string[];
/** Map of node_type string → React component */
nodeTypes?: Record<string, NodeTypeComponent>;
/** Map of calculator name → EdgePathCalculator function */
edgePathCalculators?: Record<string, EdgePathCalculator>;
/** Mapping contexts to push into the gesture pipeline */
gestureContexts?: MappingContext[];
/** Action handlers to register in the gesture dispatcher */
actionHandlers?: Record<string, ActionHandler>;
/** Command definitions to register in the command palette */
commands?: CommandDefinition[];
/** Action definitions to register in the action registry */
actions?: ActionDefinition[];
/**
* Called when the plugin is registered.
* Receives a context with store access.
* Return a cleanup function for teardown.
*/
onRegister?: (ctx: PluginContext) => void | (() => void);
}
/**
* Context passed to plugin lifecycle hooks.
*/
interface PluginContext {
/** The plugin being registered */
pluginId: string;
/** Access to the plugin registry for querying other plugins */
getPlugin: (id: string) => CanvasPlugin | undefined;
/** Check if a plugin is registered */
hasPlugin: (id: string) => boolean;
}
/**
* usePlugin — React hook for plugin registration
*
* Registers a plugin on mount and unregisters on unmount.
* Safe to call with the same plugin across re-renders — it
* only registers once (keyed by plugin.id).
*/
/**
* Register a canvas plugin for the lifetime of the component.
*
* @example
* ```tsx
* const myPlugin: CanvasPlugin = {
* id: 'my-plugin',
* name: 'My Plugin',
* nodeTypes: { 'custom': CustomNode },
* };
*
* function App() {
* usePlugin(myPlugin);
* return <Canvas ... />;
* }
* ```
*/
declare function usePlugin(plugin: CanvasPlugin): void;
/**
* Register multiple plugins at once.
* Plugins are registered in order, so dependencies should come first.
*
* @example
* ```tsx
* usePlugins([basePlugin, extensionPlugin]);
* ```
*/
declare function usePlugins(plugins: CanvasPlugin[]): void;
export { type CanvasDragState, type CanvasSelectionState, type CanvasViewportState, FitToBoundsMode, type NodePositionUpdate, type Rect, type ResizeDirection, type UseActionExecutorOptions, type UseActionExecutorReturn, type UseAnimatedLayoutOptions, type UseAnimatedLayoutReturn, type UseCanvasGraphReturn, type UseCanvasHistoryOptions, type UseCanvasSettingsReturn, type UseForceLayoutOptions, type UseGridLayoutOptions, type UseNodeDragOptions, type UseNodeResizeOptions, type UseNodeResizeProps, type UseSplitGestureReturn, type UseTapGestureOptions, type UseTreeLayoutOptions, type UseVirtualizationReturn, type UseZoomTransitionReturn, type VirtualizationMetrics, useActionExecutor, useAnimatedLayout, useArrowKeyNavigation, useCanvasDrag, useCanvasGraph, useCanvasHistory, useCanvasSelection, useCanvasSettings, useCanvasViewport, useCommandLine, useFitToBounds, useForceLayout, useGestureResolver, useGetGraphBounds, useGridLayout, useLayout, useNodeDrag, useNodeResize, useNodeSelection, usePlugin, usePlugins, useSelectionBounds, useSplitGesture, useTapGesture, useTreeLayout, useVirtualization, useZoomTransition };