@ssota-labs/canvasdown-reactflow
v0.8.0
Published
**React Flow adapter for Canvasdown** — Render Canvasdown DSL diagrams in React Flow with your custom components.
Downloads
1,096
Maintainers
Readme
@ssota-labs/canvasdown-reactflow
React Flow adapter for Canvasdown — Render Canvasdown DSL diagrams in React Flow with your custom components.
Overview
@ssota-labs/canvasdown-reactflow provides React hooks and components to integrate Canvasdown DSL with React Flow. It converts Canvasdown graph data into React Flow nodes and edges, allowing you to use your existing React Flow node components.
Installation
npm install @ssota-labs/canvasdown-reactflow @ssota-labs/canvasdown @xyflow/react react react-domPeer Dependencies:
react^19.0.0react-dom^19.0.0@xyflow/react^12.8.2
Quick Start
import { CanvasdownCore } from '@ssota-labs/canvasdown';
import { useCanvasdown } from '@ssota-labs/canvasdown-reactflow';
import { ReactFlow } from '@xyflow/react';
import { MarkdownBlock } from './components/MarkdownBlock';
import { ShapeBlock } from './components/ShapeBlock';
import { ZoneBlock } from './components/ZoneBlock';
// 1. Set up core with block types
const core = new CanvasdownCore({
defaultExtent: 'parent', // Optional: constrain zone children to parent bounds
});
core.registerBlockType({
name: 'shape',
defaultProperties: { shapeType: 'rectangle', color: 'blue' },
defaultSize: { width: 200, height: 100 },
});
// Register zone type (group node)
core.registerBlockType({
name: 'zone',
isGroup: true, // Mark as group node
defaultProperties: {
direction: 'TB',
color: 'gray',
padding: 20,
},
defaultSize: { width: 400, height: 300 },
});
// 2. Map block types to your React components
const nodeTypes = {
shape: ShapeBlock,
markdown: MarkdownBlock,
zone: ZoneBlock, // Group node component
};
function MyCanvas() {
const dsl = `
canvas LR
@shape start "Start" { color: green }
@markdown content "# Hello" { theme: dark }
start -> content
`;
// Or with zones
const dslWithZones = `
canvas TB
@zone thesis "Core Thesis" {
direction: TB,
color: blue
}
@shape main_thesis "Main Argument" { shapeType: ellipse, color: blue }
@end
@zone claims "Supporting Claims" {
direction: LR,
color: green
}
@shape claim1 "Claim 1" { shapeType: rectangle, color: green }
@shape claim2 "Claim 2" { shapeType: rectangle, color: green }
@end
main_thesis -> claim1
`;
const { nodes, edges, error } = useCanvasdown(dslWithZones, { core });
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView>
<Background />
<Controls />
</ReactFlow>
);
}Features
parseCanvasdownFunction — Synchronous DSL parsing without React hooksuseCanvasdownHook — Parse DSL and get React Flow nodes/edges- NodeTypes generic — Type-safe node types: pass
nodeTypesto constrainnodes[].typeto your registered keys useCanvasdownPatchHook — Incrementally update canvas with Patch DSL- Custom Edge Component —
CustomEdgewith label and marker (arrow) support - Edge markers — Configure
markerEnd/markerStartin DSL or via edge typeedgePropertySchema - State Management —
CanvasStateManagerfor advanced use cases - Type Safety — Full TypeScript support
- Zone/Group Support — Automatic conversion of zones to React Flow group nodes with
parentIdandextent
Functions
parseCanvasdown
Parse DSL synchronously and convert to React Flow nodes/edges without React hooks. Useful when you need direct control over parsing or when working outside React components.
import { useMemo } from 'react';
import { parseCanvasdown } from '@ssota-labs/canvasdown-reactflow';
function MyCanvas() {
const dsl = `
canvas LR
@shape start "Start" { color: green }
@shape end "End" { color: red }
start -> end
`;
const { nodes, edges, error } = useMemo(() => {
return parseCanvasdown(dsl, {
core: canvasdownCore,
direction: 'LR', // Optional: override DSL direction
nodeTypes: nodeTypes, // Optional: for type safety
});
}, [dsl, core]);
if (error) {
return <div>Error: {error}</div>;
}
return <ReactFlow nodes={nodes} edges={edges} />;
}Options:
core: CanvasdownCore— Core instance with registered typesdirection?: 'LR' | 'RL' | 'TB' | 'BT'— Override layout directionnodeTypes?: TNodeTypes— Optional. Pass your React FlownodeTypesobject for type safety
Returns:
nodes: Node[]— React Flow nodesedges: Edge[]— React Flow edges (includesourceHandle/targetHandlefrom layout direction)error: string | null— Parsing error message if any
When to use parseCanvasdown vs useCanvasdown:
- Use
parseCanvasdownwhen you need synchronous parsing without React render cycle dependencies - Use
parseCanvasdownwhen working outside React components or in event handlers - Use
useCanvasdownwhen you want automatic memoization based on dependencies
Hooks
useCanvasdown
Parse DSL and get React Flow nodes and edges.
import { useCanvasdown } from '@ssota-labs/canvasdown-reactflow';
function MyCanvas() {
const { nodes, edges, error } = useCanvasdown(dsl, {
core: canvasdownCore,
direction: 'LR', // Optional: override DSL direction
});
return <ReactFlow nodes={nodes} edges={edges} />;
}Options:
core: CanvasdownCore— Core instance with registered typesdirection?: 'LR' | 'RL' | 'TB' | 'BT'— Override layout directionnodeTypes?: TNodeTypes— Optional. Pass your React FlownodeTypesobject for type safety (see below)
Returns:
nodes: Node[]— React Flow nodesedges: Edge[]— React Flow edges (includesourceHandle/targetHandlefrom layout direction)error: string | null— Parsing error message if any
Type-safe node types (NodeTypes generic)
Pass nodeTypes so that returned nodes have type constrained to your registered keys. Fully backward compatible.
const nodeTypes = {
shape: ShapeBlock,
markdown: MarkdownBlock,
zone: ZoneBlock,
} as const;
const { nodes, edges } = useCanvasdown(dsl, {
core,
nodeTypes, // nodes[].type is now 'shape' | 'markdown' | 'zone'
});
// TypeScript knows the exact node type keys
<ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} />;useCanvasdownPatch
Incrementally update canvas with Patch DSL.
import { useCanvasdownPatch } from '@ssota-labs/canvasdown-reactflow';
function MyCanvas() {
const initialDsl = `
canvas LR
@shape a "Node A"
@shape b "Node B"
a -> b
`;
const { nodes, edges, applyPatch, error } = useCanvasdownPatch(initialDsl, {
core: canvasdownCore,
});
const handleAddNode = () => {
applyPatch(`
@add [shape:c] "Node C" { color: purple }
@connect b -> c
`);
};
return (
<>
<button onClick={handleAddNode}>Add Node</button>
<ReactFlow nodes={nodes} edges={edges} />
</>
);
}Options:
core: CanvasdownCore— Core instance with registered typesdirection?: 'LR' | 'RL' | 'TB' | 'BT'— Override layout direction
Returns:
nodes: Node[]— React Flow nodesedges: Edge[]— React Flow edgesapplyPatch: (patchDsl: string) => void— Apply patch DSLerror: Error | null— Parsing error if any
Components
CustomEdge
Custom edge component with label and marker support.
import { CustomEdge } from '@ssota-labs/canvasdown-reactflow';
const edgeTypes = {
default: CustomEdge,
};
<ReactFlow nodes={nodes} edges={edges} edgeTypes={edgeTypes} />;The CustomEdge component automatically handles:
- Edge labels (center, start, end)
- Edge markers — Renders SVG markers for
markerEnd/markerStartwhen set (e.g.arrowclosed,arrow) - Custom edge styles
- Selection styling
Advanced Usage
State Manager
For more control over canvas state:
import { CanvasdownCore } from '@ssota-labs/canvasdown';
import {
CanvasStateManager,
toReactFlowGraph,
} from '@ssota-labs/canvasdown-reactflow';
const core = new CanvasdownCore();
// ... register types
const manager = new CanvasStateManager(core);
// Parse DSL
const result = core.parseAndLayout(dsl);
// Convert to React Flow
const { nodes, edges } = toReactFlowGraph(result);
// Update with patch
const patchResult = manager.applyPatch(result, patchDsl);
const { nodes: newNodes, edges: newEdges } = toReactFlowGraph(patchResult);Manual Conversion
Convert Canvasdown graph data to React Flow format:
import { CanvasdownCore } from '@ssota-labs/canvasdown';
import {
toReactFlowEdges,
toReactFlowNodes,
} from '@ssota-labs/canvasdown-reactflow';
const core = new CanvasdownCore();
const result = core.parseAndLayout(dsl);
const nodes = toReactFlowNodes(result.nodes);
const edges = toReactFlowEdges(result.edges);Using Your React Components
Your React Flow node components receive Canvasdown properties via the data prop:
// DSL
@kanban-card task1 "Implement Login" {
status: "in-progress"
assignee: "alice"
priority: "high"
}// Your component
import { NodeProps } from '@xyflow/react';
function KanbanCard({ data }: NodeProps) {
const { status, assignee, priority } = data;
return (
<Card className={`status-${status} priority-${priority}`}>
<h3>{data.label}</h3>
<Badge>{status}</Badge>
<Avatar user={assignee} />
</Card>
);
}
// Register and use
const nodeTypes = {
'kanban-card': KanbanCard,
};Zone/Group Nodes
Zones are automatically converted to React Flow group nodes. Create a group node component:
import { Handle, NodeProps, Position } from '@xyflow/react';
function ZoneBlock({ data, selected }: NodeProps) {
const { label, color, padding } = data;
return (
<div
style={{
border: `2px solid ${color}`,
borderRadius: '8px',
padding: `${padding}px`,
backgroundColor: `${color}20`,
minWidth: '200px',
minHeight: '100px',
}}
>
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}>{label}</div>
{/* Children will be rendered inside this group */}
</div>
);
}
// Register zone type
core.registerBlockType({
name: 'zone',
isGroup: true,
defaultProperties: { direction: 'TB', color: 'gray', padding: 20 },
defaultSize: { width: 400, height: 300 },
});
const nodeTypes = {
zone: ZoneBlock,
};Zone Features:
- Children automatically get
parentIdset to the zone's ID extentproperty controls whether children are constrained within zone boundaries- Set
defaultExtent: 'parent'inCanvasdownCoreconstructor to constrain all zone children - Or set
extent: 'parent'per-node in DSL to override default - Set
extent: undefinedor omit it to allow free movement of children
Patch Operations
Supported Patch DSL commands:
@add [blockType:id] "Label" { ... } // Add new block
@update id { property: newValue } // Update block properties
@delete id // Delete block
@connect source -> target // Add edge
@disconnect source -> target // Remove edge
@move id { x: 100, y: 200 } // Move block
@resize id { width: 300, height: 200 } // Resize blockCustomizing @update (data.properties, content → TipTap)
By default, @update merges patch properties into node.data directly. If your nodes store props under data.properties (e.g. color, shape) or need content (markdown string) converted to TipTap JSON, use transformUpdateNode:
import type {
TransformUpdateNode,
UpdateOperation,
} from '@ssota-labs/canvasdown-reactflow';
// TipTap: use your markdown → JSON helper (e.g. @tiptap/core, or a custom parser)
import { generateJSON } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
import type { Node } from '@xyflow/react';
const markdownToTipTapJson = (markdown: string) =>
generateJSON(markdown, [StarterKit]);
const transformUpdateNode: TransformUpdateNode = (node, operation) => {
const nextData = { ...node.data };
// 1. Put shape/color etc. under data.properties
if (operation.properties) {
const { content, ...rest } = operation.properties;
nextData.properties = {
...(nextData.properties as Record<string, unknown>),
...rest,
};
// 2. Convert content (markdown) to TipTap JSON
if (content != null && typeof content === 'string') {
nextData.content = markdownToTipTapJson(content);
}
}
if (operation.customProperties?.length) {
nextData.customProperties = [...(nextData.customProperties ?? [])];
for (const { key, value } of operation.customProperties) {
const arr = nextData.customProperties as Array<{
schemaId: string;
value: unknown;
}>;
const i = arr.findIndex(c => c.schemaId === key);
const entry = { schemaId: key, value };
if (i >= 0) arr[i] = entry;
else arr.push(entry);
}
}
return { ...node, data: nextData };
};
// Usage with useCanvasdownPatch
const { applyPatch } = useCanvasdownPatch(core, {
preservePositions: true,
transformUpdateNode,
});
// Then applyPatch('@update node1 { color: blue, content: "# Hello" }') will
// set data.properties.color and data.content (TipTap JSON).- Without
transformUpdateNode: the adapter merges intonode.dataonly (default). - With
transformUpdateNode: you control the whole update (e.g.data.properties, markdown → TipTap in your app).
Patch applied callback (server sync)
After a patch is successfully applied (React Flow state updated), you can run side effects (e.g. sync to server) via onPatchApplied. The callback receives the applied operations and the new nodes/edges so you don't need to re-parse the patch string.
import type { UpdateOperation } from '@ssota-labs/canvasdown';
import type { PatchAppliedResult } from '@ssota-labs/canvasdown-reactflow';
const { applyPatch } = useCanvasdownPatch(core, {
transformUpdateNode,
onPatchApplied(result: PatchAppliedResult) {
const updates = result.operations.filter(
(op): op is UpdateOperation => op.type === 'update'
);
for (const u of updates) {
const blockMountId = nodeIdMapRef.current.get(u.targetId);
if (blockMountId != null && u.properties?.content != null)
updateBlockContentByMountId(
blockMountId,
markdownToTiptap(u.properties.content),
u.properties.content
);
}
},
onPatchError(err) {
toast.error('Patch failed');
},
});- onPatchApplied: Called once after
setNodes/setEdges. Receives{ operations, nodes, edges, patchDsl? }.patchDslis set only when applied viaapplyPatch(dsl); it isundefinedwhen applied viaapplyPatchOperations(ops). Async callbacks are fire-and-forget; rejections are logged. - onPatchError: Called when validation or apply fails. Use for toasts or alerts.
TypeScript
Full TypeScript support with type inference:
import type {
UseCanvasdownOptions,
UseCanvasdownReturn,
} from '@ssota-labs/canvasdown-reactflow';
const options: UseCanvasdownOptions = {
core: canvasdownCore,
direction: 'LR',
};
const { nodes, edges }: UseCanvasdownReturn = useCanvasdown(dsl, options);Generic node types: Use the NodeTypes generic to get type-safe nodes[].type:
const nodeTypes = {
shape: ShapeBlock,
markdown: MarkdownBlock,
} as const;
const { nodes, edges } = useCanvasdown(dsl, {
core,
nodeTypes,
});
// nodes[].type is 'shape' | 'markdown'Examples
Basic Flowchart
function Flowchart() {
const dsl = `
canvas LR
@shape start "Start" { shapeType: ellipse, color: green }
@shape process "Process" { color: blue }
@shape end "End" { shapeType: ellipse, color: red }
start -> process -> end
`;
const { nodes, edges } = useCanvasdown(dsl, { core });
return <ReactFlow nodes={nodes} edges={edges} />;
}With Zones (Groups)
function ZoneCanvas() {
const core = new CanvasdownCore({
defaultExtent: 'parent', // Constrain zone children
});
core.registerBlockType({
name: 'zone',
isGroup: true,
defaultProperties: { direction: 'TB', color: 'gray', padding: 20 },
defaultSize: { width: 400, height: 300 },
});
const dsl = `
canvas TB
@zone thesis "Core Thesis" {
direction: TB,
color: blue
}
@shape main_thesis "Main Argument" { shapeType: ellipse, color: blue }
@end
@zone claims "Supporting Claims" {
direction: LR,
color: green
}
@shape claim1 "Claim 1" { shapeType: rectangle, color: green }
@shape claim2 "Claim 2" { shapeType: rectangle, color: green }
@shape claim3 "Claim 3" { shapeType: rectangle, color: green }
@end
main_thesis -> claim1 : "supports"
claim1 -> claim2
claim2 -> claim3
`;
const { nodes, edges } = useCanvasdown(dsl, { core });
const nodeTypes = {
shape: ShapeBlock,
zone: ZoneBlock,
};
return (
<ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView />
);
}Interactive Canvas with Patches
function InteractiveCanvas() {
const [dsl, setDsl] = useState(initialDsl);
const { nodes, edges, applyPatch } = useCanvasdownPatch(dsl, { core });
const addNode = () => {
applyPatch(`@add [shape:newNode] "New" { color: purple }`);
};
const updateNode = (id: string, color: string) => {
applyPatch(`@update ${id} { color: "${color}" }`);
};
return (
<>
<button onClick={addNode}>Add Node</button>
<ReactFlow
nodes={nodes}
edges={edges}
onNodeClick={e => updateNode(e.node.id, 'red')}
/>
</>
);
}Edge labels and markers
Labels (center, start, end):
const dsl = `
canvas TB
@shape a "Node A"
@shape b "Node B"
a -> b : "main flow" {
startLabel: "→"
endLabel: "✓"
}
`;Markers (arrows at source/target):
Register edge type with edgePropertySchema and set markers in DSL.
In your Canvasdown core setup (e.g. register-block-types.ts):
core.registerEdgeType({
name: 'default',
defaultShape: 'default',
defaultStyle: { stroke: '#b1b1b7', strokeWidth: 2 },
edgePropertySchema: {
markerEnd: {
type: 'enum',
enum: ['arrow', 'arrowclosed'],
description: 'Marker at the end of the edge (target side)',
},
markerStart: {
type: 'enum',
enum: ['arrow', 'arrowclosed'],
description: 'Marker at the start of the edge (source side)',
},
},
});In DSL:
a -> b { markerEnd: "arrowclosed" }
a -> b { markerStart: "arrow", markerEnd: "arrowclosed" }CustomEdge renders SVG marker definitions and passes url(#id) to React Flow’s BaseEdge, so arrows appear without extra setup.
API Reference
parseCanvasdown(dsl: string, options: ParseCanvasdownOptions)
Synchronously parse DSL and return React Flow nodes/edges. Does not depend on React render cycles.
useCanvasdown(dsl: string, options: UseCanvasdownOptions)
Parse DSL and return React Flow nodes/edges. Internally uses parseCanvasdown with useMemo for automatic memoization.
useCanvasdownPatch(initialDsl: string, options: UseCanvasdownPatchOptions)
Initialize canvas and return patch function.
toReactFlowNodes(graphNodes: GraphNode[]) / toReactFlowNodes<TNodeTypes>(graphNodes: GraphNode[])
Convert Canvasdown nodes to React Flow nodes. When using useCanvasdown with the nodeTypes option, the generic constrains returned nodes’ type to your registered keys.
toReactFlowEdges(graphEdges: GraphEdge[], direction?: 'LR' | 'RL' | 'TB' | 'BT')
Convert Canvasdown edges to React Flow edges. Sets sourceHandle / targetHandle from direction and passes through markerEnd / markerStart when present.
toReactFlowGraph(graph: GraphOutput)
Convert entire Canvasdown graph to React Flow format.
CanvasStateManager
State manager for advanced canvas operations.
CustomEdge
React Flow edge component with label and marker (arrow) support. Renders SVG marker definitions for markerEnd / markerStart when provided by the edge data.
Development
# Install dependencies
pnpm install
# Build
pnpm build
# Test
pnpm test
# Test with coverage
pnpm test:coverage
# Type check
pnpm typecheck
# Lint
pnpm lintRelated Packages
@ssota-labs/canvasdown— Core DSL parser and layout engine- Main Canvasdown README — Full documentation
License
MIT
