@squeed/flow-sdk 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # @squeed/flow-sdk
2
2
 
3
- Interactive React flow diagrams from JSON, using DOM nodes, SVG edges, and Dagre layout. Cytoscape is not included.
3
+ Interactive React flow diagrams from JSON, using DOM nodes, SVG edges, and Dagre layout.
4
4
 
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install @squeed/flow-sdk@^2 react@^18 react-dom@^18
8
+ npm install @squeed/flow-sdk@^2 react@^18 react-dom@^18 markdown-to-jsx@^9.10.2
9
9
  ```
10
10
 
11
- Requires React 18 and Chakra UI 3.36 or later. Peer dependencies: `react`, `react-dom`, `@chakra-ui/react`, `@emotion/react`, `@emotion/styled`, and `framer-motion` (11 or 12). npm installs the remaining peers automatically. React 19 is not currently supported.
11
+ Requires React 18 and Chakra UI 3.36 or later. Peer dependencies: `react`, `react-dom`, `@chakra-ui/react`, `@emotion/react`, `@emotion/styled`, `framer-motion` (11 or 12), and `markdown-to-jsx` (^9.10.2). npm installs the remaining peers automatically. React 19 is not currently supported.
12
+
13
+ The Markdown renderer is a required peer supplied by the consuming application, not embedded in the SDK bundle. It remains part of the application's dependency footprint.
12
14
 
13
15
  ## Quick Start
14
16
 
@@ -87,56 +89,561 @@ You can provide either `json` (auto-generates nodes/edges) or `nodes` + `edges`
87
89
 
88
90
  ## Config
89
91
 
92
+ Clicking the empty graph background opens the **Graph theme** swatches after the
93
+ set/array control. They change the default node theme without replacing explicit
94
+ node colors or modifying graph JSON, and hide while a node or edge is selected
95
+ or the diagram is read-only. Without `config.themeColor`, the selection is stored
96
+ locally. For a controlled or persisted theme, pass `config.themeColor` and update
97
+ it from `callbacks.onThemeColorChange(color)`.
98
+
90
99
  ### `FlowDiagramConfig`
91
100
 
92
- | Option | Type | Default | Description |
93
- | ----------------------------------------------------------------------- | ------------------------------------------------ | ------------ | -------------------------------------------------------------------------------------- |
94
- | `direction` | `"LR" \| "RL" \| "TB" \| "BT" \| "C"` | `"LR"` | Layout direction; `C` splits the JSON root's branches |
95
- | `nodeAlignment` | `"leading" \| "center" \| "trailing"` | `"center"` | Align unequal node sizes within each layout rank |
96
- | `edgeMode` | `"workflow" \| "properties"` | `"workflow"` | Attach edges to node boxes or named object-property rows; saved mappings are unchanged |
97
- | `layout` | `LayoutAlgorithm` | `"dagre"` | Only Dagre is implemented; other legacy values do not select a different layout |
98
- | `themeColor` | `string` | `"blue.500"` | Accent color (Chakra token) |
99
- | `colorMode` | `"light" \| "dark"` | `"light"` | Color mode |
100
- | `backgroundPattern` | `"dot" \| "grid" \| "steel" \| "none"` | `"dot"` | Background pattern |
101
- | `backgroundColor` | `string` | — | Background color override |
102
- | `edgeColor` | `string` | — | Edge/line color override |
103
- | `minZoom` | `number` | `0.01` | Minimum zoom level |
104
- | `maxZoom` | `number` | `1.5` | Maximum zoom level |
105
- | `initialViewport` | `{ pan: { x, y }, zoom }` | — | Initial viewport; omitted means fit on first layout |
106
- | `edgeHandles` | `boolean` | — | `false` disables edge drawing; `true` also starts in draw mode |
107
- | `readOnly` | `boolean` | `false` | Disable graph gestures and edits; toolbar navigation remains available |
108
- | `editable` | `boolean` | `false` | Enable inline node label editing |
109
- | `parentNodeStyle` | `ParentNodeStyle` | — | Compound-node fill, border, padding, and label defaults |
110
- | `selectionToolbar` | `SelectionToolbarConfig \| false` | — | Customize palette and hidden controls, or disable selection tools |
111
- | `nodeOverlayActions` | `NodeOverlayAction[]` | — | Actions in node hover overlay |
112
- | `renderNodeOverlay` | `(props: NodeOverlayProps) => ReactNode` | — | Custom node hover overlay |
113
- | `renderSelectedNodeToolbar` | `(props: SelectedNodeToolbarProps) => ReactNode` | — | Custom multi-select toolbar |
114
- | `renderSelectedEdgeToolbar` | `(props: SelectedEdgeToolbarProps) => ReactNode` | — | Custom edge selection toolbar |
115
- | `renderBottomBar` | `(props: BottomBarRenderProps) => ReactNode` | — | Replace the bottom bar |
116
- | `customViews` | `Record<string, ComponentType>` | — | Override node views |
117
- | `disableKeyboardShortcuts` | `boolean` | `false` | Disable built-in keyboard shortcuts |
118
- | `isZoomMode`, `isMultiSelectMode`, `isDragMode`, `isEdgeHandlesEnabled` | `boolean` | — | Externally controlled interaction modes |
101
+ | Option | Type | Default | Description |
102
+ | ----------------------------------------------------------------------- | ------------------------------------------------ | ------------ | ------------------------------------------------------------------------------------------------- |
103
+ | `direction` | `"LR" \| "RL" \| "TB" \| "BT" \| "C"` | `"LR"` | Layout direction; `C` splits the JSON root's branches |
104
+ | `nodeAlignment` | `"leading" \| "center" \| "trailing"` | `"center"` | Align unequal node sizes within each layout rank |
105
+ | `edgeMode` | `"nodes" \| "properties" \| "workflow"` | `"nodes"` | Attach edges to nodes or named property rows; `"workflow"` is a compatibility alias for `"nodes"` |
106
+ | `layout` | `LayoutAlgorithm` | `"dagre"` | Only Dagre is implemented; other legacy values do not select a different layout |
107
+ | `themeColor` | `string` | `"blue.500"` | Accent color (Chakra token) |
108
+ | `colorMode` | `"light" \| "dark"` | `"light"` | Color mode |
109
+ | `backgroundPattern` | `"dot" \| "grid" \| "steel" \| "none"` | `"dot"` | Background pattern |
110
+ | `backgroundColor` | `string` | — | Background color override |
111
+ | `edgeColor` | `string` | — | Edge/line color override |
112
+ | `minZoom` | `number` | `0.01` | Minimum zoom level |
113
+ | `maxZoom` | `number` | `1.5` | Maximum zoom level |
114
+ | `initialViewport` | `{ pan: { x, y }, zoom }` | — | Initial viewport; omitted means fit on first layout |
115
+ | `edgeHandles` | `boolean` | — | `false` disables edge drawing; enabling handles does not activate Draw mode |
116
+ | `readOnly` | `boolean` | `false` | Disable graph gestures and edits; toolbar navigation remains available |
117
+ | `editable` | `boolean` | `false` | Enable inline node label editing |
118
+ | `parentNodeStyle` | `ParentNodeStyle` | — | Compound-node fill, border, padding, and label defaults |
119
+ | `selectionToolbar` | `SelectionToolbarConfig \| false` | — | Customize palette and hidden controls, or disable selection tools |
120
+ | `nodeOverlayActions` | `NodeOverlayAction[]` | — | Actions in node hover overlay |
121
+ | `renderNodeOverlay` | `(props: NodeOverlayProps) => ReactNode` | — | Custom node hover overlay |
122
+ | `renderSelectedNodeToolbar` | `(props: SelectedNodeToolbarProps) => ReactNode` | — | Custom multi-select toolbar |
123
+ | `renderSelectedEdgeToolbar` | `(props: SelectedEdgeToolbarProps) => ReactNode` | — | Custom edge selection toolbar |
124
+ | `renderBottomBar` | `(props: BottomBarRenderProps) => ReactNode` | — | Replace the bottom bar |
125
+ | `customViews` | `Record<string, ComponentType>` | — | Override node views |
126
+ | `disableKeyboardShortcuts` | `boolean` | `false` | Disable built-in keyboard shortcuts |
127
+ | `isZoomMode`, `isMultiSelectMode`, `isDragMode`, `isEdgeHandlesEnabled` | `boolean` | — | Externally controlled interaction modes |
119
128
 
120
129
  Use `$collapsed: true` to collapse a JSON branch, and `false` to keep the expansion control. Automatic depth-based collapse is not part of the current API.
121
130
 
122
131
  `nodeAlignment: "leading"` aligns left edges in `LR`/`C`, right edges in `RL`, top edges in `TB`, and bottom edges in `BT`. `"trailing"` aligns the opposite edge. Node dimensions and graph connections are unchanged; containers and edge paths follow the aligned positions.
123
132
 
133
+ ## JSON Streaming And Playback
134
+
135
+ `createJsonStream` accepts incremental `append(chunk)` or cumulative
136
+ `snapshot(text)` input. Supply `parse`, `repair`, `preview`, and `commit`
137
+ callbacks: repaired previews are throttled, while `finish()` strictly parses the
138
+ original JSON before committing. `cancel()` clears pending work. The defaults
139
+ are a 150 ms preview interval and a 1 MiB input limit. The host supplies its
140
+ repair library, such as `jsonrepair`; the SDK has no model or transport dependency.
141
+
142
+ `createJsonPlayback` reuses that buffer for a saved document:
143
+
144
+ ```ts
145
+ const player = createJsonPlayback({
146
+ ...streamOptions,
147
+ source: JSON.stringify(document),
148
+ onChange: ({ position, total, playing }) =>
149
+ updateProgress(position, total, playing),
150
+ onError: reportError,
151
+ });
152
+ player.play();
153
+ player.pause();
154
+ player.seek(0);
155
+ player.dispose();
156
+ ```
157
+
158
+ Playback defaults to eight seconds, supports resuming and replaying, and reports
159
+ character offsets for a progress slider. Use `projectJsonPlaybackPreview(partial,
160
+ document)` when replaying Squeed JSON to keep structured metadata such as `$state`
161
+ references atomic. Keep previews separate from saved documents. SDK Test includes
162
+ a themed top-right player that reveals connections with their endpoints and
163
+ automatically fits every new layout without modifying the saved document.
164
+
165
+ ## Custom Node Editing
166
+
167
+ Custom views replace the node renderer. Wrap custom visuals in the exported
168
+ `NodeView` to reuse SDK selection, multi-selection, overlays, and collapse
169
+ controls. Use `EditableLabel` for the standard inline editor, including
170
+ read-only handling and `onNodeLabelChange` notifications.
171
+
172
+ ```tsx
173
+ import { NodeView, EditableLabel } from "@squeed/flow-sdk";
174
+ import type { FlowDiagramNode } from "@squeed/flow-sdk";
175
+
176
+ function CustomNode({ data }: FlowDiagramNode) {
177
+ return (
178
+ <NodeView data={data}>
179
+ {({ isActive, isEditing, onEditStart, onEditEnd }) => (
180
+ <div
181
+ data-flow-node-surface=""
182
+ data-selected={isActive}
183
+ style={{ width: 240, height: 160 }}
184
+ >
185
+ <EditableLabel
186
+ nodeId={data.id}
187
+ label={data.label ?? "Custom"}
188
+ color={data.viewProps?.textColor ?? "gray.900"}
189
+ isEditing={isEditing}
190
+ onEditStart={onEditStart}
191
+ onEditEnd={onEditEnd}
192
+ />
193
+ </div>
194
+ )}
195
+ </NodeView>
196
+ );
197
+ }
198
+ ```
199
+
200
+ Register the component under its node type in `config.customViews`. Do not stop
201
+ click propagation on the visual surface or maintain a separate selection state.
202
+ The custom renderer must apply the styling fields it supports, such as
203
+ `data.viewProps.bgColor`, `borderColor`, `borderWidth`, and `outline`, to its
204
+ HTML or SVG. Raw-graph callbacks must update node data; JSON mode persists edits
205
+ through `onJsonChange`. The wrapper does not change custom SVG geometry.
206
+
207
+ Mark the visible shape with `data-flow-node-surface`, excluding captions and
208
+ form summaries. For SVG views, put it on the closed geometry element (such as
209
+ `circle`, `polygon`, or `path`), not the surrounding `svg`. Connectors, draw
210
+ handles, and animated borders use that surface; layout still reserves the full
211
+ node bounds. Unmarked custom views retain rectangular host bounds.
212
+
213
+ For content and custom-shape views, use `<FlowPort id="tools" role="source"
214
+ side="bottom" position={0.5} />` inside the node. `side` accepts `top`, `right`,
215
+ `bottom`, or `left`; `position` ranges from 0 to 1 and defaults to the midpoint.
216
+ Top/bottom positions run left to right; left/right positions run top to bottom.
217
+ The handle and connector attach to the marked outline, including SVG slopes.
218
+ These ports work in Auto and Properties modes. Omitting
219
+ `side` retains the existing property-row behavior.
220
+
221
+ Built-in views accept the same declarations in `data.config.ports` or JSON
222
+ `$ports`, for example `[{ "id": "prompt", "role": "target", "side": "top" }]`.
223
+ Connections identify these ports using `sourceProperty` and `targetProperty`.
224
+ Read-only mode and disabled edge handles continue to prevent dragging.
225
+
226
+ `config.edgeMode` controls visibility: `"auto"` (default) shows both normal
227
+ node edges and property/port edges; `"nodes"` shows only edges without property
228
+ endpoints; `"properties"` shows only edges with `sourceProperty` or
229
+ `targetProperty`. `"workflow"` remains an alias for `"nodes"`. Switching modes
230
+ does not remove saved edges or change the graph used for layout by default.
231
+
232
+ Set `config.compactPropertyLayout: true` to re-layout and fit only visible
233
+ nodes and edges in Properties mode and in isolated views (including Auto and
234
+ Nodes modes). Hidden nodes and connections no longer reserve layout space or
235
+ influence edge routes. This flag defaults to `false`; SDK Test enables it.
236
+ The expanded bottom toolbar includes the labelled **Compact layout**
237
+ icon toggle in Auto, Nodes, and Properties modes. In SDK Test it follows
238
+ **Connected nodes** and stays in sync with the Layout-panel pill.
239
+ The SDK manages the toggle locally when `config.compactPropertyLayout` is
240
+ omitted. For controlled usage, handle
241
+ `callbacks.onCompactPropertyLayoutChange(enabled)` and update the config flag.
242
+ Leaving Properties mode or restoring an isolated view restores the applicable
243
+ full layout. Saved graph data is unchanged. Without the flag, isolation keeps
244
+ the existing node positions.
245
+
246
+ ## Bind Your Own Data
247
+
248
+ Start with the [minimal properties example](examples/form-connection/README.md):
249
+ two nodes, plain state, one binding, and a three-field interface. Its
250
+ [React component](examples/form-connection/MinimalExample.tsx) shows the complete
251
+ setup, including form edits, display references, appearance, and connection modes.
252
+ No workflow backend is required.
253
+
254
+ Then try **Shared Configuration** and **Reusable Property** in SDK Test's Templates
255
+ menu. The [small-step walkthrough](examples/configuration-basics/README.md) shows
256
+ two consumers sharing values, editing them, and exporting plain data, before
257
+ introducing field-level mapping and returning values inline.
258
+
259
+ ## Maintainer Guide
260
+
261
+ The SDK Test **Form Connection** dataset loads
262
+ [examples/form-connection/ui.json](examples/form-connection/ui.json), its
263
+ [bindings](examples/form-connection/bindings.json), and the actual
264
+ [TypeScript interface](examples/form-connection/interface.ts). Double-click
265
+ **Generated Form** to edit the separate state shown by the State JSON node.
266
+ The test page preloads the bundled interface; manual uploads can replace it.
267
+
268
+ **Config Catalog** covers all 25 enabled Backflow step types with independently
269
+ bound, reusable configuration nodes. The full editor interface retains all
270
+ 138 declared config fields, including JSON and dictionaries. This is form
271
+ coverage, not production execution validation. See the
272
+ [coverage and readiness checks](examples/workflow-form/README.md).
273
+
274
+ **Workflow State 2** loads the supplied
275
+ [Run Cast Session workflow](examples/workflow-state-2/workflow.json) as a
276
+ separate local dataset, with 16 step forms and 23 reusable tools. Its
277
+ [view generator](examples/workflow-state-2/example.ts) renders all tools in one
278
+ Object View node, with a distinct output port and connection identity per tool.
279
+ Its `portResources` binding maps those ports to individual state entries.
280
+ Forms edit `config`; the original `params`, `input`, and other
281
+ workflow fields are retained independently. Dependency links initialize the
282
+ view from `dependsOn`; diagram edits are not a production deployment adapter.
283
+
284
+ SDK Test's **Forms** popover exposes **Reusable configuration** after an
285
+ interface upload, or when a static mapping is configured. Choose an interface
286
+ and check the inline fields to lift into resource nodes. Object values use
287
+ Object View; compact form-bound nodes expose a floating dot to reveal their
288
+ properties, and the object header dot returns to the icon.
289
+
290
+ SDK Test fits the switched node or parent after layout completes. Its
291
+ `focusOptions` prop controls both click focus and dot-switch focus:
292
+
293
+ ```tsx
294
+ <SDKTest focusOptions={{ animate: true, padding: 90 }} />
295
+ ```
296
+
297
+ These are the defaults; `animate: false` makes the fit immediate, and `padding`
298
+ is in screen pixels. The SDK itself does not auto-focus on dot switches.
299
+ `callbacks.onNodeViewToggle(nodeId)` notifies a client of a dot action; clients
300
+ can queue `engine.focusNode(nodeId, padding, animate)` for `onLayoutComplete`.
301
+
302
+ Object rows with an empty string value and an attached property connection
303
+ show a full-width label without an empty value cell. Other fields retain both
304
+ columns, and row order, port identities, and saved values are unchanged.
305
+
306
+ The [mapping adapter](examples/workflow-state/resourceMapping.ts) uses the same
307
+ JSON-serializable definition for static setup and popover edits:
308
+
309
+ ```ts
310
+ const resourceMappings = {
311
+ "workflow-state-2": {
312
+ version: 1 as const,
313
+ templates: true,
314
+ fields: [
315
+ { type: "CastLlmConfig", field: "prompt" },
316
+ { type: "CastLlmConfig", field: "model", createEmpty: true },
317
+ { type: "CastDatabaseConfig", field: "data" },
318
+ ],
319
+ },
320
+ };
321
+
322
+ <SDKTest resourceMappings={resourceMappings} />;
323
+ ```
324
+
325
+ These are also the static defaults for Workflow State 2. An empty Model resource
326
+ does not add a model value to exported configuration until configured. Resource
327
+ forms retain their source interface through `StateFormBinding.schemaType`, so
328
+ reuploading the original interface does not require generated type declarations.
329
+ The popover imports/exports mapping JSON and exports a resolved workflow;
330
+ unmapping restores current resource values inline. Existing annotated resource
331
+ ports stay separate from user-selected inline mappings.
332
+
333
+ `templates` exposes runtime variables as Object View nodes and links step-output
334
+ references to their consuming property rows. Expressions are parsed, never
335
+ evaluated or rewritten; both sides of fallback expressions are represented.
336
+ Collapsing a node retains its links, and deleted template links are not restored
337
+ merely by rerendering. Runtime variable values are not fabricated.
338
+
339
+ [examples/squeed.json](examples/squeed.json) is an expandable diagram for new
340
+ maintainers: setup, architecture, data ownership, UI code, change recipes,
341
+ testing, workflow contracts, and SDK Test integration. Load it through the
342
+ `json` prop; the local Backflow admin SDK Test page includes it as **squeed**
343
+ alongside **advanced**. Its **File Map** chapter links concrete source files
344
+ with labeled runtime imports, type-only dependencies, re-exports, and tooling
345
+ relationships. **Trace a Change Across Files** explains callback/data flow and
346
+ ownership separately. The map covers important internal relationships, not every
347
+ external dependency. Tests verify mapped paths and import/export arrows against
348
+ the source tree. Edit this single JSON source to keep the guide current.
349
+
124
350
  ## Callbacks
125
351
 
352
+ ### All Icon-Label Views
353
+
354
+ Inside a diagram, `useNodeViewActions()` (or `useFlowDiagramContext()`) exposes
355
+ `allNodesIconLabel` and `setAllNodesIconLabel(boolean)`. The bulk action reuses
356
+ `setNodeViewMode(id, "icon-label")` for objects/arrays and `onNodeCollapse` for
357
+ parents. Disable it to restore default object views and reopen parents. Dots use
358
+ the same per-node actions and remain interactive; there is no global view lock
359
+ or separate override state. `allNodesIconLabel` reflects the current node modes.
360
+
361
+ `NodeViewMode` supports `"default"`, `"properties"` and `"icon-label"`; controlled
362
+ consumers continue using `config.nodeViewModes` and `onNodeViewModeChange`.
363
+ Parent changes follow the host's existing collapse persistence behavior. The
364
+ name `collapsed` remains reserved for child-collapse behavior, not object view
365
+ modes. `onAllNodesIconLabelChange` reports bulk actions. SDK Test exposes the
366
+ action under Visibility as **All icon labels** and fits the resulting layout.
367
+
368
+ ### Reusable Nodes From Property Draws
369
+
370
+ SDK Test's **Blank Workflow** starts with zero nodes, steps and resources. The
371
+ canvas **Create node** button opens the existing reusable-node picker without a
372
+ source port. It creates one standalone step with an empty configuration and no
373
+ connection. Subsequent nodes can be created by dragging ports into empty canvas;
374
+ these use the same picker and the existing typed-port and dependency mapping.
375
+ Canceling the picker does not change the graph. No example workflow is imported.
376
+
377
+ SDK Test opens a reusable-node picker when a Properties-mode draw ends on empty
378
+ canvas. It reuses `NodeFormPopover` with a top filter, a fixed-height scrolling
379
+ list and the form vignette. Items use the current JSON bindings, interfaces and
380
+ icons; incompatible ports are disabled and multiple compatible ports can be
381
+ selected explicitly. Closing the modal makes no changes.
382
+
383
+ `callbacks.onNodeCreateRequest(request)` lets a host defer creation by returning
384
+ `true`. The request contains `sourceNodeId`, `sourceProperty`, `reverse` and the
385
+ model-space `position`. A workflow template creates a new step through the
386
+ configured collection/identity/dependency fields; an existing reusable retains
387
+ its shared state and connection. Node-mode drawing is unchanged.
388
+
389
+ After layout, `engine.setNodePosition(id, position)` places a measured node at
390
+ the draw endpoint without changing viewport zoom or pan. This renderer-local
391
+ placement is retained across relayouts while the node remains rendered; pass
392
+ `undefined` to release it back to automatic layout. It is not serialized into
393
+ the workflow document.
394
+
395
+ Use `engine.setNodePosition(id, position, { persist: false })` for an initial
396
+ drop placement that settles into the size-aware automatic layout. The example
397
+ creation picker uses this mode so object expansion, property edits, and the
398
+ spacing slider can reposition new nodes without retaining their drop positions.
399
+
400
+ ### Animated Viewport Actions
401
+
402
+ Capture `FlowEngine` through `callbacks.onInit`. Set `config.animateViewport`
403
+ to animate viewport actions by default, or pass a boolean per call:
404
+
405
+ ```ts
406
+ engine.fit(60, true);
407
+ engine.centerAndFit(60, true);
408
+ engine.center(true);
409
+ engine.focusNode(nodeId, 60, true);
410
+ engine.focusBranch(nodeId, 60, true);
411
+ engine.isolateBranch(nodeId, 60, true);
412
+ engine.restoreView(true);
413
+ ```
414
+
415
+ Animation defaults to false. Explicit false overrides the config default.
416
+ Transitions interpolate pan and zoom over 300ms; center preserves zoom.
417
+ Reduced-motion preferences disable animation. New viewport commands cancel
418
+ previous transitions, and unmount cancels pending frames. Methods return when
419
+ dispatched, not when animation finishes; `onViewportChange` reports intermediate
420
+ and final positions. Use `onLayoutComplete(engine)` to fit newly laid-out data:
421
+
422
+ ```tsx
423
+ <FlowDiagram
424
+ config={{ animateViewport: true }}
425
+ callbacks={{ onLayoutComplete: (engine) => engine.centerAndFit(60) }}
426
+ />
427
+ ```
428
+
429
+ This callback runs after each settled layout, not viewport-only changes. Hosts
430
+ that only want to fit after dataset selection should gate it with a pending flag.
431
+
432
+ ### Structured Form Bindings
433
+
434
+ Set `forms.structured: true` with `forms.state`, `forms.bindings` and
435
+ `config.formSchema` to author nested objects and arrays through flow nodes.
436
+ Use `config.edgeMode: "properties"` for their input and output handles.
437
+ This is opt-in; existing form consumers keep their current controls.
438
+
439
+ Objects expose typed scalar fields and nested configuration ports. Arrays have
440
+ ordered item nodes with add, remove and move controls. Open dictionaries expose
441
+ property-name and value-type controls. JSON and mixed-type values have typed
442
+ value nodes instead of JSON textareas. Existing resource option selectors remain
443
+ available. Missing optional values are not created until the user adds them.
444
+
445
+ Dragging an empty configuration port into space creates its value node.
446
+ Connecting a compatible value node to another field copies the value into that
447
+ field; it does not create a shared reference. Subsequent edits are independent.
448
+ Deleting an ownership edge or generated node removes the value; array deletion
449
+ compacts the remaining indices. Reordering changes index-based item identities.
450
+ All edits use `forms.onStateChange` (or `onChange`) and preserve the original
451
+ inline state/export shape. Read-only diagrams disable these mutations.
452
+
453
+ Generated nodes, ports and schemas are derived, not written into your view JSON;
454
+ their appearance is inherited rather than saved through document style callbacks.
455
+ Existing explicit bindings at the same state address are reused. Projection is
456
+ limited to 24 levels and 512 form nodes, using the SDK's existing safe dot-path
457
+ property addressing. Custom `renderForm` implementations remain host-owned.
458
+
459
+ ### Compose Your Own Controls
460
+
461
+ All reusable control components are exported from `@squeed/flow-sdk` through
462
+ [src/index.ts](src/index.ts). Hide the default toolbar with `showBottomBar={false}`
463
+ and mount your controls as children to share the diagram and form providers:
464
+
465
+ ```tsx
466
+ import {
467
+ FlowDiagram,
468
+ WorkflowControls,
469
+ DiagramAppearanceControls,
470
+ SvgExportButton,
471
+ ToolbarDivider,
472
+ } from "@squeed/flow-sdk";
473
+
474
+ <FlowDiagram
475
+ json={view}
476
+ forms={forms}
477
+ callbacks={callbacks}
478
+ showBottomBar={false}
479
+ >
480
+ <div
481
+ style={{
482
+ position: "absolute",
483
+ bottom: 20,
484
+ left: 20,
485
+ right: 20,
486
+ display: "flex",
487
+ alignItems: "center",
488
+ overflowX: "auto",
489
+ zIndex: 100,
490
+ }}
491
+ >
492
+ <WorkflowControls />
493
+ <ToolbarDivider />
494
+ <DiagramAppearanceControls />
495
+ <SvgExportButton />
496
+ </div>
497
+ </FlowDiagram>;
498
+ ```
499
+
500
+ | Category | Public exports |
501
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
502
+ | Toolbar primitives | `ToolbarIconButton`, `ToolbarTooltip`, `ToolbarDivider`, `ToolbarSelect`, `ToolbarScrollContainer`, `TooltipIconButton`, `ToolbarCommitInput`, `ToolbarCloseButton` |
503
+ | Selection controls | `NodeTools`, `EdgeTools`, `Swatches`, `NodeBorderRadiusControl`, `NodeIconPicker`, `DEFAULT_PALETTE`, `TEXT_PALETTE` |
504
+ | Control groups | `BottomBar`, `WorkflowControls`, `DiagramAppearanceControls`, `SvgExportButton` |
505
+ | Forms | `NodeForm`, `NodeFormPopover`, `NodeConfigSummary`, `FormDocumentContext` |
506
+ | Composition | `FlowDiagramProvider`, `FlowView`, `useFlowDiagramContext`, `useFlowData`, `FlowDiagramContextValue` |
507
+ | Feedback | `ErrorToast` |
508
+
509
+ Use `useFlowDiagramContext()` inside a child component for mode setters,
510
+ selection, bound edit callbacks and `engineRef`; `useFlowData()` supplies node
511
+ and edge lookups. Its maps are read-only by convention: do not mutate them.
512
+ Standalone controls outside these providers cannot access a diagram. For an
513
+ external DOM toolbar, render a React portal from a child component so context
514
+ is preserved. `SvgExportButton` needs a DOM ancestor with `data-flow-diagram`,
515
+ so use the public `exportSvg` helper when your export button is portaled outside it.
516
+
517
+ `FlowDiagramProvider` plus `FlowView` supports advanced raw-graph composition;
518
+ it does not compile JSON or create form state. Prefer `FlowDiagram` children
519
+ for document mode. Context mode setters do not override explicitly controlled
520
+ config props; the host must update those props. Selection setters change state
521
+ only; invoke selection callbacks explicitly if your custom UI requires them.
522
+ `NodeTools`/`EdgeTools` receive selected IDs and an `onClose` handler. Their
523
+ edit actions use bound callbacks, preserving document-mode updates.
524
+
525
+ The main toolbar's individual mode buttons remain part of `BottomBar`; compose
526
+ equivalent buttons with `ToolbarIconButton` and the context setters. Appearance
527
+ sliders are exposed together through `DiagramAppearanceControls`.
528
+
529
+ ### Named Ports In Custom Views
530
+
531
+ Use `FlowPort` inside a custom node component with `config.edgeMode: "auto"` or `"properties"`
532
+ and `config.edgeHandles: true`. Port IDs must be unique within a node.
533
+
534
+ ```tsx
535
+ <FlowPort id="true" role="source">True</FlowPort>
536
+ <FlowPort id="false" role="source">False</FlowPort>
537
+ <FlowPort id="input" role="target">Input</FlowPort>
538
+ ```
539
+
540
+ The engine measures each port row and supplies connection handles at the node
541
+ boundary. Outputs are on the right and inputs on the left, reversed in RL mode.
542
+ Top/bottom anchors are not supported yet. These ports do not require object
543
+ property rows or form state fields. Existing `sourceProperty`/`targetProperty`
544
+ edge fields carry the port IDs, including in `onEdgeCreate`. The host owns edge
545
+ persistence and any resulting workflow-state changes. Enable Draw mode to connect.
546
+
547
+ ### Schema-Bound Node Creation
548
+
549
+ For independent workflow state, use the `forms` prop and external node bindings;
550
+ see [Independent Workflow State](examples/workflow-state/README.md). This keeps
551
+ form values outside the Squeed document and supports state-backed `$content`.
552
+ The `$form` document binding described here remains a legacy-compatible mode.
553
+
554
+ In legacy JSON mode, a selected form-bound node has a gear that opens its
555
+ configuration in a screen-sized dialog. The form resolves `$form.$type` using
556
+ `config.formSchema` or the **Upload workflow interface** control, which also
557
+ supplies connection validation. Nested objects and
558
+ non-enum arrays are edited as validated JSON; simple fields use inferred controls.
559
+
560
+ To create a typed item when drawing from a node into empty space, declare:
561
+
562
+ ```json
563
+ {
564
+ "workflow": {
565
+ "$label": "Trigger",
566
+ "$form": {
567
+ "$type": "Workflow",
568
+ "$create": {
569
+ "$address": "root.workflow.steps",
570
+ "$type": "Step",
571
+ "$label": "Step",
572
+ "$edgeLabel": "Next",
573
+ "$defaults": { "name": "New step", "type": "conditional" }
574
+ }
575
+ },
576
+ "name": "Orders",
577
+ "trigger": "manual",
578
+ "steps": []
579
+ }
580
+ }
581
+ ```
582
+
583
+ The schema must define `Workflow.steps` as an object array and expose `Step`.
584
+ Upload discovers both binding types. Creation appends to that array, gives a
585
+ string `id` field a UUID, copies the form/creation binding, and adds a labeled
586
+ `$connections` link from the source. One `onJsonChange` reports the new node.
587
+ Required fields receive initial values; strings may remain empty and require
588
+ configuration. `$defaults` supplies explicit initial values. Further draws from
589
+ the new step append siblings to the same destination. Untyped drawing is unchanged.
590
+ The link label does not execute sequence or if/else logic; those semantics remain
591
+ the host application's responsibility.
592
+
593
+ ### SVG Download
594
+
595
+ The expanded toolbar's **Download SVG** icon exports the current graph view,
596
+ including HTML nodes, SVG edges and the background, without toolbar buttons or
597
+ drawing handles. Pan and zoom are preserved; off-screen content is not fitted
598
+ automatically. The exporter is loaded on demand.
599
+
600
+ For custom UI, call `downloadSvg(element, "diagram.svg")`, or use
601
+ `await exportSvg(element)` to obtain an SVG data URL. Pass the diagram's
602
+ `[data-flow-background]` element. The SVG embeds styled HTML via `foreignObject`:
603
+ it works in browsers but is not a pure-path SVG for every vector editor.
604
+ Remote images/fonts need accessible URLs and may be limited by CORS.
605
+
606
+ ### Edge Shapes
607
+
608
+ Set `config.edgeType` to `straight`, `bezier`, `orthogonal`, or
609
+ `rounded-orthogonal` (default). `edge.data.type` overrides it for one raw edge.
610
+ Register custom SVG geometry factories in `config.customEdgeTypes`; their keys
611
+ also appear in the selected-edge toolbar's **Edge shape** menu.
612
+
613
+ Selecting a shape calls `callbacks.onEdgeTypeChange(edgeId, type)`.
614
+ Raw-graph consumers must update that edge's `data.type`; choosing **Default**
615
+ passes `undefined` to remove the override. JSON mode persists it automatically
616
+ through `onJsonChange`: `$edgeType` for hierarchy edges, `$targetEdgeType` for
617
+ `$target` links, and `type` on the selected `$connections` entry.
618
+ The selector follows `selectionToolbar.hide: ["style"]` and read-only mode.
619
+
620
+ Custom factories receive `edge`, node bounds (`source`, `target`),
621
+ `sourcePoint`, `targetPoint`, `direction`, property row positions, `inverted`,
622
+ and `defaultGeometry`. Return `{ d, label: { x, y, angle }, arrow? }` in model
623
+ coordinates; `arrow` is an optional filled SVG path. Return `null` to use the
624
+ default route. The SDK retains selection, colors, dash animation, and labels.
625
+
126
626
  ### Node Events
127
627
 
128
- | Callback | Signature | Description |
129
- | ------------------- | -------------------------------------- | ---------------------------------------------- |
130
- | `onNodeSelect` | `(nodeId, nodeData) => void` | Node clicked/selected |
131
- | `onNodeDeselect` | `() => void` | Selection cleared |
132
- | `onNodeDoubleClick` | `(nodeId, nodeData) => void` | Node double-clicked |
133
- | `onNodeCreate` | `(params) => void` | Node created via edge drawing into empty space |
134
- | `onNodeDelete` | `(nodeId, nodeData) => void` | Node deleted |
135
- | `onNodeLabelChange` | `(nodeId, newLabel) => void` | Node label edited inline |
136
- | `onNodeCollapse` | `(nodeId, collapsed) => void` | Collapse toggle clicked |
137
- | `onNodeColorChange` | `(nodeId, colorType, color) => void` | Node color changed via toolbar |
138
- | `onNodeIconChange` | `(nodeId, iconName) => void` | Node icon changed via toolbar |
139
- | `onNodeHoverAction` | `(actionId, nodeId, nodeData) => void` | Hover overlay action clicked |
628
+ | Callback | Signature | Description |
629
+ | -------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------ |
630
+ | `onNodeSelect` | `(nodeId, nodeData) => void` | Node clicked/selected |
631
+ | `onNodeDeselect` | `() => void` | Selection cleared |
632
+ | `onNodeDoubleClick` | `(nodeId, nodeData) => void` | Node double-clicked |
633
+ | `onNodeCreate` | `(params) => void` | Node created via edge drawing into empty space |
634
+ | `onNodeDelete` | `(nodeId, nodeData) => void` | Node deleted |
635
+ | `onNodeLabelChange` | `(nodeId, newLabel) => void` | Node label edited inline |
636
+ | `onNodeContentChange` | `(nodeId, content: string) => void` | Multiline content saved; raw-graph consumers store `data.config.content` |
637
+ | `onNodeAnimationChange` | `(nodeId, animated: boolean) => void` | Border animation toggled; raw-graph consumers store `data.isNodeAnimated` |
638
+ | `onNodeCollapse` | `(nodeId, collapsed) => void` | Collapse toggle clicked |
639
+ | `onNodeColorChange` | `(nodeId, colorType, color) => void` | Node color changed via toolbar |
640
+ | `onNodeBorderColorChange` | `(nodeId, color: string) => void` | Border color picked; raw consumers update `data.viewProps.borderColor` (empty string resets) |
641
+ | `onNodeBorderStyleChange` | `(nodeId, "solid" \| "dashed") => void` | Border style changed; raw consumers update `data.viewProps.borderStyle` |
642
+ | `onNodeBorderWidthChange` | `(nodeId, width: number \| undefined) => void` | Border thickness changed; raw consumers update `data.viewProps.borderWidth` (`undefined` resets) |
643
+ | `onNodeBorderRadiusChange` | `(nodeId, radius: number \| undefined) => void` | Corner radius changed; raw consumers update `data.viewProps.borderRadius` (`undefined` resets) |
644
+ | `onNodeIconChange` | `(nodeId, iconName) => void` | Node icon changed via toolbar |
645
+ | `onNodeOutlineChange` | `(nodeId, outline: boolean) => void` | Outline toggled; raw-graph consumers store `data.viewProps.outline` |
646
+ | `onNodeHoverAction` | `(actionId, nodeId, nodeData) => void` | Hover overlay action clicked |
140
647
 
141
648
  ### Edge Events
142
649
 
@@ -165,20 +672,27 @@ Use `$collapsed: true` to collapse a JSON branch, and `false` to keep the expans
165
672
 
166
673
  Special `$`-prefixed keys control node rendering:
167
674
 
168
- | Key | Description |
169
- | ----------------------------------- | ------------------------------------------------------------------------------- |
170
- | `$label` | Display label for the node |
171
- | `$bgColor` | Background color (Chakra token, e.g. `"blue.500"`) |
172
- | `$textColor` | Text color |
173
- | `$icon` | React Icons name, e.g. `PiGear` |
174
- | `$collapsed` | `true` to collapse children, `false` to force expanded |
175
- | `$parent` | Parent node ID (creates compound/container nodes) |
176
- | `$target` | Cross-link to a JSON address, e.g. `root.database`; one per source node |
177
- | `$connections` | `SqueedConnection[]`; multiple node/property mappings, independent of `$target` |
178
- | `$sourceLabel` / `$targetLabel` | Hierarchy-edge / cross-link label |
179
- | `$edgeColor` | Edge color |
180
- | `$isEdgeDashed` / `$isEdgeAnimated` | Dashed or animated edges |
181
- | `$` | Array value (creates array child list, e.g. `{ $: ["a", "b"] }`) |
675
+ | Key | Description |
676
+ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |
677
+ | `$label` | Display label for the node |
678
+ | `$bgColor` | Background color (Chakra token, e.g. `"blue.500"`) |
679
+ | `$textColor` | Text color |
680
+ | `$borderColor` | Border color; accepts Chakra tokens, hex, or `"transparent"` |
681
+ | `$borderStyle` | Border style; the toolbar toggles `"solid"` / `"dashed"` |
682
+ | `$borderWidth` | Nonnegative border thickness in diagram pixels; `0` hides the border, removing the key restores the default |
683
+ | `$borderRadius` | Nonnegative corner radius; `0` makes square corners, removing the key restores the default |
684
+ | `$outline` | `true` for an unfilled node with a border; `false` restores its saved fill |
685
+ | `$content` | Markdown source, including an empty string; editable when `config.editable` is enabled |
686
+ | `$isNodeAnimated` | `true` for a clockwise circulating border stroke; independent of edge animation and off by default |
687
+ | `$icon` | React Icons name, e.g. `PiGear` |
688
+ | `$collapsed` | `true` to collapse children, `false` to force expanded |
689
+ | `$parent` | Parent node ID (creates compound/container nodes) |
690
+ | `$target` | Cross-link to a JSON address, e.g. `root.database`; one per source node |
691
+ | `$connections` | `SqueedConnection[]`; multiple node/property mappings, independent of `$target` |
692
+ | `$sourceLabel` / `$targetLabel` | Hierarchy-edge / cross-link label |
693
+ | `$edgeColor` | Edge color |
694
+ | `$isEdgeDashed` / `$isEdgeAnimated` | Dashed or animated edges |
695
+ | `$` | Array value (creates array child list, e.g. `{ $: ["a", "b"] }`) |
182
696
 
183
697
  ```json
184
698
  {
@@ -196,6 +710,61 @@ Special `$`-prefixed keys control node rendering:
196
710
  }
197
711
  ```
198
712
 
713
+ ### Content And Node Animation
714
+
715
+ ```json
716
+ {
717
+ "runbook": {
718
+ "$label": "Runbook",
719
+ "$content": "## Recovery\n\n1. Check queue depth\n2. Restart workers",
720
+ "$isNodeAnimated": true
721
+ }
722
+ }
723
+ ```
724
+
725
+ `$content` renders Markdown headings, lists, emphasis, tables, links, and fenced code. Supply Markdown source text, not a filename or URL to load. Raw HTML is disabled and unsafe link schemes are filtered.
726
+
727
+ With `config.editable: true`, double-click the content body to edit the original Markdown source. Enter inserts a new line; Ctrl/Cmd+Enter or clicking away saves, and Escape cancels. Source whitespace is preserved. The node grows vertically as text is added; label editors grow horizontally within their width limit. Both keep their starting size as a minimum and omit input focus outlines. Clearing the body keeps an empty, editable content view; use `setNodeProp(json, address, "$content", undefined)` to remove the content property. Content nodes use the same minimum width as their object-property rows, and the right-aligned table/article button switches views without changing the document.
728
+
729
+ The node animation toggle supports single and multiple selections. The SVG stroke follows the measured node border without changing layout or intercepting input. Text, array, object/content, and compound nodes support it, including outlined or transparent nodes. It uses the explicit border color or theme accent. Reduced-motion preferences pause the movement; graphs over 200 nodes suppress the trace in performance mode. `readOnly` prevents editing/toggling but still renders configured animation; `selectionToolbar.hide: ["animation"]` hides the toggle.
730
+
731
+ When animation is enabled, an **Animation** selector appears immediately to
732
+ the right of its bottom-toolbar icon:
733
+
734
+ - Nodes: **Trace** (the existing travelling stroke) or **Pulse** (the full border fades in and out).
735
+ - Edges: **Dash** (the existing moving dashes) or **Trace** (a travelling highlight along the routed edge).
736
+
737
+ Stopping animation hides the selector but retains the chosen variant. Node
738
+ Trace and edge Dash remain the defaults. Both respect reduced-motion preferences.
739
+
740
+ Node JSON stores `$nodeAnimationVariant: "traveling" | "pulse"` independently
741
+ of `$isNodeAnimated`. Hierarchy edges use `$edgeAnimationVariant`, target
742
+ links use `$targetAnimationVariant`, and `$connections` entries use
743
+ `animationVariant: "dashed" | "traveling"` independently of `style`.
744
+ Raw graphs use `data.nodeAnimationVariant` and `data.edgeAnimationVariant`.
745
+ Handle `onNodeAnimationVariantChange(nodeId, variant)` and
746
+ `onEdgeAnimationVariantChange(edgeId, variant)` for controlled raw-graph edits;
747
+ JSON mode persists them through `onJsonChange`.
748
+
749
+ In JSON mode content and animation edits emit `onJsonChange` with types `"nodeContent"` and `"nodeAnimation"`. In raw `nodes`/`edges` mode, handle the callbacks listed above and return updated nodes. Existing edges, colors, outlines, and mappings are unchanged.
750
+
751
+ ### Transparent Colors And Outlines
752
+
753
+ Every built-in color palette includes a checkerboard `transparent` swatch, including custom palettes. This is distinct from reset, which removes the override. Node fill, text/icon, border, edge, container, and canvas colors accept `"transparent"`. Transparent fills keep default text readable in light and dark modes; explicit transparent text also hides its icon.
754
+
755
+ The outline toggle applies to one or multiple selected nodes. It clears all node fills (including object headers/rows) without overwriting stored colors. Text, array, object, and compound nodes support it. Selection highlighting remains visible; an explicit transparent border is still respected.
756
+
757
+ The separate **Edit node border** toggle is a toolbar-only mode: it does not remove the fill or change `$outline`. It reuses the node-color swatches for border color, hides only the text-color controls, and reveals a dashed toggle and thickness input. Switching it off restores background/text-color editing. Color, style, and thickness persist independently; explicit border colors remain visible during selection and hover.
758
+
759
+ Border colors emit `nodeColor` changes, while style and thickness emit `nodeStyle` with `$borderStyle` / `$borderWidth`. Raw consumers handle the border callbacks above. Thickness accepts nonnegative numbers; clearing the input resets it, and zero hides the border and its animation. `selectionToolbar.hide: ["color"]` hides the shared palette and border mode; `"borderColor"` hides the mode toggle, and `"style"` hides dashed/thickness controls. Hiding `"outline"` affects only the separate fill-removing toggle.
760
+
761
+ ```tsx
762
+ setDoc((doc) => setNodeProp(doc, "root.service", "$outline", true));
763
+ setDoc((doc) => setNodeProp(doc, "root.service", "$bgColor", "transparent"));
764
+ ```
765
+
766
+ For raw nodes use `data.viewProps.outline` and handle `onNodeOutlineChange(nodeId, outline)`; in JSON mode the SDK emits `onJsonChange` with `type: "nodeOutline"`. Container defaults can use `config.parentNodeStyle.outline`, with per-node values taking precedence. `selectionToolbar.hide: ["outline"]` hides the toggle. `readOnly` disables edits.
767
+
199
768
  ### Property Connections
200
769
 
201
770
  Store mappings on the source node. Property references are exact row keys, while `target` is the destination node's JSON address:
@@ -230,7 +799,56 @@ Store mappings on the source node. Property references are exact row keys, while
230
799
  />
231
800
  ```
232
801
 
233
- `"workflow"` attaches the same edges to node boxes; `"properties"` attaches to rows. Switching modes does not change the document or graph connections. Edges without property references keep their ordinary node attachment. Row ports stay left/right, reversed in `RL`, even for vertical layouts.
802
+ Use **Nodes / Properties** for edge-mode controls: `"nodes"` (the default) attaches edges to node boxes; `"properties"` attaches to rows. The previous `"workflow"` value remains accepted as an alias for `"nodes"`. Switching modes does not change the document or graph connections. Edges without property references keep their ordinary node attachment. Row ports stay left/right, reversed in `RL`, even for vertical layouts.
803
+
804
+ ### Workflow Interfaces
805
+
806
+ Selected edges expose **Choose edge icon** and an **Edge icon** text field. The picker supports named React icons, SVG uploads (stored as base64), and HTTP(S) image URLs, with **Remove edge icon** to clear the choice. Icons render beside the label or by themselves for unlabeled edges; arrowheads and validation markers remain independent. Named icons follow the edge color; SVG images retain their own artwork colors. `selectionToolbar.hide: ["icon"]` hides both controls.
807
+
808
+ Raw graph hosts store `data.icon` through `callbacks.onEdgeIconChange(edgeId, icon)`; an empty string removes the icon. JSON mode persists automatically through `onJsonChange`: `$edgeIcon` on the child for hierarchy edges, `$targetIcon` on the source for `$target` cross-links, and `icon` on the selected `$connections` entry. Editing an icon never changes sibling connections.
809
+
810
+ `DynamicIcon` works in HTML and SVG contexts. Inside an SVG, use `<DynamicIcon name={icon} svg size={48} x={-24} y={-24} color="#dc2626" />`. Named React icons render as SVG; uploaded base64 SVGs and HTTP(S) image URLs render as SVG `<image>` elements with preserved aspect ratio. Without `svg`, image sources keep using HTML `<img>`. Uploaded markup is never inserted into the DOM. Edge validation markers reuse this renderer with `PiWarningCircleFill`.
811
+
812
+ The bottom toolbar includes the Nodes/Properties dropdown after Graph theme, with a divider before the theme swatches. Properties mode exposes an upload icon for `.ts` interfaces or `.json` JSON Schema. Uploads stay local and separate from graph JSON. Incompatible connections remain editable and turn red; missing declarations are amber/unverified. Hover the `!` marker for the reason. Validation compares the declared source property type to the target type, not the current property values. Node-only edges are not validated.
813
+
814
+ TypeScript contracts use a `Graph` interface or type alias keyed by node ID. A single declaration may use another name. Exact IDs are checked first; `root.alpha` also matches `alpha`. Labels are not used for matching.
815
+
816
+ ```ts
817
+ interface Graph {
818
+ alpha: { output: string; optional?: number };
819
+ beta: { input: string; count: number };
820
+ }
821
+ ```
822
+
823
+ Equivalent JSON Schema:
824
+
825
+ ```json
826
+ {
827
+ "type": "object",
828
+ "properties": {
829
+ "alpha": {
830
+ "type": "object",
831
+ "properties": {
832
+ "output": { "type": "string" },
833
+ "optional": { "type": "number" }
834
+ },
835
+ "required": ["output"]
836
+ },
837
+ "beta": {
838
+ "type": "object",
839
+ "properties": {
840
+ "input": { "type": "string" },
841
+ "count": { "type": "number" }
842
+ },
843
+ "required": ["input", "count"]
844
+ }
845
+ }
846
+ }
847
+ ```
848
+
849
+ Supported types are string, number, boolean, null, primitive literals/unions, arrays, and nested structural objects. JSON additionally supports integer and nonrecursive local `$ref`. Properties are optional unless included in JSON Schema `required`; optional sources cannot supply required targets. Extra object fields are allowed. This is a bounded structural compatibility checker, not the complete TypeScript type system or a full JSON Schema validator. Unsupported constraints (including closed objects), recursion, inheritance, generics, imports and executable code are rejected. Files are limited to 256 KB and expansion to 512 type nodes. TypeScript is parsed, never executed; its runtime dependency loads lazily on `.ts` uploads. Vite hosts may pre-optimize `typescript` to avoid a development-only reload on its first use.
850
+
851
+ For host-owned state, supply `config.edgeMode` and `config.workflowContract`, and apply `callbacks.onEdgeModeChange(mode)` and `callbacks.onWorkflowContractChange(contract)` to that state. Omit these config fields for local state. An explicitly present `workflowContract: undefined` clears a controlled contract; a failed upload retains the previous one. Read-only mode disables mode changes and uploads. Exported `parseJsonContract(source, name)`, async `parseTypeScriptContract(source, name)`, `readWorkflowContract(file)`, and `connectionIssue(contract, edgeData)` let workflow hosts reuse the same check outside the UI. `connectionIssue` returns a reason or `undefined`; it does not block workflow execution itself.
234
852
 
235
853
  Drag an output port to an input port or destination row to append a mapping. Property drags into empty space cancel. Existing node-level drawing still writes `$target`. The edge toolbar edits/deletes only the selected mapping. In document mode, store the full `onJsonChange` document; in raw `nodes`/`edges` mode, store the optional third `onEdgeCreate` argument in the edge's `data`.
236
854
 
@@ -238,6 +856,8 @@ Each entry accepts `target`, optional `sourceProperty`/`targetProperty`, `id`, `
238
856
 
239
857
  Missing or unrendered rows fall back to the corresponding node endpoint. Missing/collapsed nodes hide their edges without deleting the saved mappings. Custom views can expose rows with `data-flow-property={key}`; their positions are measured automatically. `readOnly` and `edgeHandles: false` disable port dragging.
240
858
 
859
+ Draw mode starts off by default, including when `edgeHandles: true`. Enable it with the toolbar, or explicitly control it with `config.isEdgeHandlesEnabled`. Hover handles remain available for individual connections when Draw mode is off.
860
+
241
861
  Mappings are metadata. The SDK does not execute workflows or transfer data.
242
862
 
243
863
  ## Exported Utilities
@@ -255,6 +875,25 @@ const visible = applyCollapsed(handBuiltNodes, handBuiltEdges);
255
875
 
256
876
  `createFlowDiagram` remains exported as the lower-level compiler. Use `compileSqueed` for stable edge IDs, document styling, and `$connections`. `addConnection(json, sourceAddress, connection)` appends a mapping immutably. Hierarchy edges are removed by removing the child; `$target` links and individual `$connections` can be deleted independently.
257
877
 
878
+ ### Address-Based Edits
879
+
880
+ | Helper | Result |
881
+ | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
882
+ | `getNode(json, address)` | Value at an address, or `undefined` |
883
+ | `updateNode(json, address, updater)` | New document with the updater's replacement at that address |
884
+ | `setNodeProp(json, address, key, value)` | New document with a `$` rendering property; null/undefined resets it, as does empty string except for `$content` |
885
+ | `setCollapsed(json, address, collapsed)` | New document; preserves an explicit `false` |
886
+ | `addChild(json, parentAddress, child, key?)` | `{ json, address }`; appends to plain or `$`-wrapped arrays, otherwise uses a key |
887
+ | `removeNode(json, address)` | New document without that node; root and absent paths are left unchanged |
888
+ | `addTargetEdge(json, sourceAddress, targetAddress, label?)` | New document; replaces the source's single `$target` |
889
+ | `addConnection(json, sourceAddress, connection)` | New document; appends a mapping unless its endpoints already exist |
890
+ | `setEdgeProp(json, edgeRef, key, value)` / `setEdgeStyle(json, edgeRef, style)` | New document updating only the edge's JSON owner |
891
+ | `removeEdge(json, edgeRef)` | `{ json, removed }`; independently removes cross-links or property mappings |
892
+
893
+ Addresses use dot-separated paths such as `root.backend.runtime` or `root.tags.1`; `$` array wrappers are traversed automatically. Keys containing literal dots are not escaped by this address format. Array deletion shifts subsequent indices; these addresses are not permanent IDs across structural edits.
894
+
895
+ Helpers reuse untouched branches and do not fire callbacks themselves. The `updateNode` updater must return a replacement without mutating its argument. It can add a leaf under an existing parent but does not create missing ancestor chains. Pass the returned document back through `json` to refresh the diagram.
896
+
258
897
  ## Custom Overlays
259
898
 
260
899
  ### Node Hover Overlay
@@ -338,6 +977,11 @@ const hex = getChakraColorHex("blue.500"); // → "#3182ce"
338
977
 
339
978
  ## Development Checks
340
979
 
980
+ See [CONTRIBUTING.md](https://github.com/sciphergfx/flow-sdk/blob/main/CONTRIBUTING.md)
981
+ for local setup and pull-request guidance, and
982
+ [SECURITY.md](https://github.com/sciphergfx/flow-sdk/blob/main/SECURITY.md)
983
+ for private vulnerability reporting.
984
+
341
985
  ```bash
342
986
  npm ci
343
987
  npx playwright install chromium