@statelyai/flow-react 0.4.0

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.
@@ -0,0 +1,979 @@
1
+ import { a as useFlowEntity, c as useFlowPort, d as useFlowViewport, f as FlowInstanceProvider, h as createFlowInstance, i as useFlowEdge, l as useFlowSelector, m as useOptionalFlowInstance, n as useFlow, o as useFlowMeasurement, p as useFlowInstance, r as useFlowContext, s as useFlowNode, t as useCreateFlowInstance, u as useFlowValue } from "./hooks.js";
2
+ import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { CONNECTION_PREVIEW_EDGE_ID, createConnectionPreviewGraph, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getToolbarPlacement, nodeDepths, pathToSVG, profileModesForContext, viewportCenteredOn } from "@statelyai/flow";
4
+ import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, computeRenderableIds, createMeasurementObserver, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/useFlowInput.ts
7
+ /**
8
+ * Bind a container element to the core input runtime: all pointer, wheel,
9
+ * and keyboard gestures inside the container are recognized by input modes
10
+ * and applied to the flow store. This replaces hand-written gesture handlers.
11
+ */
12
+ function useFlowInput(flow, containerRef, options = {}) {
13
+ const optionsRef = useRef(options);
14
+ optionsRef.current = options;
15
+ useEffect(() => {
16
+ const container = containerRef.current;
17
+ if (!container) return;
18
+ const { modes, profile } = optionsRef.current;
19
+ const runtime = createFlowInputRuntime({
20
+ store: flow.store,
21
+ engine: flow.engine,
22
+ modes: modes ?? (profile ? profileModesForContext(profile) : void 0)
23
+ });
24
+ return attachInput({
25
+ container,
26
+ store: flow.store,
27
+ runtime,
28
+ autoPan: (context, screenPoint) => getAutoPanDelta(context, screenPoint, flow.engine)
29
+ });
30
+ }, [flow, containerRef]);
31
+ }
32
+ //#endregion
33
+ //#region src/NodeContext.tsx
34
+ const NodeRenderContext = createContext(null);
35
+ const NodeRenderContextProvider = NodeRenderContext.Provider;
36
+ function useNodeRenderContext() {
37
+ const context = useContext(NodeRenderContext);
38
+ if (!context) throw new Error("Port must be rendered inside a StatelyFlow node.");
39
+ return context;
40
+ }
41
+ //#endregion
42
+ //#region src/StatelyFlow.tsx
43
+ function stringArraysEqual(a, b) {
44
+ if (a.length !== b.length) return false;
45
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
46
+ return true;
47
+ }
48
+ function useRenderableIds(instance, enabled) {
49
+ return useFlowSelector((context) => computeRenderableIds(instance.engine, context, enabled), { isEqual: renderableIdsEqual });
50
+ }
51
+ function useViewportTransform(ref, instance) {
52
+ useLayoutEffect(() => {
53
+ const el = ref.current;
54
+ if (!el) return;
55
+ return attachViewportTransform(instance.store, el);
56
+ }, [ref, instance]);
57
+ }
58
+ function StatelyFlow(props) {
59
+ if (props.flow) return /* @__PURE__ */ jsx(StatelyFlowView, {
60
+ ...props,
61
+ flow: props.flow
62
+ });
63
+ return /* @__PURE__ */ jsx(OwnedStatelyFlow, { ...props });
64
+ }
65
+ function OwnedStatelyFlow(props) {
66
+ const flow = useCreateFlowInstance({
67
+ graph: props.graph,
68
+ settings: props.settings,
69
+ listeners: props.listeners,
70
+ events: props.events
71
+ });
72
+ return /* @__PURE__ */ jsx(StatelyFlowView, {
73
+ ...props,
74
+ flow
75
+ });
76
+ }
77
+ function StatelyFlowView(props) {
78
+ const containerRef = useRef(null);
79
+ const viewportRef = useRef(null);
80
+ useViewportTransform(viewportRef, props.flow);
81
+ useLayoutEffect(() => {
82
+ const element = containerRef.current;
83
+ if (!element) return;
84
+ return attachContainer(props.flow.store, element);
85
+ }, [props.flow]);
86
+ useFlowInput(props.flow, containerRef, {
87
+ modes: props.inputModes,
88
+ profile: props.settings?.store?.profile
89
+ });
90
+ return /* @__PURE__ */ jsx(FlowInstanceProvider, {
91
+ flow: props.flow,
92
+ children: /* @__PURE__ */ jsxs("div", {
93
+ ref: containerRef,
94
+ className: props.className,
95
+ style: {
96
+ position: "relative",
97
+ overflow: "hidden",
98
+ width: "100%",
99
+ height: "100%",
100
+ minHeight: 320,
101
+ touchAction: "none",
102
+ ...props.style
103
+ },
104
+ children: [
105
+ props.background,
106
+ /* @__PURE__ */ jsxs("div", {
107
+ ref: viewportRef,
108
+ "data-flow-viewport": true,
109
+ style: viewportStyle(),
110
+ children: [
111
+ /* @__PURE__ */ jsx(EdgeLayer, {
112
+ renderEdge: props.renderEdge,
113
+ onlyRenderVisible: props.onlyRenderVisible
114
+ }),
115
+ /* @__PURE__ */ jsx(NodeLayer, {
116
+ renderNode: props.renderNode,
117
+ onlyRenderVisible: props.onlyRenderVisible
118
+ }),
119
+ /* @__PURE__ */ jsx(SnapLinesLayer, {}),
120
+ /* @__PURE__ */ jsx(EdgeLabelLayer, { onlyRenderVisible: props.onlyRenderVisible })
121
+ ]
122
+ }),
123
+ /* @__PURE__ */ jsx("div", {
124
+ "data-flow-overlay": true,
125
+ children: props.children
126
+ })
127
+ ]
128
+ })
129
+ });
130
+ }
131
+ function SnapLinesLayer() {
132
+ const instance = useFlowInstance();
133
+ const snapResult = useFlowSelector(() => instance.engine.getSnapResult());
134
+ const viewport = useFlowSelector((context) => context.viewport);
135
+ const viewportSize = useFlowSelector((context) => context.viewportSize);
136
+ if (!snapResult) return null;
137
+ const visibleBounds = {
138
+ x: -viewport.x / viewport.zoom,
139
+ y: -viewport.y / viewport.zoom,
140
+ width: viewportSize.width / viewport.zoom,
141
+ height: viewportSize.height / viewport.zoom
142
+ };
143
+ const strokeWidth = 1.5 / viewport.zoom;
144
+ const capSize = 4 / viewport.zoom;
145
+ return /* @__PURE__ */ jsx("svg", {
146
+ "aria-hidden": "true",
147
+ "data-flow-canvas-layer": true,
148
+ style: {
149
+ position: "absolute",
150
+ inset: 0,
151
+ width: "100%",
152
+ height: "100%",
153
+ overflow: "visible",
154
+ pointerEvents: "none"
155
+ },
156
+ children: /* @__PURE__ */ jsxs("g", { children: [snapResult.snapLines.map((line, index) => line.axis === "x" ? /* @__PURE__ */ jsx("line", {
157
+ x1: line.value,
158
+ y1: visibleBounds.y,
159
+ x2: line.value,
160
+ y2: visibleBounds.y + visibleBounds.height,
161
+ stroke: "#ef4444",
162
+ strokeWidth,
163
+ strokeDasharray: `${6 / viewport.zoom} ${4 / viewport.zoom}`
164
+ }, `snap-x-${index}`) : /* @__PURE__ */ jsx("line", {
165
+ x1: visibleBounds.x,
166
+ y1: line.value,
167
+ x2: visibleBounds.x + visibleBounds.width,
168
+ y2: line.value,
169
+ stroke: "#ef4444",
170
+ strokeWidth,
171
+ strokeDasharray: `${6 / viewport.zoom} ${4 / viewport.zoom}`
172
+ }, `snap-y-${index}`)), snapResult.spacingGuides?.flatMap((guide, guideIndex) => guide.ranges.flatMap((range, rangeIndex) => {
173
+ const key = `spacing-${guideIndex}-${rangeIndex}`;
174
+ if (guide.axis === "x") return [
175
+ /* @__PURE__ */ jsx("line", {
176
+ x1: range.start,
177
+ y1: guide.position,
178
+ x2: range.end,
179
+ y2: guide.position,
180
+ stroke: "#ef4444",
181
+ strokeWidth
182
+ }, `${key}-line`),
183
+ /* @__PURE__ */ jsx("line", {
184
+ x1: range.start,
185
+ y1: guide.position - capSize,
186
+ x2: range.start,
187
+ y2: guide.position + capSize,
188
+ stroke: "#ef4444",
189
+ strokeWidth,
190
+ strokeLinecap: "round"
191
+ }, `${key}-start`),
192
+ /* @__PURE__ */ jsx("line", {
193
+ x1: range.end,
194
+ y1: guide.position - capSize,
195
+ x2: range.end,
196
+ y2: guide.position + capSize,
197
+ stroke: "#ef4444",
198
+ strokeWidth,
199
+ strokeLinecap: "round"
200
+ }, `${key}-end`)
201
+ ];
202
+ return [
203
+ /* @__PURE__ */ jsx("line", {
204
+ x1: guide.position,
205
+ y1: range.start,
206
+ x2: guide.position,
207
+ y2: range.end,
208
+ stroke: "#ef4444",
209
+ strokeWidth
210
+ }, `${key}-line`),
211
+ /* @__PURE__ */ jsx("line", {
212
+ x1: guide.position - capSize,
213
+ y1: range.start,
214
+ x2: guide.position + capSize,
215
+ y2: range.start,
216
+ stroke: "#ef4444",
217
+ strokeWidth,
218
+ strokeLinecap: "round"
219
+ }, `${key}-start`),
220
+ /* @__PURE__ */ jsx("line", {
221
+ x1: guide.position - capSize,
222
+ y1: range.end,
223
+ x2: guide.position + capSize,
224
+ y2: range.end,
225
+ stroke: "#ef4444",
226
+ strokeWidth,
227
+ strokeLinecap: "round"
228
+ }, `${key}-end`)
229
+ ];
230
+ }))] })
231
+ });
232
+ }
233
+ function EdgeLayer(props) {
234
+ const instance = useFlowInstance();
235
+ const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
236
+ const edgeIds = useFlowSelector((context) => context.graph.edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
237
+ const renderEdge = props.renderEdge;
238
+ return /* @__PURE__ */ jsx("svg", {
239
+ "aria-label": "edges",
240
+ "data-flow-canvas-layer": true,
241
+ style: {
242
+ position: "absolute",
243
+ inset: 0,
244
+ width: "100%",
245
+ height: "100%",
246
+ overflow: "visible",
247
+ pointerEvents: "auto"
248
+ },
249
+ children: /* @__PURE__ */ jsxs("g", { children: [edgeIds.map((id) => {
250
+ if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
251
+ return /* @__PURE__ */ jsx(EdgeView, {
252
+ flow: instance,
253
+ id,
254
+ renderEdge
255
+ }, id);
256
+ }), /* @__PURE__ */ jsx(ConnectionPreviewEdge, {
257
+ flow: instance,
258
+ renderEdge
259
+ })] })
260
+ });
261
+ }
262
+ const EdgeView = memo(function EdgeView(props) {
263
+ const { edge, pathData, selected, hovered, highlights } = useFlowEntity(props.id, (context) => ({
264
+ edge: props.flow.getEdge(props.id) ?? null,
265
+ pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
266
+ selected: context.selection.has(props.id),
267
+ hovered: context.hoveredEntityIds.has(props.id),
268
+ highlights: props.flow.engine.getHighlights(props.id)
269
+ }), { isEqual: (a, b) => a.edge === b.edge && a.pathData === b.pathData && a.selected === b.selected && a.hovered === b.hovered && a.highlights === b.highlights });
270
+ if (!edge || !pathData) return null;
271
+ if (props.renderEdge) return /* @__PURE__ */ jsxs("g", {
272
+ "data-entity-id": props.id,
273
+ "data-flow-selected": selected ? "true" : void 0,
274
+ style: { pointerEvents: "auto" },
275
+ children: [/* @__PURE__ */ jsx(EdgeHitTarget, {
276
+ flow: props.flow,
277
+ edge,
278
+ pathData,
279
+ selected
280
+ }), props.renderEdge({
281
+ flow: props.flow,
282
+ edge,
283
+ pathData,
284
+ selected,
285
+ hovered,
286
+ highlights
287
+ })]
288
+ });
289
+ return /* @__PURE__ */ jsx("g", {
290
+ "data-entity-id": props.id,
291
+ "data-flow-selected": selected ? "true" : void 0,
292
+ style: { pointerEvents: "auto" },
293
+ children: /* @__PURE__ */ jsx(DefaultEdge, {
294
+ flow: props.flow,
295
+ edge,
296
+ pathData,
297
+ selected
298
+ })
299
+ });
300
+ });
301
+ /** The in-progress connection gesture's preview edge (renders only mid-gesture). */
302
+ function ConnectionPreviewEdge(props) {
303
+ const connection = useFlowSelector((context) => context.connection);
304
+ if (!connection?.active) return null;
305
+ const previewGraph = createConnectionPreviewGraph(props.flow.getSnapshot().graph, connection);
306
+ const pathData = props.flow.getAllEdgePaths({ graph: previewGraph }).get(CONNECTION_PREVIEW_EDGE_ID);
307
+ const edge = previewGraph.edges.find((candidate) => candidate.id === CONNECTION_PREVIEW_EDGE_ID);
308
+ if (!edge || !pathData) return null;
309
+ return /* @__PURE__ */ jsx("g", {
310
+ style: { pointerEvents: "none" },
311
+ children: props.renderEdge ? props.renderEdge({
312
+ flow: props.flow,
313
+ edge,
314
+ pathData,
315
+ selected: false,
316
+ hovered: false,
317
+ highlights: []
318
+ }) : /* @__PURE__ */ jsx(DefaultEdge, {
319
+ flow: props.flow,
320
+ edge,
321
+ pathData,
322
+ selected: false,
323
+ preview: true
324
+ })
325
+ });
326
+ }
327
+ function DefaultEdge(props) {
328
+ return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
329
+ const d = pathToSVG(segment);
330
+ return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("path", {
331
+ d,
332
+ fill: "none",
333
+ stroke: props.preview ? props.edge.data?.isValid === false ? "#dc2626" : props.edge.data?.isValid === true ? "#16a34a" : "#0f766e" : props.selected ? "#2563eb" : "#94a3b8",
334
+ strokeDasharray: props.preview ? "6 6" : void 0,
335
+ strokeWidth: props.selected ? 3 : 2
336
+ }), props.preview ? null : /* @__PURE__ */ jsx("path", {
337
+ d,
338
+ "data-entity-id": props.edge.id,
339
+ fill: "none",
340
+ stroke: "#000",
341
+ strokeOpacity: 0,
342
+ strokeWidth: 18,
343
+ pointerEvents: "stroke"
344
+ })] }, index);
345
+ }) });
346
+ }
347
+ function EdgeHitTarget(props) {
348
+ return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
349
+ return /* @__PURE__ */ jsx("path", {
350
+ d: pathToSVG(segment),
351
+ "data-entity-id": props.edge.id,
352
+ fill: "none",
353
+ stroke: "#000",
354
+ strokeOpacity: 0,
355
+ strokeWidth: 18,
356
+ pointerEvents: "stroke"
357
+ }, index);
358
+ }) });
359
+ }
360
+ function EdgeLabelLayer(props) {
361
+ const instance = useFlowInstance();
362
+ const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
363
+ return /* @__PURE__ */ jsx("div", {
364
+ "aria-label": "edge labels",
365
+ style: {
366
+ position: "absolute",
367
+ inset: 0,
368
+ pointerEvents: "none"
369
+ },
370
+ children: useFlowSelector((context) => context.graph.edges.filter((edge) => edge.label).map((edge) => edge.id), { isEqual: stringArraysEqual }).map((id) => {
371
+ if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
372
+ return /* @__PURE__ */ jsx(EdgeLabelItem, {
373
+ flow: instance,
374
+ id
375
+ }, id);
376
+ })
377
+ });
378
+ }
379
+ const EdgeLabelItem = memo(function EdgeLabelItem(props) {
380
+ const { edge, pathData, selected } = useFlowEntity(props.id, (context) => ({
381
+ edge: props.flow.getEdge(props.id) ?? null,
382
+ pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
383
+ selected: context.selection.has(props.id)
384
+ }), { isEqual: (a, b) => a.edge === b.edge && a.pathData === b.pathData && a.selected === b.selected });
385
+ if (!edge?.label || !pathData?.labelBounds) return null;
386
+ return /* @__PURE__ */ jsx(EdgeLabelView, {
387
+ flow: props.flow,
388
+ edge,
389
+ bounds: pathData.labelBounds,
390
+ selected
391
+ });
392
+ });
393
+ function EdgeLabelView(props) {
394
+ const ref = useRef(null);
395
+ const observer = useMemo(() => createMeasurementObserver(props.flow.store), [props.flow.store]);
396
+ useLayoutEffect(() => {
397
+ const element = ref.current;
398
+ if (!element) return;
399
+ observer.observe(element, { edgeId: props.edge.id });
400
+ return () => observer.disconnect();
401
+ }, [observer, props.edge.id]);
402
+ const text = (props.edge.label?.data)?.text;
403
+ return /* @__PURE__ */ jsx("div", {
404
+ ref,
405
+ "data-entity-id": props.edge.id,
406
+ "data-flow-edge-label": "",
407
+ style: {
408
+ position: "absolute",
409
+ left: props.bounds.x,
410
+ top: props.bounds.y,
411
+ width: props.bounds.width,
412
+ height: props.bounds.height,
413
+ boxSizing: "border-box",
414
+ padding: "4px 8px",
415
+ border: `1px solid ${props.selected ? "#2563eb" : "#cbd5e1"}`,
416
+ borderRadius: 999,
417
+ background: "#fff",
418
+ color: "#334155",
419
+ display: "grid",
420
+ placeItems: "center",
421
+ cursor: "grab",
422
+ font: "600 12px/1 system-ui, sans-serif",
423
+ pointerEvents: "auto",
424
+ userSelect: "none"
425
+ },
426
+ children: String(text ?? props.edge.id)
427
+ });
428
+ }
429
+ function NodeLayer(props) {
430
+ const instance = useFlowInstance();
431
+ const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
432
+ const nodeIds = useFlowSelector((context) => {
433
+ const depths = nodeDepths(context.graph.nodes);
434
+ return context.graph.nodes.map((node) => node.id).sort((a, b) => (depths.get(a) ?? 0) - (depths.get(b) ?? 0));
435
+ }, { isEqual: stringArraysEqual });
436
+ const observer = useMemo(() => createMeasurementObserver(instance.store), [instance]);
437
+ useLayoutEffect(() => () => observer.disconnect(), [observer]);
438
+ return /* @__PURE__ */ jsx("div", {
439
+ "aria-label": "nodes",
440
+ "data-flow-canvas-layer": true,
441
+ style: {
442
+ position: "absolute",
443
+ inset: 0,
444
+ transformOrigin: "0 0",
445
+ pointerEvents: "none"
446
+ },
447
+ children: nodeIds.map((id) => {
448
+ if (renderableIds && !renderableIds.nodeIds.has(id)) return null;
449
+ return /* @__PURE__ */ jsx(NodeView, {
450
+ flow: instance,
451
+ id,
452
+ observer,
453
+ renderNode: props.renderNode
454
+ }, id);
455
+ })
456
+ });
457
+ }
458
+ function rectsShallowEqual(a, b) {
459
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
460
+ }
461
+ const NodeView = memo(function NodeView(props) {
462
+ const ref = useRef(null);
463
+ const { node, hidden, selected, dragging, focused, hovered, highlights, bounds } = useFlowEntity(props.id, (context) => {
464
+ const current = props.flow.getNode(props.id) ?? null;
465
+ return {
466
+ node: current,
467
+ hidden: !current || props.flow.engine.resolveEntityConfig(current).hidden,
468
+ selected: context.selection.has(props.id),
469
+ dragging: context.draggedEntityId === props.id,
470
+ focused: context.focusedEntityId === props.id,
471
+ hovered: context.hoveredEntityIds.has(props.id),
472
+ highlights: props.flow.engine.getHighlights(props.id),
473
+ bounds: context.resizingEntityId === props.id && context.resizeCorners ? props.flow.engine.getResizeBounds() ?? props.flow.engine.resolvedBounds(props.id) : props.flow.engine.resolvedBounds(props.id)
474
+ };
475
+ }, { isEqual: (a, b) => a.node === b.node && a.hidden === b.hidden && a.selected === b.selected && a.dragging === b.dragging && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights && rectsShallowEqual(a.bounds, b.bounds) });
476
+ useLayoutEffect(() => {
477
+ const element = ref.current;
478
+ if (!element) return;
479
+ const target = props.flow.getNode(props.id)?.group?.type === "derived" ? element.querySelector("[data-flow-group-content-measure]") : element;
480
+ if (!target) return;
481
+ props.observer.observe(target, { nodeId: props.id });
482
+ return () => props.observer.unobserve(target);
483
+ }, [
484
+ props.id,
485
+ props.observer,
486
+ props.flow,
487
+ hidden
488
+ ]);
489
+ if (!node || hidden) return null;
490
+ return /* @__PURE__ */ jsx(NodeRenderContextProvider, {
491
+ value: {
492
+ node,
493
+ bounds
494
+ },
495
+ children: /* @__PURE__ */ jsx("div", {
496
+ ref,
497
+ "data-entity-id": props.id,
498
+ "data-flow-selected": selected ? "true" : void 0,
499
+ role: "group",
500
+ "aria-roledescription": "flow node",
501
+ "aria-label": props.flow.engine.getAriaLabel(props.id) + (selected ? ", selected" : ""),
502
+ tabIndex: 0,
503
+ onFocus: () => props.flow.trigger.focus({ entityId: props.id }),
504
+ style: {
505
+ ...nodePositionStyle(bounds, {
506
+ autoSize: node.autoSize,
507
+ dragging
508
+ }),
509
+ cursor: dragging ? "grabbing" : "grab",
510
+ pointerEvents: "auto",
511
+ userSelect: "none",
512
+ outline: focused ? "2px solid #2563eb" : "none",
513
+ outlineOffset: 2
514
+ },
515
+ children: props.renderNode ? props.renderNode({
516
+ flow: props.flow,
517
+ node,
518
+ bounds,
519
+ selected,
520
+ dragging,
521
+ hovered,
522
+ highlights
523
+ }) : node.group ? /* @__PURE__ */ jsx(DefaultGroupNode, {
524
+ flow: props.flow,
525
+ node,
526
+ bounds,
527
+ selected,
528
+ dragging,
529
+ hovered,
530
+ highlights
531
+ }) : /* @__PURE__ */ jsx(DefaultNode, {
532
+ flow: props.flow,
533
+ node,
534
+ bounds,
535
+ selected,
536
+ dragging,
537
+ hovered,
538
+ highlights
539
+ })
540
+ })
541
+ });
542
+ });
543
+ /**
544
+ * Default group rendering: content block (the drag handle) on its configured
545
+ * side plus a children-area backdrop. The geometry comes entirely from
546
+ * `engine.getGroupGeometry`; custom renderers can do the same.
547
+ */
548
+ function DefaultGroupNode(props) {
549
+ const geometry = props.flow.engine.getGroupGeometry(props.node.id);
550
+ const label = props.node.data?.label;
551
+ if (!geometry) return /* @__PURE__ */ jsx(DefaultNode, { ...props });
552
+ const relative = (rect) => ({
553
+ left: rect.x - props.bounds.x,
554
+ top: rect.y - props.bounds.y,
555
+ width: rect.width,
556
+ height: rect.height
557
+ });
558
+ const dropTarget = props.highlights.some((entry) => entry.kind === "drop-target");
559
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", { style: {
560
+ position: "absolute",
561
+ ...relative(geometry.childArea),
562
+ boxSizing: "border-box",
563
+ border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : "#cbd5e1"}`,
564
+ ...dropTarget ? { background: "rgba(22, 163, 74, 0.08)" } : null,
565
+ borderRadius: 10,
566
+ background: "rgba(148, 163, 184, 0.07)",
567
+ pointerEvents: "none"
568
+ } }), /* @__PURE__ */ jsx("div", {
569
+ "data-flow-drag-handle": true,
570
+ style: {
571
+ position: "absolute",
572
+ ...relative(geometry.contentBounds),
573
+ boxSizing: "border-box",
574
+ display: "flex",
575
+ alignItems: "center",
576
+ padding: "0 12px",
577
+ border: `2px solid ${props.selected ? "#2563eb" : "#94a3b8"}`,
578
+ borderRadius: 8,
579
+ background: props.selected ? "#eff6ff" : "#f8fafc",
580
+ color: "#0f172a",
581
+ font: "600 13px/1.2 system-ui, sans-serif",
582
+ cursor: props.dragging ? "grabbing" : "grab",
583
+ userSelect: "none"
584
+ },
585
+ children: String(label ?? props.node.id)
586
+ })] });
587
+ }
588
+ function DefaultNode(props) {
589
+ const label = props.node.data?.label;
590
+ return /* @__PURE__ */ jsx("div", {
591
+ style: {
592
+ display: "grid",
593
+ placeItems: "center",
594
+ width: "100%",
595
+ height: "100%",
596
+ boxSizing: "border-box",
597
+ padding: "10px 14px",
598
+ border: `2px solid ${props.selected ? "#2563eb" : props.hovered ? "#94a3b8" : "#cbd5e1"}`,
599
+ borderRadius: 8,
600
+ background: props.selected ? "#eff6ff" : "#ffffff",
601
+ color: "#0f172a",
602
+ font: "600 14px/1.2 system-ui, sans-serif",
603
+ boxShadow: props.selected ? "0 0 0 4px rgba(37, 99, 235, 0.16)" : "0 1px 2px rgba(15, 23, 42, 0.1)"
604
+ },
605
+ children: String(label ?? props.node.id)
606
+ });
607
+ }
608
+ //#endregion
609
+ //#region src/FlowToolbar.tsx
610
+ /** Border-box size of an element, kept current via ResizeObserver. */
611
+ function useMeasuredSize(deps = []) {
612
+ const [size, setSize] = useState({
613
+ width: 0,
614
+ height: 0
615
+ });
616
+ const ref = useRef(null);
617
+ const measure = useCallback((node) => {
618
+ const rect = node.getBoundingClientRect();
619
+ setSize((prev) => prev.width === rect.width && prev.height === rect.height ? prev : {
620
+ width: rect.width,
621
+ height: rect.height
622
+ });
623
+ }, []);
624
+ const setRef = useCallback((node) => {
625
+ ref.current = node;
626
+ if (node) measure(node);
627
+ }, [measure]);
628
+ useLayoutEffect(() => {
629
+ const node = ref.current;
630
+ if (!node) return;
631
+ measure(node);
632
+ const observer = new ResizeObserver(() => measure(node));
633
+ observer.observe(node);
634
+ return () => observer.disconnect();
635
+ }, [measure, ...deps]);
636
+ return [setRef, size];
637
+ }
638
+ function rectsEqual(a, b) {
639
+ return a === b || a !== null && b !== null && a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
640
+ }
641
+ function useFlowToolbar(options = {}) {
642
+ const flow = useFlowInstance();
643
+ const idsKey = options.ids?.join(" ");
644
+ const anchor = useFlowSelector((context) => {
645
+ const ids = options.ids ?? [...context.selection];
646
+ if (ids.length === 0) return null;
647
+ const canvasRect = flow.engine.getToolbarAnchor(ids);
648
+ if (!canvasRect) return null;
649
+ const zoom = context.viewport.zoom;
650
+ return {
651
+ x: canvasRect.x * zoom + context.viewport.x,
652
+ y: canvasRect.y * zoom + context.viewport.y,
653
+ width: canvasRect.width * zoom,
654
+ height: canvasRect.height * zoom
655
+ };
656
+ }, {
657
+ flow,
658
+ isEqual: rectsEqual
659
+ });
660
+ const viewportSize = useFlowSelector((context) => context.viewportSize, { flow });
661
+ const [setRef, size] = useMeasuredSize([!!anchor, idsKey]);
662
+ if (!anchor) return {
663
+ setRef,
664
+ style: { display: "none" },
665
+ visible: false,
666
+ anchor: null,
667
+ side: "top"
668
+ };
669
+ const placement = getToolbarPlacement({
670
+ anchor,
671
+ toolbarWidth: size.width,
672
+ toolbarHeight: size.height,
673
+ viewportWidth: viewportSize.width,
674
+ viewportHeight: viewportSize.height,
675
+ offset: options.offset,
676
+ padding: options.padding
677
+ });
678
+ return {
679
+ setRef,
680
+ style: {
681
+ position: "absolute",
682
+ left: placement.left,
683
+ top: placement.top,
684
+ visibility: size.width > 0 ? "visible" : "hidden",
685
+ pointerEvents: "auto"
686
+ },
687
+ visible: true,
688
+ anchor,
689
+ side: placement.side
690
+ };
691
+ }
692
+ /**
693
+ * Unstyled positioned toolbar anchored to the selection (or explicit `ids`).
694
+ * Marked `data-flow-interactive`, so its contents receive pointer events
695
+ * unopposed by canvas gestures.
696
+ */
697
+ function FlowToolbar(props) {
698
+ const { children, className, style, ...options } = props;
699
+ const toolbar = useFlowToolbar(options);
700
+ if (!toolbar.visible) return null;
701
+ return /* @__PURE__ */ jsx("div", {
702
+ ref: toolbar.setRef,
703
+ "data-flow-interactive": true,
704
+ className,
705
+ style: {
706
+ ...toolbar.style,
707
+ ...style
708
+ },
709
+ children
710
+ });
711
+ }
712
+ //#endregion
713
+ //#region src/MiniMap.tsx
714
+ /**
715
+ * Overview map. Deliberately thin: all math lives in core
716
+ * (`getMinimapProjection`, `viewportCenteredOn`) and every interaction is a
717
+ * plain store trigger — this component is exactly what you would write in
718
+ * userland, kept here as a convenience.
719
+ */
720
+ function MiniMap(props) {
721
+ const flow = useFlowInstance();
722
+ const width = props.width ?? 200;
723
+ const height = props.height ?? 140;
724
+ const nodes = useFlowSelector((context) => context.graph.nodes);
725
+ const viewport = useFlowSelector((context) => context.viewport);
726
+ const viewportSize = useFlowSelector((context) => context.viewportSize);
727
+ const draggingRef = useRef(false);
728
+ const projection = getMinimapProjection({
729
+ contentBounds: flow.getGraphBounds(),
730
+ viewport,
731
+ viewportSize,
732
+ width,
733
+ height,
734
+ padding: props.padding
735
+ });
736
+ const jumpTo = (event) => {
737
+ const rect = event.currentTarget.getBoundingClientRect();
738
+ const canvasPoint = projection.toCanvas({
739
+ x: event.clientX - rect.left,
740
+ y: event.clientY - rect.top
741
+ });
742
+ flow.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
743
+ };
744
+ return /* @__PURE__ */ jsxs("svg", {
745
+ "aria-label": "minimap",
746
+ width,
747
+ height,
748
+ className: props.className,
749
+ onPointerDown: (event) => {
750
+ draggingRef.current = true;
751
+ jumpTo(event);
752
+ try {
753
+ event.currentTarget.setPointerCapture(event.pointerId);
754
+ } catch {}
755
+ },
756
+ onPointerMove: (event) => {
757
+ if (draggingRef.current) jumpTo(event);
758
+ },
759
+ onPointerUp: (event) => {
760
+ draggingRef.current = false;
761
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
762
+ },
763
+ style: {
764
+ position: "absolute",
765
+ right: 12,
766
+ bottom: 12,
767
+ background: "rgba(248, 250, 252, 0.9)",
768
+ border: "1px solid #e2e8f0",
769
+ borderRadius: 8,
770
+ boxShadow: "0 2px 10px rgba(15, 23, 42, 0.12)",
771
+ cursor: "pointer",
772
+ pointerEvents: "auto",
773
+ ...props.style
774
+ },
775
+ children: [nodes.map((node) => {
776
+ const rect = projection.rectToMinimap(flow.engine.resolvedBounds(node.id));
777
+ return props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
778
+ x: rect.x,
779
+ y: rect.y,
780
+ width: Math.max(rect.width, 1),
781
+ height: Math.max(rect.height, 1),
782
+ rx: 1,
783
+ fill: "#94a3b8"
784
+ }, node.id);
785
+ }), /* @__PURE__ */ jsx("rect", {
786
+ x: projection.viewportRect.x,
787
+ y: projection.viewportRect.y,
788
+ width: projection.viewportRect.width,
789
+ height: projection.viewportRect.height,
790
+ fill: "rgba(37, 99, 235, 0.08)",
791
+ stroke: "#2563eb",
792
+ strokeWidth: 1
793
+ })]
794
+ });
795
+ }
796
+ //#endregion
797
+ //#region src/Controls.tsx
798
+ const buttonStyle = {
799
+ width: 28,
800
+ height: 28,
801
+ display: "grid",
802
+ placeItems: "center",
803
+ border: "1px solid #e2e8f0",
804
+ borderRadius: 6,
805
+ background: "#ffffff",
806
+ color: "#0f172a",
807
+ font: "600 14px/1 system-ui, sans-serif",
808
+ cursor: "pointer"
809
+ };
810
+ /**
811
+ * Zoom/fit toolbar. Deliberately thin: every button is one store trigger
812
+ * (`zoomBy`, `fitView`) — replicate or extend it freely in userland.
813
+ */
814
+ function Controls(props) {
815
+ const flow = useFlowInstance();
816
+ return /* @__PURE__ */ jsxs("div", {
817
+ role: "toolbar",
818
+ "aria-label": "canvas controls",
819
+ className: props.className,
820
+ style: {
821
+ position: "absolute",
822
+ left: 12,
823
+ bottom: 12,
824
+ display: "flex",
825
+ flexDirection: "column",
826
+ gap: 4,
827
+ pointerEvents: "auto",
828
+ ...props.style
829
+ },
830
+ children: [
831
+ /* @__PURE__ */ jsx("button", {
832
+ type: "button",
833
+ "aria-label": "zoom in",
834
+ style: buttonStyle,
835
+ onClick: () => flow.trigger.zoomBy({ factor: 1.2 }),
836
+ children: "+"
837
+ }),
838
+ /* @__PURE__ */ jsx("button", {
839
+ type: "button",
840
+ "aria-label": "zoom out",
841
+ style: buttonStyle,
842
+ onClick: () => flow.trigger.zoomBy({ factor: 1 / 1.2 }),
843
+ children: "−"
844
+ }),
845
+ /* @__PURE__ */ jsx("button", {
846
+ type: "button",
847
+ "aria-label": "fit view",
848
+ style: buttonStyle,
849
+ onClick: () => flow.trigger.fitView({ padding: .1 }),
850
+ children: "⛶"
851
+ }),
852
+ props.children
853
+ ]
854
+ });
855
+ }
856
+ //#endregion
857
+ //#region src/EntityAnchor.tsx
858
+ function EntityAnchor(props) {
859
+ const viewport = useFlowSelector((context) => context.viewport);
860
+ const transform = getAnchorTransform(props.bounds, viewport, props.anchor);
861
+ return /* @__PURE__ */ jsx("div", {
862
+ className: props.className,
863
+ style: {
864
+ position: "absolute",
865
+ left: transform.x,
866
+ top: transform.y,
867
+ transform: props.inverseScale ? `translate(-50%, -50%) scale(${transform.scale})` : "translate(-50%, -50%)",
868
+ transformOrigin: "center",
869
+ pointerEvents: "auto",
870
+ ...props.style
871
+ },
872
+ children: props.children
873
+ });
874
+ }
875
+ //#endregion
876
+ //#region src/Port.tsx
877
+ /**
878
+ * A connection port. Purely declarative: it renders the
879
+ * `data-flow-port-name`/`data-flow-node-id` attributes that the input
880
+ * runtime's `connectMode` recognizes, and reports its measured bounds to the
881
+ * store. The connection gesture itself (start/move/validate/end) lives in
882
+ * core — customize it by swapping `connectMode({ isValid })` in the
883
+ * `inputModes` prop of `StatelyFlow` (or your own `useFlowInput` call), and
884
+ * observe results via `flow.on("connectionEnded", …)` or the store's
885
+ * `connection` state.
886
+ */
887
+ function Port(props) {
888
+ const flow = useFlowInstance();
889
+ const { node } = useNodeRenderContext();
890
+ const ref = useRef(null);
891
+ const observerRef = useRef(null);
892
+ const measured = props.measured ?? true;
893
+ useLayoutEffect(() => {
894
+ const element = ref.current;
895
+ if (!element || !measured) return;
896
+ const observer = createMeasurementObserver(flow.store);
897
+ observerRef.current = observer;
898
+ observer.observe(element, {
899
+ nodeId: node.id,
900
+ portName: props.name
901
+ });
902
+ return () => {
903
+ observerRef.current = null;
904
+ observer.disconnect();
905
+ };
906
+ }, [
907
+ flow.store,
908
+ node.id,
909
+ props.name,
910
+ measured
911
+ ]);
912
+ useLayoutEffect(() => {
913
+ const element = ref.current;
914
+ if (!element) return;
915
+ if (flow.getSnapshot().canvasState !== "idle") return;
916
+ observerRef.current?.measure(element);
917
+ });
918
+ const candidate = useFlowSelector((context) => {
919
+ const connection = context.connection;
920
+ if (!connection?.active) return null;
921
+ if (connection.targetId !== node.id || connection.targetPort !== props.name) return null;
922
+ return connection.isValid === false ? "invalid" : "valid";
923
+ }, { flow });
924
+ return /* @__PURE__ */ jsx("div", {
925
+ ref,
926
+ className: props.className,
927
+ "data-flow-port-name": props.name,
928
+ "data-flow-node-id": node.id,
929
+ "data-connection-candidate": candidate ?? void 0,
930
+ style: props.style,
931
+ children: props.children
932
+ });
933
+ }
934
+ //#endregion
935
+ //#region src/ColorMode.tsx
936
+ function FlowColorModeScope(props) {
937
+ const { colorMode, resolvedColorMode } = useFlowColorMode();
938
+ return /* @__PURE__ */ jsx("div", {
939
+ className: props.className,
940
+ "data-flow-color-mode": resolvedColorMode,
941
+ "data-flow-color-mode-preference": colorMode,
942
+ style: {
943
+ colorScheme: resolvedColorMode,
944
+ ...props.style
945
+ },
946
+ children: props.children
947
+ });
948
+ }
949
+ function useFlowColorMode() {
950
+ const colorMode = useFlowSelector((context) => context.colorMode);
951
+ const [systemColorMode, setSystemColorMode] = useState(() => resolveFlowColorMode("system"));
952
+ useEffect(() => {
953
+ if (typeof window === "undefined" || !window.matchMedia) return;
954
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
955
+ function update() {
956
+ setSystemColorMode(resolveFlowColorMode("system"));
957
+ }
958
+ update();
959
+ media.addEventListener("change", update);
960
+ return () => media.removeEventListener("change", update);
961
+ }, []);
962
+ return useMemo(() => ({
963
+ colorMode,
964
+ resolvedColorMode: colorMode === "system" ? systemColorMode : colorMode
965
+ }), [colorMode, systemColorMode]);
966
+ }
967
+ function useApplyFlowColorMode(element) {
968
+ const { colorMode, resolvedColorMode } = useFlowColorMode();
969
+ useEffect(() => {
970
+ if (!element) return;
971
+ applyFlowColorMode(element, colorMode);
972
+ }, [
973
+ colorMode,
974
+ element,
975
+ resolvedColorMode
976
+ ]);
977
+ }
978
+ //#endregion
979
+ export { Controls, EntityAnchor, FlowColorModeScope, FlowInstanceProvider, FlowToolbar, MiniMap, Port, StatelyFlow, createFlowInstance, useApplyFlowColorMode, useCreateFlowInstance, useFlow, useFlowColorMode, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowViewport, useMeasuredSize, useOptionalFlowInstance };