@statelyai/flow-react 0.6.0 → 0.7.1

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/dist/esm/index.js CHANGED
@@ -1,45 +1,45 @@
1
- import { n as useFlowInstance, r as useOptionalFlowInstance, t as FlowInstanceProvider } from "./FlowInstanceContext.js";
2
- import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
1
+ import { a as FlowInstanceProvider, i as LinesBackground, n as CrossBackground, o as useFlowInstance, r as DotsBackground, s as useOptionalFlowInstance, t as Background } from "./backgrounds2.js";
2
+ import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, clientToCanvas, computeRenderableIds, createEntityGeometryObserver, createFlowView, createMeasurementObserver, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
3
+ import { CONNECTION_PREVIEW_EDGE_ID, areNodesMeasured, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getProjectedEdgeAttrs, getProjectedNodeAttrs, getToolbarPlacement, nodeDepths, pathToSVG, profileModesForContext, profilePoliciesForContext, viewportCenteredOn } from "@statelyai/flow";
4
+ import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- import { applyFlowColorMode, attachContainer, attachInput, attachViewportTransform, computeRenderableIds, createMeasurementObserver, getAttachedContainer, nodePositionStyle, renderableIdsEqual, resolveFlowColorMode, viewportStyle } from "@statelyai/flow-dom";
5
- import { CONNECTION_PREVIEW_EDGE_ID, createConnectionPreviewGraph, createEntityChangeNotifier, createFlow, createFlowInputRuntime, getAnchorTransform, getAutoPanDelta, getMinimapProjection, getToolbarPlacement, nodeDepths, pathToSVG, profileModesForContext, viewportCenteredOn } from "@statelyai/flow";
6
+ import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
7
+ import { createPortal } from "react-dom";
6
8
  export * from "@statelyai/flow-dom";
7
9
  //#region src/createFlowInstance.ts
8
10
  function createFlowInstance(options) {
11
+ const { keymap, ...createFlowSettings } = options.settings ?? {};
9
12
  const flow = createFlow({
10
- graph: options.graph,
11
- ...options.settings
13
+ graph: options.initialGraph,
14
+ ...createFlowSettings
12
15
  });
16
+ const inputSettings = { keymap };
13
17
  const cleanup = /* @__PURE__ */ new Set();
14
18
  function getSnapshot() {
15
- return flow.store.getSnapshot().context;
19
+ return flow.getSnapshot();
16
20
  }
17
- function clientToCanvas(point) {
18
- const rect = getAttachedContainer(flow.store)?.getBoundingClientRect();
19
- return flow.screenToCanvas({
20
- x: point.x - (rect?.left ?? 0),
21
- y: point.y - (rect?.top ?? 0)
22
- });
21
+ function clientToCanvas$1(point) {
22
+ return clientToCanvas(flow.internals.store, point);
23
23
  }
24
24
  function listen(selector, listener, listenerOptions = {}) {
25
25
  const isEqual = listenerOptions.isEqual ?? Object.is;
26
26
  let previousValue = selector(getSnapshot());
27
27
  if (listenerOptions.fireImmediately) listener(previousValue, previousValue, getSnapshot());
28
- const unsubscribe = flow.subscribe((context) => {
29
- const nextValue = selector(context);
30
- if (isEqual(previousValue, nextValue)) return;
31
- const currentPreviousValue = previousValue;
28
+ const unsubscribe = flow.subscribe(selector, (nextValue, currentPreviousValue) => {
32
29
  previousValue = nextValue;
33
- listener(nextValue, currentPreviousValue, context);
34
- });
30
+ listener(nextValue, currentPreviousValue, getSnapshot());
31
+ }, { equals: isEqual });
35
32
  cleanup.add(unsubscribe);
36
33
  return () => {
37
34
  cleanup.delete(unsubscribe);
38
35
  unsubscribe();
39
36
  };
40
37
  }
38
+ const configure = (next) => {
39
+ flow.configure(next);
40
+ };
41
41
  function subscribeEvent(event, listener) {
42
- const subscription = flow.on(event, listener);
42
+ const subscription = event === "commit" ? flow.on("commit", listener) : flow.on(event, listener);
43
43
  const unsubscribe = () => subscription.unsubscribe();
44
44
  cleanup.add(unsubscribe);
45
45
  return () => {
@@ -49,12 +49,16 @@ function createFlowInstance(options) {
49
49
  }
50
50
  if (options.events?.connectionEnded) subscribeEvent("connectionEnded", options.events.connectionEnded);
51
51
  if (options.events?.layoutRequested) subscribeEvent("layoutRequested", options.events.layoutRequested);
52
+ if (options.events?.commit) subscribeEvent("commit", options.events.commit);
53
+ if (options.onCommit) subscribeEvent("commit", options.onCommit);
52
54
  for (const listener of options.listeners ?? []) listen(listener.selector, listener.onChange, listener);
53
55
  return {
54
56
  ...flow,
55
57
  getSnapshot,
58
+ configure,
56
59
  listen,
57
- clientToCanvas,
60
+ clientToCanvas: clientToCanvas$1,
61
+ inputSettings,
58
62
  destroy() {
59
63
  for (const unsubscribe of cleanup) unsubscribe();
60
64
  cleanup.clear();
@@ -67,25 +71,19 @@ function createFlowInstance(options) {
67
71
  function useFlow(options) {
68
72
  return useCreateFlowInstance(options);
69
73
  }
74
+ /** @deprecated Use `useFlow(options)`. */
70
75
  function useCreateFlowInstance(options) {
71
76
  const instanceRef = useRef(null);
72
77
  const listenersRef = useRef(options.listeners);
73
78
  const eventsRef = useRef(options.events);
74
- const didMountRef = useRef(false);
79
+ const lifecycleRef = useRef(0);
75
80
  listenersRef.current = options.listeners;
76
81
  eventsRef.current = options.events;
77
82
  if (!instanceRef.current) instanceRef.current = createFlowInstance({
78
- graph: options.graph,
83
+ initialGraph: options.initialGraph,
79
84
  settings: options.settings
80
85
  });
81
86
  const flow = instanceRef.current;
82
- useEffect(() => {
83
- if (!didMountRef.current) {
84
- didMountRef.current = true;
85
- return;
86
- }
87
- flow.trigger.setGraph({ graph: options.graph });
88
- }, [flow, options.graph]);
89
87
  useEffect(() => {
90
88
  const cleanup = [];
91
89
  if (eventsRef.current?.connectionEnded) {
@@ -96,6 +94,14 @@ function useCreateFlowInstance(options) {
96
94
  const subscription = flow.on("layoutRequested", eventsRef.current.layoutRequested);
97
95
  cleanup.push(() => subscription.unsubscribe());
98
96
  }
97
+ if (eventsRef.current?.commit) {
98
+ const subscription = flow.on("commit", eventsRef.current.commit);
99
+ cleanup.push(() => subscription.unsubscribe());
100
+ }
101
+ if (options.onCommit) {
102
+ const subscription = flow.on("commit", options.onCommit);
103
+ cleanup.push(() => subscription.unsubscribe());
104
+ }
99
105
  for (const listener of listenersRef.current ?? []) cleanup.push(flow.listen(listener.selector, listener.onChange, listener));
100
106
  return () => {
101
107
  for (const dispose of cleanup) dispose();
@@ -103,40 +109,70 @@ function useCreateFlowInstance(options) {
103
109
  }, [
104
110
  flow,
105
111
  options.listeners,
106
- options.events
112
+ options.events,
113
+ options.onCommit
107
114
  ]);
108
- useEffect(() => () => flow.destroy(), [flow]);
115
+ useEffect(() => {
116
+ const lifecycle = lifecycleRef;
117
+ const generation = ++lifecycle.current;
118
+ return () => {
119
+ queueMicrotask(() => {
120
+ if (lifecycle.current !== generation) return;
121
+ lifecycle.current++;
122
+ flow.destroy();
123
+ });
124
+ };
125
+ }, [flow]);
109
126
  return flow;
110
127
  }
111
128
  function useFlowSelector(selector, options = {}) {
112
129
  const contextFlow = useOptionalFlowInstance();
113
130
  const flow = options.flow ?? contextFlow;
114
131
  if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
115
- const selectorRef = useRef(selector);
116
- const isEqualRef = useRef(options.isEqual ?? Object.is);
117
- selectorRef.current = selector;
118
- isEqualRef.current = options.isEqual ?? Object.is;
119
- const [selected, setSelected] = useState(() => selector(flow.getSnapshot()));
120
- useEffect(() => {
121
- setSelected((previous) => {
122
- const next = selectorRef.current(flow.getSnapshot());
123
- return isEqualRef.current(previous, next) ? previous : next;
124
- });
125
- return flow.listen((context) => selectorRef.current(context), (value) => {
126
- setSelected((previous) => isEqualRef.current(previous, value) ? previous : value);
127
- });
128
- }, [flow]);
129
- return selected;
132
+ return useSyncExternalStoreWithSelector(useCallback((listener) => flow.subscribe(() => listener()), [flow]), flow.getSnapshot, flow.getSnapshot, selector, options.isEqual);
130
133
  }
131
134
  function useFlowContext() {
132
135
  return useFlowSelector((context) => context);
133
136
  }
137
+ function useConnection() {
138
+ return useFlowSelector((context) => context.connection);
139
+ }
140
+ function useFlowConnections(input) {
141
+ const flow = useFlowInstance();
142
+ return useFlowSelector(() => flow.connections.get(input), { isEqual: (current, next) => current.length === next.length && current.every((connection, index) => {
143
+ const candidate = next[index];
144
+ return connection.edge === candidate.edge && connection.source.nodeId === candidate.source.nodeId && connection.source.port === candidate.source.port && connection.target.nodeId === candidate.target.nodeId && connection.target.port === candidate.target.port;
145
+ }) });
146
+ }
147
+ function useNodesInitialized() {
148
+ return useFlowSelector(areNodesMeasured);
149
+ }
150
+ function useKeyPress(keys) {
151
+ const keySet = useMemo(() => new Set(Array.isArray(keys) ? keys : [keys]), [keys]);
152
+ const [pressed, setPressed] = useState(false);
153
+ useEffect(() => {
154
+ function update(event, value) {
155
+ const target = event.target;
156
+ if (target && (target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT")) return;
157
+ if (keySet.has(event.key)) setPressed(value);
158
+ }
159
+ const down = (event) => update(event, true);
160
+ const up = (event) => update(event, false);
161
+ window.addEventListener("keydown", down);
162
+ window.addEventListener("keyup", up);
163
+ return () => {
164
+ window.removeEventListener("keydown", down);
165
+ window.removeEventListener("keyup", up);
166
+ };
167
+ }, [keySet]);
168
+ return pressed;
169
+ }
134
170
  const entityNotifiers = /* @__PURE__ */ new WeakMap();
135
171
  function getEntityNotifier(flow) {
136
- let notifier = entityNotifiers.get(flow.store);
172
+ let notifier = entityNotifiers.get(flow.internals.store);
137
173
  if (!notifier) {
138
- notifier = createEntityChangeNotifier(flow.store);
139
- entityNotifiers.set(flow.store, notifier);
174
+ notifier = createEntityChangeNotifier(flow.internals.store, { getEdgeDependents: flow.internals.engine.getEdgeSourceDependents });
175
+ entityNotifiers.set(flow.internals.store, notifier);
140
176
  }
141
177
  return notifier;
142
178
  }
@@ -152,15 +188,23 @@ function useFlowEntity(id, selector, options = {}) {
152
188
  if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
153
189
  const selectorRef = useRef(selector);
154
190
  const isEqualRef = useRef(options.isEqual ?? Object.is);
191
+ const selectedRef = useRef(void 0);
192
+ const selectedInitializedRef = useRef(false);
155
193
  selectorRef.current = selector;
156
194
  isEqualRef.current = options.isEqual ?? Object.is;
157
- const [selected, setSelected] = useState(() => selector(flow.getSnapshot()));
195
+ const [selected, setSelected] = useState(() => {
196
+ const initial = selector(flow.getSnapshot());
197
+ selectedRef.current = initial;
198
+ selectedInitializedRef.current = true;
199
+ return initial;
200
+ });
158
201
  useEffect(() => {
159
202
  const update = () => {
160
- setSelected((previous) => {
161
- const next = selectorRef.current(flow.getSnapshot());
162
- return isEqualRef.current(previous, next) ? previous : next;
163
- });
203
+ const next = selectorRef.current(flow.getSnapshot());
204
+ if (selectedInitializedRef.current && isEqualRef.current(selectedRef.current, next)) return;
205
+ selectedRef.current = next;
206
+ selectedInitializedRef.current = true;
207
+ setSelected(next);
164
208
  };
165
209
  update();
166
210
  return getEntityNotifier(flow).subscribe(id, update);
@@ -205,7 +249,7 @@ function useFlowEdge(id, selector, isEqual) {
205
249
  function useFlowPort(id, selector, isEqual) {
206
250
  const flow = useFlowInstance();
207
251
  return useFlowSelector((context) => {
208
- const addressable = flow?.getAddressable(id);
252
+ const addressable = flow?.getEntityRef(id);
209
253
  if (!addressable || addressable.kind !== "port") throw new Error(`Flow port "${id}" was not found.`);
210
254
  const binding = {
211
255
  ...addressable.port,
@@ -244,24 +288,128 @@ function getDerivedFields(context, id, props) {
244
288
  * and applied to the flow store. This replaces hand-written gesture handlers.
245
289
  */
246
290
  function useFlowInput(flow, containerRef, options = {}) {
247
- const optionsRef = useRef(options);
248
- optionsRef.current = options;
291
+ const { modes, profile, keyboardTarget, keymap } = options;
292
+ const effectiveKeymap = keymap ?? flow.inputSettings?.keymap;
293
+ const keymapKey = effectiveKeymap ? JSON.stringify(effectiveKeymap) : void 0;
294
+ const keymapRef = useRef(effectiveKeymap);
295
+ keymapRef.current = effectiveKeymap;
249
296
  useEffect(() => {
250
297
  const container = containerRef.current;
251
298
  if (!container) return;
252
- const { modes, profile } = optionsRef.current;
299
+ const activeKeymap = keymapRef.current;
253
300
  const runtime = createFlowInputRuntime({
254
- store: flow.store,
255
- engine: flow.engine,
256
- modes: modes ?? (profile ? profileModesForContext(profile) : void 0)
301
+ store: flow.internals.store,
302
+ engine: flow.internals.engine,
303
+ modes: modes ?? (profile ? profileModesForContext(profile) : void 0),
304
+ policies: profile ? profilePoliciesForContext(profile) : void 0,
305
+ ...activeKeymap ? { keymap: activeKeymap } : {}
257
306
  });
258
307
  return attachInput({
259
308
  container,
260
- store: flow.store,
309
+ store: flow.internals.store,
261
310
  runtime,
262
- autoPan: (context, screenPoint) => getAutoPanDelta(context, screenPoint, flow.engine)
311
+ keyboardTarget,
312
+ autoPan: (context, screenPoint) => getAutoPanDelta(context, screenPoint, flow.internals.engine)
263
313
  });
264
- }, [flow, containerRef]);
314
+ }, [
315
+ flow,
316
+ containerRef,
317
+ modes,
318
+ profile,
319
+ keyboardTarget,
320
+ keymapKey
321
+ ]);
322
+ }
323
+ //#endregion
324
+ //#region src/MeasurementContext.tsx
325
+ const MeasurementContext = createContext(null);
326
+ function MeasurementProvider(props) {
327
+ return /* @__PURE__ */ jsx(MeasurementContext.Provider, {
328
+ value: props.observer,
329
+ children: props.children
330
+ });
331
+ }
332
+ function useMeasurementObserver() {
333
+ const observer = useContext(MeasurementContext);
334
+ if (!observer) throw new Error("Measurement observer is missing.");
335
+ return observer;
336
+ }
337
+ //#endregion
338
+ //#region src/useFlowView.tsx
339
+ const FlowViewContext = createContext(null);
340
+ function FlowViewProvider(props) {
341
+ const measurements = useMeasurementObserver();
342
+ const view = useMemo(() => ({
343
+ exportImage: (options) => {
344
+ const container = props.containerRef.current;
345
+ if (!container) return Promise.reject(/* @__PURE__ */ new Error("Flow view is not mounted."));
346
+ return createFlowView(container, { measurements }).exportImage(options);
347
+ },
348
+ refreshMeasurements: (targets) => measurements.refresh(targets)
349
+ }), [measurements, props.containerRef]);
350
+ return /* @__PURE__ */ jsx(FlowViewContext.Provider, {
351
+ value: view,
352
+ children: props.children
353
+ });
354
+ }
355
+ function useFlowView() {
356
+ const view = useContext(FlowViewContext);
357
+ if (!view) throw new Error("useFlowView() must be used within <StatelyFlow>.");
358
+ return view;
359
+ }
360
+ //#endregion
361
+ //#region src/baseStyles.ts
362
+ /**
363
+ * Minimal base stylesheet for the flow canvas. Intentionally tiny and
364
+ * inert-by-default: it only styles elements that opt in via `data-flow-*`
365
+ * attributes, and every visual value is a namespaced `--flow-*` CSS variable
366
+ * so consumers can theme it (set the variable on the canvas root or any
367
+ * ancestor) without overriding rules.
368
+ *
369
+ * Attributes:
370
+ * - `data-flow-interactive-when-selected` — the element ignores all pointer /
371
+ * wheel input (so canvas pan/zoom and box-select pass straight through it,
372
+ * e.g. over an embedded `<iframe>`) until its node/edge is selected, at which
373
+ * point it becomes fully interactive. Mark the embedded content, not the
374
+ * whole node, so ports and the selecting click still work. Don't also set an
375
+ * inline `pointer-events` on the same element — inline wins over this rule.
376
+ * - `data-flow-selection-box` — the box-select marquee (styled here; geometry
377
+ * is set inline by the renderer).
378
+ */
379
+ const FLOW_BASE_STYLES_ID = "flow-base-styles";
380
+ const FLOW_BASE_CSS = `
381
+ [data-flow-root] {
382
+ --flow-selection-color: #2563eb;
383
+ --flow-selection-bg: rgba(37, 99, 235, 0.12);
384
+ touch-action: none;
385
+ }
386
+
387
+ [data-flow-interactive-when-selected] {
388
+ pointer-events: none;
389
+ }
390
+ [data-flow-selected="true"] [data-flow-interactive-when-selected] {
391
+ pointer-events: auto;
392
+ }
393
+
394
+ [data-flow-selection-box] {
395
+ position: absolute;
396
+ box-sizing: border-box;
397
+ border: calc(1px / var(--flow-zoom, 1)) solid var(--flow-selection-color);
398
+ border-radius: calc(2px / var(--flow-zoom, 1));
399
+ background: var(--flow-selection-bg);
400
+ pointer-events: none;
401
+ }
402
+ `;
403
+ /** Inject the base stylesheet once per document (deduped by element id). */
404
+ function useFlowBaseStyles() {
405
+ useLayoutEffect(() => {
406
+ if (typeof document === "undefined") return;
407
+ if (document.getElementById("flow-base-styles")) return;
408
+ const style = document.createElement("style");
409
+ style.id = FLOW_BASE_STYLES_ID;
410
+ style.textContent = FLOW_BASE_CSS;
411
+ document.head.appendChild(style);
412
+ }, []);
265
413
  }
266
414
  //#endregion
267
415
  //#region src/NodeContext.tsx
@@ -273,20 +421,56 @@ function useNodeRenderContext() {
273
421
  return context;
274
422
  }
275
423
  //#endregion
424
+ //#region src/LayerPortals.tsx
425
+ const FlowLayerTargetsContext = createContext(null);
426
+ function FlowLayerTargetsProvider(props) {
427
+ return /* @__PURE__ */ jsx(FlowLayerTargetsContext.Provider, {
428
+ value: props.targets,
429
+ children: props.children
430
+ });
431
+ }
432
+ function useFlowLayerTargets() {
433
+ const targets = useContext(FlowLayerTargetsContext);
434
+ if (!targets) throw new Error("Flow layer portals must be rendered within <StatelyFlow>.");
435
+ return targets;
436
+ }
437
+ /** Render canvas-coordinate content in the transformed viewport layer. */
438
+ function ViewportPortal(props) {
439
+ const target = useFlowLayerTargets().viewport;
440
+ return target ? createPortal(props.children, target) : null;
441
+ }
442
+ /** Render screen-coordinate content in the untransformed pane layer. */
443
+ function PanePortal(props) {
444
+ const target = useFlowLayerTargets().pane;
445
+ return target ? createPortal(props.children, target) : null;
446
+ }
447
+ //#endregion
276
448
  //#region src/StatelyFlow.tsx
277
449
  function stringArraysEqual(a, b) {
278
450
  if (a.length !== b.length) return false;
279
451
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
280
452
  return true;
281
453
  }
454
+ function nodePaintEntriesEqual(a, b) {
455
+ if (a.length !== b.length) return false;
456
+ for (let index = 0; index < a.length; index++) if (a[index]?.id !== b[index]?.id || a[index]?.orderIndex !== b[index]?.orderIndex) return false;
457
+ return true;
458
+ }
459
+ function classNames$1(...values) {
460
+ return values.filter(Boolean).join(" ");
461
+ }
462
+ function assignRef(ref, value) {
463
+ if (typeof ref === "function") ref(value);
464
+ else if (ref) ref.current = value;
465
+ }
282
466
  function useRenderableIds(instance, enabled) {
283
- return useFlowSelector((context) => computeRenderableIds(instance.engine, context, enabled), { isEqual: renderableIdsEqual });
467
+ return useFlowSelector((context) => computeRenderableIds(instance.internals.engine, context, enabled), { isEqual: renderableIdsEqual });
284
468
  }
285
469
  function useViewportTransform(ref, instance) {
286
470
  useLayoutEffect(() => {
287
471
  const el = ref.current;
288
472
  if (!el) return;
289
- return attachViewportTransform(instance.store, el);
473
+ return attachViewportTransform(instance.internals.store, el);
290
474
  }, [ref, instance]);
291
475
  }
292
476
  function StatelyFlow(props) {
@@ -297,11 +481,12 @@ function StatelyFlow(props) {
297
481
  return /* @__PURE__ */ jsx(OwnedStatelyFlow, { ...props });
298
482
  }
299
483
  function OwnedStatelyFlow(props) {
300
- const flow = useCreateFlowInstance({
301
- graph: props.graph,
484
+ const flow = useFlow({
485
+ initialGraph: props.initialGraph,
302
486
  settings: props.settings,
303
487
  listeners: props.listeners,
304
- events: props.events
488
+ events: props.events,
489
+ onCommit: props.onCommit
305
490
  });
306
491
  return /* @__PURE__ */ jsx(StatelyFlowView, {
307
492
  ...props,
@@ -310,61 +495,155 @@ function OwnedStatelyFlow(props) {
310
495
  }
311
496
  function StatelyFlowView(props) {
312
497
  const containerRef = useRef(null);
498
+ const setContainerRef = useCallback((element) => {
499
+ containerRef.current = element;
500
+ assignRef(props.rootRef, element);
501
+ }, [props.rootRef]);
313
502
  const viewportRef = useRef(null);
503
+ const [viewportLayer, setViewportLayer] = useState(null);
504
+ const [paneLayer, setPaneLayer] = useState(null);
505
+ const layerTargets = useMemo(() => ({
506
+ viewport: viewportLayer,
507
+ pane: paneLayer
508
+ }), [viewportLayer, paneLayer]);
509
+ const measurementObserver = useMemo(() => createMeasurementObserver(props.flow.internals.store, { container: () => containerRef.current }), [props.flow]);
510
+ const geometryObserver = useMemo(() => createEntityGeometryObserver(props.flow.internals.store, { getEdgeDependents: props.flow.internals.engine.getEdgeSourceDependents }), [props.flow]);
511
+ useLayoutEffect(() => () => {
512
+ measurementObserver.disconnect();
513
+ geometryObserver.disconnect();
514
+ }, [geometryObserver, measurementObserver]);
314
515
  useViewportTransform(viewportRef, props.flow);
516
+ useFlowBaseStyles();
517
+ const projections = useActiveProjections(props.flow, props.projections);
518
+ const { className: rootClassName, style: rootStyle, tabIndex: rootTabIndex, ...rootProps } = props.rootProps ?? {};
315
519
  useLayoutEffect(() => {
316
520
  const element = containerRef.current;
317
521
  if (!element) return;
318
- return attachContainer(props.flow.store, element);
522
+ return attachContainer(props.flow.internals.store, element);
319
523
  }, [props.flow]);
320
524
  useFlowInput(props.flow, containerRef, {
321
525
  modes: props.inputModes,
322
- profile: props.settings?.store?.profile
526
+ profile: props.settings?.store?.profile,
527
+ keyboardTarget: props.keyboardTarget,
528
+ keymap: props.settings?.keymap
323
529
  });
530
+ useEffect(() => {
531
+ props.flow.configure({
532
+ engine: props.settings?.engine,
533
+ store: props.settings?.store,
534
+ defaultEdgeOptions: props.settings?.defaultEdgeOptions,
535
+ createEdgeOnConnect: props.settings?.createEdgeOnConnect,
536
+ layout: props.settings?.layout,
537
+ historyLimit: props.settings?.store?.historyLimit
538
+ });
539
+ if (props.settings?.projections) props.flow.projections.set(props.settings.projections);
540
+ }, [
541
+ props.flow,
542
+ props.settings?.engine,
543
+ props.settings?.store,
544
+ props.settings?.defaultEdgeOptions,
545
+ props.settings?.createEdgeOnConnect,
546
+ props.settings?.layout,
547
+ props.settings?.projections
548
+ ]);
324
549
  return /* @__PURE__ */ jsx(FlowInstanceProvider, {
325
550
  flow: props.flow,
326
- children: /* @__PURE__ */ jsxs("div", {
327
- ref: containerRef,
328
- className: props.className,
329
- style: {
330
- position: "relative",
331
- overflow: "hidden",
332
- width: "100%",
333
- height: "100%",
334
- minHeight: 320,
335
- touchAction: "none",
336
- ...props.style
337
- },
338
- children: [
339
- props.background,
340
- /* @__PURE__ */ jsxs("div", {
341
- ref: viewportRef,
342
- "data-flow-viewport": true,
343
- style: viewportStyle(),
344
- children: [
345
- /* @__PURE__ */ jsx(EdgeLayer, {
346
- renderEdge: props.renderEdge,
347
- onlyRenderVisible: props.onlyRenderVisible
348
- }),
349
- /* @__PURE__ */ jsx(NodeLayer, {
350
- renderNode: props.renderNode,
351
- onlyRenderVisible: props.onlyRenderVisible
352
- }),
353
- /* @__PURE__ */ jsx(SnapLinesLayer, {}),
354
- /* @__PURE__ */ jsx(EdgeLabelLayer, { onlyRenderVisible: props.onlyRenderVisible })
355
- ]
356
- }),
357
- /* @__PURE__ */ jsx("div", {
358
- "data-flow-overlay": true,
359
- children: props.children
551
+ children: /* @__PURE__ */ jsx(MeasurementProvider, {
552
+ observer: measurementObserver,
553
+ children: /* @__PURE__ */ jsx(FlowViewProvider, {
554
+ containerRef,
555
+ children: /* @__PURE__ */ jsx(FlowLayerTargetsProvider, {
556
+ targets: layerTargets,
557
+ children: /* @__PURE__ */ jsxs("div", {
558
+ ...rootProps,
559
+ ref: setContainerRef,
560
+ className: classNames$1("stately-flow", "stately-flow__pane", props.className, rootClassName),
561
+ "data-flow-root": "",
562
+ tabIndex: rootTabIndex ?? 0,
563
+ style: {
564
+ position: "relative",
565
+ overflow: "hidden",
566
+ width: "100%",
567
+ height: "100%",
568
+ minHeight: 320,
569
+ touchAction: "none",
570
+ ...props.style,
571
+ ...rootStyle
572
+ },
573
+ children: [
574
+ props.background,
575
+ /* @__PURE__ */ jsxs("div", {
576
+ ref: viewportRef,
577
+ className: "stately-flow__viewport",
578
+ "data-flow-viewport": true,
579
+ style: viewportStyle(),
580
+ children: [
581
+ /* @__PURE__ */ jsx(EdgeLayer, {
582
+ geometryObserver,
583
+ renderEdge: props.renderEdge,
584
+ renderConnectionLine: props.renderConnectionLine,
585
+ projections,
586
+ onlyRenderVisible: props.onlyRenderVisible
587
+ }),
588
+ /* @__PURE__ */ jsx(NodeLayer, {
589
+ geometryObserver,
590
+ renderNode: props.renderNode,
591
+ projections,
592
+ onlyRenderVisible: props.onlyRenderVisible
593
+ }),
594
+ /* @__PURE__ */ jsx(SnapLinesLayer, {}),
595
+ /* @__PURE__ */ jsx(EdgeLabelLayer, {
596
+ geometryObserver,
597
+ renderEdgeLabel: props.renderEdgeLabel,
598
+ projections,
599
+ onlyRenderVisible: props.onlyRenderVisible
600
+ }),
601
+ /* @__PURE__ */ jsx(SelectionBoxLayer, {}),
602
+ /* @__PURE__ */ jsx("div", {
603
+ ref: setViewportLayer,
604
+ className: "stately-flow__viewport-layer",
605
+ "data-flow-canvas-layer": "",
606
+ "data-flow-layer": "viewport",
607
+ style: {
608
+ position: "absolute",
609
+ inset: 0,
610
+ pointerEvents: "none"
611
+ },
612
+ children: props.layers?.viewport
613
+ })
614
+ ]
615
+ }),
616
+ /* @__PURE__ */ jsx("div", {
617
+ ref: setPaneLayer,
618
+ className: "stately-flow__pane-layer",
619
+ "data-flow-canvas-layer": "",
620
+ "data-flow-layer": "pane",
621
+ style: {
622
+ position: "absolute",
623
+ inset: 0,
624
+ pointerEvents: "none"
625
+ },
626
+ children: props.layers?.pane
627
+ }),
628
+ /* @__PURE__ */ jsx("div", {
629
+ className: "stately-flow__overlays",
630
+ "data-flow-overlay": true,
631
+ children: props.children
632
+ })
633
+ ]
634
+ })
360
635
  })
361
- ]
636
+ })
362
637
  })
363
638
  });
364
639
  }
640
+ function useActiveProjections(flow, projections) {
641
+ const flowProjections = useSyncExternalStore(flow.projections.subscribe, flow.projections.get, flow.projections.get);
642
+ return projections ?? flowProjections;
643
+ }
365
644
  function SnapLinesLayer() {
366
645
  const instance = useFlowInstance();
367
- const snapResult = useFlowSelector(() => instance.engine.getSnapResult());
646
+ const snapResult = useFlowSelector(() => instance.internals.engine.getSnapResult());
368
647
  const viewport = useFlowSelector((context) => context.viewport);
369
648
  const viewportSize = useFlowSelector((context) => context.viewportSize);
370
649
  if (!snapResult) return null;
@@ -374,11 +653,16 @@ function SnapLinesLayer() {
374
653
  width: viewportSize.width / viewport.zoom,
375
654
  height: viewportSize.height / viewport.zoom
376
655
  };
377
- const strokeWidth = 1.5 / viewport.zoom;
656
+ const strokeWidth = 1 / viewport.zoom;
378
657
  const capSize = 4 / viewport.zoom;
658
+ const fontSize = 10 / viewport.zoom;
659
+ const labelOffset = 6 / viewport.zoom;
660
+ const snapColor = "var(--flow-snap-line-color, #ef4444)";
661
+ const spacingColor = "var(--flow-spacing-guide-color, var(--flow-snap-line-color, #ef4444))";
379
662
  return /* @__PURE__ */ jsx("svg", {
380
663
  "aria-hidden": "true",
381
664
  "data-flow-canvas-layer": true,
665
+ "data-flow-snap-guides": "",
382
666
  style: {
383
667
  position: "absolute",
384
668
  inset: 0,
@@ -388,89 +672,128 @@ function SnapLinesLayer() {
388
672
  pointerEvents: "none"
389
673
  },
390
674
  children: /* @__PURE__ */ jsxs("g", { children: [snapResult.snapLines.map((line, index) => line.axis === "x" ? /* @__PURE__ */ jsx("line", {
675
+ "data-flow-snap-line": "x",
391
676
  x1: line.value,
392
677
  y1: visibleBounds.y,
393
678
  x2: line.value,
394
679
  y2: visibleBounds.y + visibleBounds.height,
395
- stroke: "#ef4444",
680
+ stroke: snapColor,
396
681
  strokeWidth,
397
- strokeDasharray: `${6 / viewport.zoom} ${4 / viewport.zoom}`
682
+ strokeDasharray: `${4 / viewport.zoom}`
398
683
  }, `snap-x-${index}`) : /* @__PURE__ */ jsx("line", {
684
+ "data-flow-snap-line": "y",
399
685
  x1: visibleBounds.x,
400
686
  y1: line.value,
401
687
  x2: visibleBounds.x + visibleBounds.width,
402
688
  y2: line.value,
403
- stroke: "#ef4444",
689
+ stroke: snapColor,
404
690
  strokeWidth,
405
- strokeDasharray: `${6 / viewport.zoom} ${4 / viewport.zoom}`
691
+ strokeDasharray: `${4 / viewport.zoom}`
406
692
  }, `snap-y-${index}`)), snapResult.spacingGuides?.flatMap((guide, guideIndex) => guide.ranges.flatMap((range, rangeIndex) => {
407
693
  const key = `spacing-${guideIndex}-${rangeIndex}`;
408
694
  if (guide.axis === "x") return [
409
695
  /* @__PURE__ */ jsx("line", {
696
+ "data-flow-spacing-guide": "x",
410
697
  x1: range.start,
411
698
  y1: guide.position,
412
699
  x2: range.end,
413
700
  y2: guide.position,
414
- stroke: "#ef4444",
701
+ stroke: spacingColor,
415
702
  strokeWidth
416
703
  }, `${key}-line`),
417
704
  /* @__PURE__ */ jsx("line", {
418
705
  x1: range.start,
419
- y1: guide.position - capSize,
706
+ y1: guide.position,
420
707
  x2: range.start,
421
- y2: guide.position + capSize,
422
- stroke: "#ef4444",
423
- strokeWidth,
708
+ y2: guide.position,
709
+ stroke: spacingColor,
710
+ strokeWidth: capSize,
424
711
  strokeLinecap: "round"
425
712
  }, `${key}-start`),
426
713
  /* @__PURE__ */ jsx("line", {
427
714
  x1: range.end,
428
- y1: guide.position - capSize,
715
+ y1: guide.position,
429
716
  x2: range.end,
430
- y2: guide.position + capSize,
431
- stroke: "#ef4444",
432
- strokeWidth,
717
+ y2: guide.position,
718
+ stroke: spacingColor,
719
+ strokeWidth: capSize,
433
720
  strokeLinecap: "round"
434
- }, `${key}-end`)
721
+ }, `${key}-end`),
722
+ /* @__PURE__ */ jsx("text", {
723
+ "data-flow-spacing-label": "x",
724
+ x: (range.start + range.end) / 2,
725
+ y: guide.position - labelOffset,
726
+ fontSize,
727
+ fill: spacingColor,
728
+ textAnchor: "middle",
729
+ children: Math.round(guide.spacing)
730
+ }, `${key}-label`)
435
731
  ];
436
732
  return [
437
733
  /* @__PURE__ */ jsx("line", {
734
+ "data-flow-spacing-guide": "y",
438
735
  x1: guide.position,
439
736
  y1: range.start,
440
737
  x2: guide.position,
441
738
  y2: range.end,
442
- stroke: "#ef4444",
739
+ stroke: spacingColor,
443
740
  strokeWidth
444
741
  }, `${key}-line`),
445
742
  /* @__PURE__ */ jsx("line", {
446
- x1: guide.position - capSize,
743
+ x1: guide.position,
447
744
  y1: range.start,
448
- x2: guide.position + capSize,
745
+ x2: guide.position,
449
746
  y2: range.start,
450
- stroke: "#ef4444",
451
- strokeWidth,
747
+ stroke: spacingColor,
748
+ strokeWidth: capSize,
452
749
  strokeLinecap: "round"
453
750
  }, `${key}-start`),
454
751
  /* @__PURE__ */ jsx("line", {
455
- x1: guide.position - capSize,
752
+ x1: guide.position,
456
753
  y1: range.end,
457
- x2: guide.position + capSize,
754
+ x2: guide.position,
458
755
  y2: range.end,
459
- stroke: "#ef4444",
460
- strokeWidth,
756
+ stroke: spacingColor,
757
+ strokeWidth: capSize,
461
758
  strokeLinecap: "round"
462
- }, `${key}-end`)
759
+ }, `${key}-end`),
760
+ /* @__PURE__ */ jsx("text", {
761
+ "data-flow-spacing-label": "y",
762
+ x: guide.position + labelOffset,
763
+ y: (range.start + range.end) / 2,
764
+ fontSize,
765
+ fill: spacingColor,
766
+ textAnchor: "start",
767
+ dominantBaseline: "middle",
768
+ children: Math.round(guide.spacing)
769
+ }, `${key}-label`)
463
770
  ];
464
771
  }))] })
465
772
  });
466
773
  }
774
+ function SelectionBoxLayer() {
775
+ const box = useFlowSelector((context) => context.selectionBox);
776
+ if (!box) return null;
777
+ const [a, b] = box;
778
+ return /* @__PURE__ */ jsx("div", {
779
+ "aria-hidden": "true",
780
+ "data-flow-selection-box": "",
781
+ style: {
782
+ left: Math.min(a.x, b.x),
783
+ top: Math.min(a.y, b.y),
784
+ width: Math.abs(b.x - a.x),
785
+ height: Math.abs(b.y - a.y)
786
+ }
787
+ });
788
+ }
467
789
  function EdgeLayer(props) {
468
790
  const instance = useFlowInstance();
469
791
  const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
470
- const edgeIds = useFlowSelector((context) => context.graph.edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
792
+ const edgeIds = useFlowSelector((context) => getOrderedEntities(instance, context).edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
471
793
  const renderEdge = props.renderEdge;
472
794
  return /* @__PURE__ */ jsx("svg", {
473
795
  "aria-label": "edges",
796
+ className: "stately-flow__edges",
474
797
  "data-flow-canvas-layer": true,
475
798
  style: {
476
799
  position: "absolute",
@@ -478,34 +801,77 @@ function EdgeLayer(props) {
478
801
  width: "100%",
479
802
  height: "100%",
480
803
  overflow: "visible",
481
- pointerEvents: "auto"
804
+ pointerEvents: "none"
482
805
  },
483
806
  children: /* @__PURE__ */ jsxs("g", { children: [edgeIds.map((id) => {
484
807
  if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
485
808
  return /* @__PURE__ */ jsx(EdgeView, {
486
809
  flow: instance,
810
+ geometryObserver: props.geometryObserver,
487
811
  id,
812
+ projections: props.projections,
488
813
  renderEdge
489
814
  }, id);
490
815
  }), /* @__PURE__ */ jsx(ConnectionPreviewEdge, {
491
816
  flow: instance,
492
- renderEdge
817
+ renderEdge,
818
+ renderConnectionLine: props.renderConnectionLine
493
819
  })] })
494
820
  });
495
821
  }
496
822
  const EdgeView = memo(function EdgeView(props) {
497
- const { edge, pathData, selected, hovered, highlights } = useFlowEntity(props.id, (context) => ({
823
+ const ref = useRef(null);
824
+ const { graph, edge, pathData, selected, focused, hovered, highlights } = useFlowEntity(props.id, (context) => ({
825
+ graph: context.graph,
498
826
  edge: props.flow.getEdge(props.id) ?? null,
499
827
  pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
828
+ geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
500
829
  selected: context.selection.has(props.id),
830
+ focused: context.focusedEntityId === props.id,
501
831
  hovered: context.hoveredEntityIds.has(props.id),
502
- highlights: props.flow.engine.getHighlights(props.id)
503
- }), { isEqual: (a, b) => a.edge === b.edge && a.pathData === b.pathData && a.selected === b.selected && a.hovered === b.hovered && a.highlights === b.highlights });
832
+ highlights: props.flow.internals.engine.getHighlights(props.id)
833
+ }), { isEqual: (a, b) => {
834
+ if (!(a.selected === b.selected && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights) || a.geometryActive !== b.geometryActive) return false;
835
+ if (a.geometryActive) return edgeRenderContentEqual(a.edge, b.edge);
836
+ return a.graph === b.graph && a.edge === b.edge && a.pathData === b.pathData;
837
+ } });
838
+ useLayoutEffect(() => {
839
+ const element = ref.current;
840
+ if (!element) return;
841
+ return props.geometryObserver.observeEdge(element, {
842
+ id: props.id,
843
+ getPathData: () => props.flow.getAllEdgePaths().get(props.id) ?? null
844
+ });
845
+ }, [
846
+ props.flow,
847
+ props.geometryObserver,
848
+ props.id
849
+ ]);
504
850
  if (!edge || !pathData) return null;
851
+ const attrs = getProjectedEdgeAttrs({
852
+ graph,
853
+ edge,
854
+ projections: props.projections
855
+ });
856
+ const focusable = props.flow.internals.engine.resolveEntityConfig(edge).focusable;
857
+ const focusProps = {
858
+ role: "group",
859
+ "aria-roledescription": "flow edge",
860
+ "aria-label": props.flow.internals.engine.getAriaLabel(props.id),
861
+ "aria-selected": selected,
862
+ tabIndex: focusable ? 0 : void 0,
863
+ onFocus: () => props.flow.internals.store.trigger.focus({ entityId: props.id })
864
+ };
505
865
  if (props.renderEdge) return /* @__PURE__ */ jsxs("g", {
866
+ ref,
867
+ ...focusProps,
506
868
  "data-entity-id": props.id,
507
869
  "data-flow-selected": selected ? "true" : void 0,
508
- style: { pointerEvents: "auto" },
870
+ className: classNames$1("stately-flow__edge", attrs.className),
871
+ style: {
872
+ pointerEvents: "auto",
873
+ outline: focused ? "2px solid #2563eb" : "none"
874
+ },
509
875
  children: [/* @__PURE__ */ jsx(EdgeHitTarget, {
510
876
  flow: props.flow,
511
877
  edge,
@@ -515,19 +881,28 @@ const EdgeView = memo(function EdgeView(props) {
515
881
  flow: props.flow,
516
882
  edge,
517
883
  pathData,
884
+ attrs,
518
885
  selected,
519
886
  hovered,
520
887
  highlights
521
888
  })]
522
889
  });
523
890
  return /* @__PURE__ */ jsx("g", {
891
+ ref,
892
+ ...focusProps,
524
893
  "data-entity-id": props.id,
525
894
  "data-flow-selected": selected ? "true" : void 0,
526
- style: { pointerEvents: "auto" },
895
+ className: classNames$1("stately-flow__edge", attrs.className),
896
+ style: {
897
+ pointerEvents: "auto",
898
+ opacity: attrs.opacity,
899
+ outline: focused ? "2px solid #2563eb" : "none"
900
+ },
527
901
  children: /* @__PURE__ */ jsx(DefaultEdge, {
528
902
  flow: props.flow,
529
903
  edge,
530
904
  pathData,
905
+ attrs,
531
906
  selected
532
907
  })
533
908
  });
@@ -535,17 +910,22 @@ const EdgeView = memo(function EdgeView(props) {
535
910
  /** The in-progress connection gesture's preview edge (renders only mid-gesture). */
536
911
  function ConnectionPreviewEdge(props) {
537
912
  const connection = useFlowSelector((context) => context.connection);
538
- if (!connection?.active) return null;
913
+ if (!connection?.active || connection.preview === false) return null;
539
914
  const previewGraph = createConnectionPreviewGraph(props.flow.getSnapshot().graph, connection);
540
915
  const pathData = props.flow.getAllEdgePaths({ graph: previewGraph }).get(CONNECTION_PREVIEW_EDGE_ID);
541
916
  const edge = previewGraph.edges.find((candidate) => candidate.id === CONNECTION_PREVIEW_EDGE_ID);
542
917
  if (!edge || !pathData) return null;
543
918
  return /* @__PURE__ */ jsx("g", {
544
919
  style: { pointerEvents: "none" },
545
- children: props.renderEdge ? props.renderEdge({
920
+ children: props.renderConnectionLine ? props.renderConnectionLine({
921
+ connection,
922
+ pathData,
923
+ isValid: connection.isValid
924
+ }) : props.renderEdge ? props.renderEdge({
546
925
  flow: props.flow,
547
926
  edge,
548
927
  pathData,
928
+ attrs: {},
549
929
  selected: false,
550
930
  hovered: false,
551
931
  highlights: []
@@ -553,22 +933,26 @@ function ConnectionPreviewEdge(props) {
553
933
  flow: props.flow,
554
934
  edge,
555
935
  pathData,
936
+ attrs: {},
556
937
  selected: false,
557
938
  preview: true
558
939
  })
559
940
  });
560
941
  }
561
942
  function DefaultEdge(props) {
562
- return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
943
+ return /* @__PURE__ */ jsxs(Fragment, { children: [props.pathData.segments.map((segment, index) => {
563
944
  const d = pathToSVG(segment);
564
945
  return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("path", {
565
946
  d,
947
+ "data-flow-edge-segment": index,
566
948
  fill: "none",
567
- stroke: props.preview ? props.edge.data?.isValid === false ? "#dc2626" : props.edge.data?.isValid === true ? "#16a34a" : "#0f766e" : props.selected ? "#2563eb" : "#94a3b8",
949
+ stroke: props.attrs.color ?? (props.preview ? props.edge.data?.isValid === false ? "#dc2626" : props.edge.data?.isValid === true ? "#16a34a" : "#0f766e" : props.selected ? "#2563eb" : "#94a3b8"),
568
950
  strokeDasharray: props.preview ? "6 6" : void 0,
569
- strokeWidth: props.selected ? 3 : 2
951
+ strokeWidth: props.attrs.strokeWidth ?? (props.selected ? 3 : 2),
952
+ style: props.attrs.style
570
953
  }), props.preview ? null : /* @__PURE__ */ jsx("path", {
571
954
  d,
955
+ "data-flow-edge-segment": index,
572
956
  "data-entity-id": props.edge.id,
573
957
  fill: "none",
574
958
  stroke: "#000",
@@ -576,12 +960,18 @@ function DefaultEdge(props) {
576
960
  strokeWidth: 18,
577
961
  pointerEvents: "stroke"
578
962
  })] }, index);
579
- }) });
963
+ }), props.preview ? null : /* @__PURE__ */ jsx(EdgeEndpointHandles, {
964
+ flow: props.flow,
965
+ edge: props.edge,
966
+ pathData: props.pathData,
967
+ selected: props.selected
968
+ })] });
580
969
  }
581
970
  function EdgeHitTarget(props) {
582
- return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
971
+ return /* @__PURE__ */ jsxs(Fragment, { children: [props.pathData.segments.map((segment, index) => {
583
972
  return /* @__PURE__ */ jsx("path", {
584
973
  d: pathToSVG(segment),
974
+ "data-flow-edge-segment": index,
585
975
  "data-entity-id": props.edge.id,
586
976
  fill: "none",
587
977
  stroke: "#000",
@@ -589,88 +979,225 @@ function EdgeHitTarget(props) {
589
979
  strokeWidth: 18,
590
980
  pointerEvents: "stroke"
591
981
  }, index);
592
- }) });
982
+ }), /* @__PURE__ */ jsx(EdgeEndpointHandles, { ...props })] });
983
+ }
984
+ function EdgeEndpointHandles(props) {
985
+ if (!props.selected) return null;
986
+ if (!props.flow.internals.engine.resolveEntityConfig(props.edge).reconnectable) return null;
987
+ const first = props.pathData.segments[0]?.points[0];
988
+ const last = props.pathData.segments.at(-1)?.points.at(-1);
989
+ if (!first || !last) return null;
990
+ const radius = 6 / props.flow.getContext().viewport.zoom;
991
+ const strokeWidth = 2 / props.flow.getContext().viewport.zoom;
992
+ return /* @__PURE__ */ jsx(Fragment, { children: [{
993
+ end: "source",
994
+ point: first,
995
+ cursor: "crosshair"
996
+ }, {
997
+ end: "target",
998
+ point: last,
999
+ cursor: "crosshair"
1000
+ }].map(({ end, point, cursor }) => /* @__PURE__ */ jsx("circle", {
1001
+ "data-entity-id": props.edge.id,
1002
+ "data-flow-part": `edge-endpoint:${end}`,
1003
+ "data-flow-edge-endpoint": end,
1004
+ cx: point.x,
1005
+ cy: point.y,
1006
+ r: radius,
1007
+ fill: "#ffffff",
1008
+ stroke: "#2563eb",
1009
+ strokeWidth,
1010
+ style: {
1011
+ cursor,
1012
+ pointerEvents: "auto"
1013
+ }
1014
+ }, end)) });
593
1015
  }
594
1016
  function EdgeLabelLayer(props) {
595
1017
  const instance = useFlowInstance();
596
1018
  const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
1019
+ const renderEdgeLabel = props.renderEdgeLabel;
1020
+ const graphEdgeIds = useFlowSelector((context) => context.graph.edges.map((edge) => edge.id), { isEqual: stringArraysEqual });
1021
+ const durableLabelIds = useFlowSelector((context) => context.graph.edges.filter((edge) => edge.label).map((edge) => edge.id), { isEqual: stringArraysEqual });
597
1022
  return /* @__PURE__ */ jsx("div", {
598
1023
  "aria-label": "edge labels",
1024
+ className: "stately-flow__edge-labels",
599
1025
  style: {
600
1026
  position: "absolute",
601
1027
  inset: 0,
602
1028
  pointerEvents: "none"
603
1029
  },
604
- children: useFlowSelector((context) => context.graph.edges.filter((edge) => edge.label).map((edge) => edge.id), { isEqual: stringArraysEqual }).map((id) => {
1030
+ children: useMemo(() => {
1031
+ const ids = new Set(durableLabelIds);
1032
+ const existing = new Set(graphEdgeIds);
1033
+ for (const entry of props.projections ?? []) for (const id of Object.keys(entry.projection.edges ?? {})) if (existing.has(id)) ids.add(id);
1034
+ return [...ids];
1035
+ }, [
1036
+ durableLabelIds,
1037
+ graphEdgeIds,
1038
+ props.projections
1039
+ ]).map((id) => {
605
1040
  if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
606
1041
  return /* @__PURE__ */ jsx(EdgeLabelItem, {
607
1042
  flow: instance,
608
- id
1043
+ geometryObserver: props.geometryObserver,
1044
+ id,
1045
+ projections: props.projections,
1046
+ renderEdgeLabel
609
1047
  }, id);
610
1048
  })
611
1049
  });
612
1050
  }
613
1051
  const EdgeLabelItem = memo(function EdgeLabelItem(props) {
614
- const { edge, pathData, selected } = useFlowEntity(props.id, (context) => ({
1052
+ const { graph, edge, pathData, selected, dragging, hovered, highlights } = useFlowEntity(props.id, (context) => ({
1053
+ graph: context.graph,
615
1054
  edge: props.flow.getEdge(props.id) ?? null,
616
1055
  pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
617
- selected: context.selection.has(props.id)
618
- }), { isEqual: (a, b) => a.edge === b.edge && a.pathData === b.pathData && a.selected === b.selected });
619
- if (!edge?.label || !pathData?.labelBounds) return null;
1056
+ geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
1057
+ selected: context.selection.has(props.id),
1058
+ dragging: context.draggedEntityId === props.id,
1059
+ hovered: context.hoveredEntityIds.has(props.id),
1060
+ highlights: props.flow.internals.engine.getHighlights(props.id)
1061
+ }), { isEqual: (a, b) => {
1062
+ if (!(a.selected === b.selected && a.dragging === b.dragging && a.hovered === b.hovered && a.highlights === b.highlights) || a.geometryActive !== b.geometryActive) return false;
1063
+ if (a.geometryActive) return edgeRenderContentEqual(a.edge, b.edge);
1064
+ return a.graph === b.graph && a.edge === b.edge && a.pathData === b.pathData;
1065
+ } });
1066
+ if (!edge || !pathData) return null;
1067
+ const attrs = getProjectedEdgeAttrs({
1068
+ graph,
1069
+ edge,
1070
+ projections: props.projections
1071
+ });
1072
+ const bounds = pathData.labelBounds ?? projectedEdgeLabelBounds(pathData, attrs.label);
1073
+ if (!edge.label && attrs.label === void 0 || !bounds) return null;
620
1074
  return /* @__PURE__ */ jsx(EdgeLabelView, {
621
1075
  flow: props.flow,
1076
+ geometryObserver: props.geometryObserver,
622
1077
  edge,
623
- bounds: pathData.labelBounds,
624
- selected
1078
+ attrs,
1079
+ bounds,
1080
+ pathData,
1081
+ selected,
1082
+ dragging,
1083
+ hovered,
1084
+ highlights,
1085
+ projections: props.projections,
1086
+ renderEdgeLabel: props.renderEdgeLabel
625
1087
  });
626
1088
  });
627
1089
  function EdgeLabelView(props) {
628
1090
  const ref = useRef(null);
629
- const observer = useMemo(() => createMeasurementObserver(props.flow.store), [props.flow.store]);
1091
+ const observer = useMeasurementObserver();
630
1092
  useLayoutEffect(() => {
631
1093
  const element = ref.current;
632
1094
  if (!element) return;
633
1095
  observer.observe(element, { edgeId: props.edge.id });
634
- return () => observer.disconnect();
1096
+ return () => observer.unobserve(element);
635
1097
  }, [observer, props.edge.id]);
1098
+ useLayoutEffect(() => {
1099
+ const element = ref.current;
1100
+ if (!element) return;
1101
+ return props.geometryObserver.observeRect(element, {
1102
+ id: props.edge.id,
1103
+ autoSize: props.edge.label?.autoSize,
1104
+ getRect: () => {
1105
+ const pathData = props.flow.getAllEdgePaths().get(props.edge.id);
1106
+ if (!pathData) return null;
1107
+ const edge = props.flow.getEdge(props.edge.id);
1108
+ if (!edge) return null;
1109
+ const attrs = getProjectedEdgeAttrs({
1110
+ graph: props.flow.getSnapshot().graph,
1111
+ edge,
1112
+ projections: props.projections
1113
+ });
1114
+ return pathData.labelBounds ?? projectedEdgeLabelBounds(pathData, attrs.label);
1115
+ }
1116
+ });
1117
+ }, [
1118
+ props.edge.id,
1119
+ props.edge.label?.autoSize,
1120
+ props.flow,
1121
+ props.geometryObserver,
1122
+ props.projections
1123
+ ]);
636
1124
  const text = (props.edge.label?.data)?.text;
1125
+ const label = props.attrs.label ?? String(text ?? props.edge.id);
1126
+ const customLabel = props.renderEdgeLabel?.({
1127
+ flow: props.flow,
1128
+ edge: props.edge,
1129
+ pathData: props.pathData,
1130
+ attrs: props.attrs,
1131
+ bounds: props.bounds,
1132
+ selected: props.selected,
1133
+ dragging: props.dragging,
1134
+ hovered: props.hovered,
1135
+ highlights: props.highlights
1136
+ });
1137
+ const defaultStyle = props.renderEdgeLabel ? void 0 : {
1138
+ padding: "4px 8px",
1139
+ border: `1px solid ${props.selected ? "#2563eb" : props.attrs.color ?? "#cbd5e1"}`,
1140
+ borderRadius: 999,
1141
+ background: "#fff",
1142
+ color: "#334155",
1143
+ display: "grid",
1144
+ placeItems: "center",
1145
+ cursor: "grab",
1146
+ font: "600 12px/1 system-ui, sans-serif"
1147
+ };
637
1148
  return /* @__PURE__ */ jsx("div", {
638
1149
  ref,
639
1150
  "data-entity-id": props.edge.id,
640
1151
  "data-flow-edge-label": "",
1152
+ "data-flow-selected": props.selected ? "true" : void 0,
1153
+ "data-flow-dragging": props.dragging ? "true" : void 0,
1154
+ className: classNames$1("stately-flow__edge-label", props.attrs.className),
641
1155
  style: {
642
1156
  position: "absolute",
643
- left: props.bounds.x,
644
- top: props.bounds.y,
645
- width: props.bounds.width,
646
- height: props.bounds.height,
1157
+ left: 0,
1158
+ top: 0,
1159
+ transform: `translate(${props.bounds.x}px, ${props.bounds.y}px)`,
1160
+ width: props.edge.label?.autoSize ? void 0 : props.bounds.width,
1161
+ height: props.edge.label?.autoSize ? void 0 : props.bounds.height,
647
1162
  boxSizing: "border-box",
648
- padding: "4px 8px",
649
- border: `1px solid ${props.selected ? "#2563eb" : "#cbd5e1"}`,
650
- borderRadius: 999,
651
- background: "#fff",
652
- color: "#334155",
653
- display: "grid",
654
- placeItems: "center",
655
- cursor: "grab",
656
- font: "600 12px/1 system-ui, sans-serif",
1163
+ ...defaultStyle,
657
1164
  pointerEvents: "auto",
658
- userSelect: "none"
1165
+ userSelect: "none",
1166
+ opacity: props.attrs.opacity,
1167
+ ...props.attrs.style
659
1168
  },
660
- children: String(text ?? props.edge.id)
1169
+ children: props.renderEdgeLabel ? customLabel : label
661
1170
  });
662
1171
  }
1172
+ function projectedEdgeLabelBounds(pathData, label) {
1173
+ if (label === void 0) return null;
1174
+ const points = pathData.segments.flatMap((segment) => segment.points);
1175
+ if (!points.length) return null;
1176
+ const middle = points[Math.floor(points.length / 2)];
1177
+ return {
1178
+ x: middle.x - 40,
1179
+ y: middle.y - 12,
1180
+ width: 80,
1181
+ height: 24
1182
+ };
1183
+ }
663
1184
  function NodeLayer(props) {
664
1185
  const instance = useFlowInstance();
665
1186
  const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
666
- const nodeIds = useFlowSelector((context) => {
667
- const depths = nodeDepths(context.graph.nodes);
668
- return context.graph.nodes.map((node) => node.id).sort((a, b) => (depths.get(a) ?? 0) - (depths.get(b) ?? 0));
669
- }, { isEqual: stringArraysEqual });
670
- const observer = useMemo(() => createMeasurementObserver(instance.store), [instance]);
671
- useLayoutEffect(() => () => observer.disconnect(), [observer]);
1187
+ const nodePaintEntries = useFlowSelector((context) => {
1188
+ const paintOrder = new Map(getOrderedEntities(instance, context).nodes.map((node, orderIndex) => [node.id, orderIndex]));
1189
+ return context.graph.nodes.flatMap((node) => {
1190
+ const orderIndex = paintOrder.get(node.id);
1191
+ return orderIndex === void 0 ? [] : [{
1192
+ id: node.id,
1193
+ orderIndex
1194
+ }];
1195
+ });
1196
+ }, { isEqual: nodePaintEntriesEqual });
1197
+ const observer = useMeasurementObserver();
672
1198
  return /* @__PURE__ */ jsx("div", {
673
1199
  "aria-label": "nodes",
1200
+ className: "stately-flow__nodes",
674
1201
  "data-flow-canvas-layer": true,
675
1202
  style: {
676
1203
  position: "absolute",
@@ -678,35 +1205,100 @@ function NodeLayer(props) {
678
1205
  transformOrigin: "0 0",
679
1206
  pointerEvents: "none"
680
1207
  },
681
- children: nodeIds.map((id) => {
1208
+ children: nodePaintEntries.map(({ id, orderIndex }) => {
682
1209
  if (renderableIds && !renderableIds.nodeIds.has(id)) return null;
683
1210
  return /* @__PURE__ */ jsx(NodeView, {
684
1211
  flow: instance,
1212
+ geometryObserver: props.geometryObserver,
685
1213
  id,
1214
+ orderIndex,
686
1215
  observer,
1216
+ projections: props.projections,
687
1217
  renderNode: props.renderNode
688
1218
  }, id);
689
1219
  })
690
1220
  });
691
1221
  }
1222
+ function getOrderedEntities(flow, context) {
1223
+ const maybeEngine = flow.internals.engine;
1224
+ const visibleOrdered = maybeEngine.getVisibleEntities?.();
1225
+ if (visibleOrdered) return visibleOrdered;
1226
+ const ordered = maybeEngine.getEntityOrder?.();
1227
+ if (ordered) return ordered;
1228
+ const depths = nodeDepths(context.graph.nodes);
1229
+ const nodeOrder = new Map(context.graph.nodes.map((node, index) => [node.id, index]));
1230
+ const edgeOrder = new Map(context.graph.edges.map((edge, index) => [edge.id, index]));
1231
+ return {
1232
+ nodes: [...context.graph.nodes].sort((a, b) => {
1233
+ const depthDelta = (depths.get(a.id) ?? 0) - (depths.get(b.id) ?? 0);
1234
+ if (depthDelta !== 0) return depthDelta;
1235
+ const zDelta = (a.zIndex ?? 0) - (b.zIndex ?? 0);
1236
+ if (zDelta !== 0) return zDelta;
1237
+ const selectedDelta = Number(context.selection.has(a.id)) - Number(context.selection.has(b.id));
1238
+ if (selectedDelta !== 0) return selectedDelta;
1239
+ return (nodeOrder.get(a.id) ?? 0) - (nodeOrder.get(b.id) ?? 0);
1240
+ }),
1241
+ edges: [...context.graph.edges].sort((a, b) => {
1242
+ const depthDelta = Math.max(depths.get(a.sourceId) ?? 0, depths.get(a.targetId) ?? 0) - Math.max(depths.get(b.sourceId) ?? 0, depths.get(b.targetId) ?? 0);
1243
+ if (depthDelta !== 0) return depthDelta;
1244
+ const zDelta = (a.zIndex ?? 0) - (b.zIndex ?? 0);
1245
+ if (zDelta !== 0) return zDelta;
1246
+ return (edgeOrder.get(a.id) ?? 0) - (edgeOrder.get(b.id) ?? 0);
1247
+ })
1248
+ };
1249
+ }
692
1250
  function rectsShallowEqual(a, b) {
693
1251
  return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
694
1252
  }
1253
+ const ENTITY_GEOMETRY_KEYS = /* @__PURE__ */ new Set([
1254
+ "x",
1255
+ "y",
1256
+ "dx",
1257
+ "dy",
1258
+ "width",
1259
+ "height",
1260
+ "measured"
1261
+ ]);
1262
+ const EDGE_GEOMETRY_KEYS = /* @__PURE__ */ new Set([...ENTITY_GEOMETRY_KEYS, "label"]);
1263
+ function shallowObjectEqualExcept(a, b, ignored) {
1264
+ if (a === b) return true;
1265
+ if (!a || !b) return false;
1266
+ const left = a;
1267
+ const right = b;
1268
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
1269
+ for (const key of keys) if (!ignored.has(key) && left[key] !== right[key]) return false;
1270
+ return true;
1271
+ }
1272
+ function nodeRenderContentEqual(a, b) {
1273
+ return shallowObjectEqualExcept(a, b, ENTITY_GEOMETRY_KEYS);
1274
+ }
1275
+ function edgeRenderContentEqual(a, b) {
1276
+ if (a === b) return true;
1277
+ if (!a || !b) return false;
1278
+ return shallowObjectEqualExcept(a, b, EDGE_GEOMETRY_KEYS) && shallowObjectEqualExcept(a.label, b.label, ENTITY_GEOMETRY_KEYS);
1279
+ }
695
1280
  const NodeView = memo(function NodeView(props) {
696
1281
  const ref = useRef(null);
697
- const { node, hidden, selected, dragging, focused, hovered, highlights, bounds } = useFlowEntity(props.id, (context) => {
1282
+ const { graph, node, hidden, selected, dragging, focused, hovered, highlights, bounds } = useFlowEntity(props.id, (context) => {
698
1283
  const current = props.flow.getNode(props.id) ?? null;
699
1284
  return {
1285
+ graph: context.graph,
700
1286
  node: current,
701
- hidden: !current || props.flow.engine.resolveEntityConfig(current).hidden,
1287
+ geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
1288
+ hidden: !current || props.flow.internals.engine.resolveEntityConfig(current).hidden,
702
1289
  selected: context.selection.has(props.id),
703
- dragging: context.draggedEntityId === props.id,
1290
+ dragging: context.canvasState === "dragging" && context.selection.has(props.id),
704
1291
  focused: context.focusedEntityId === props.id,
705
1292
  hovered: context.hoveredEntityIds.has(props.id),
706
- highlights: props.flow.engine.getHighlights(props.id),
707
- bounds: context.resizingEntityId === props.id && context.resizeCorners ? props.flow.engine.getResizeBounds() ?? props.flow.engine.resolvedBounds(props.id) : props.flow.engine.resolvedBounds(props.id)
1293
+ highlights: props.flow.internals.engine.getHighlights(props.id),
1294
+ bounds: context.resizingEntityId === props.id && context.resizeCorners ? props.flow.internals.engine.getResizeBounds() ?? props.flow.internals.engine.visualBounds(props.id) : props.flow.internals.engine.visualBounds(props.id)
708
1295
  };
709
- }, { 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) });
1296
+ }, { isEqual: (a, b) => {
1297
+ if (!(a.hidden === b.hidden && a.selected === b.selected && a.dragging === b.dragging && a.focused === b.focused && a.hovered === b.hovered && a.highlights === b.highlights) || a.geometryActive !== b.geometryActive) return false;
1298
+ if (a.geometryActive) return nodeRenderContentEqual(a.node, b.node);
1299
+ return a.graph === b.graph && a.node === b.node && rectsShallowEqual(a.bounds, b.bounds);
1300
+ } });
1301
+ const contentOwnsSize = Boolean(node?.autoSize && node.style?.customWidth === void 0 && node.style?.customHeight === void 0);
710
1302
  useLayoutEffect(() => {
711
1303
  const element = ref.current;
712
1304
  if (!element) return;
@@ -720,7 +1312,30 @@ const NodeView = memo(function NodeView(props) {
720
1312
  props.flow,
721
1313
  hidden
722
1314
  ]);
1315
+ useLayoutEffect(() => {
1316
+ const element = ref.current;
1317
+ if (!element) return;
1318
+ return props.geometryObserver.observeRect(element, {
1319
+ id: props.id,
1320
+ autoSize: contentOwnsSize,
1321
+ getRect: () => {
1322
+ const context = props.flow.getSnapshot();
1323
+ return context.resizingEntityId === props.id && context.resizeCorners ? props.flow.internals.engine.getResizeBounds() ?? props.flow.internals.engine.visualBounds(props.id) : props.flow.internals.engine.visualBounds(props.id);
1324
+ }
1325
+ });
1326
+ }, [
1327
+ contentOwnsSize,
1328
+ props.flow,
1329
+ props.geometryObserver,
1330
+ props.id
1331
+ ]);
723
1332
  if (!node || hidden) return null;
1333
+ const attrs = getProjectedNodeAttrs({
1334
+ graph,
1335
+ node,
1336
+ projections: props.projections
1337
+ });
1338
+ const focusable = props.flow.internals.engine.resolveEntityConfig(node).focusable;
724
1339
  return /* @__PURE__ */ jsx(NodeRenderContextProvider, {
725
1340
  value: {
726
1341
  node,
@@ -730,26 +1345,31 @@ const NodeView = memo(function NodeView(props) {
730
1345
  ref,
731
1346
  "data-entity-id": props.id,
732
1347
  "data-flow-selected": selected ? "true" : void 0,
1348
+ className: classNames$1("stately-flow__node", attrs.className),
733
1349
  role: "group",
734
1350
  "aria-roledescription": "flow node",
735
- "aria-label": props.flow.engine.getAriaLabel(props.id) + (selected ? ", selected" : ""),
736
- tabIndex: 0,
737
- onFocus: () => props.flow.trigger.focus({ entityId: props.id }),
1351
+ "aria-selected": selected,
1352
+ "aria-label": props.flow.internals.engine.getAriaLabel(props.id) + (selected ? ", selected" : ""),
1353
+ tabIndex: focusable ? 0 : void 0,
1354
+ onFocus: () => props.flow.internals.store.trigger.focus({ entityId: props.id }),
738
1355
  style: {
739
1356
  ...nodePositionStyle(bounds, {
740
- autoSize: node.autoSize,
1357
+ autoSize: contentOwnsSize,
741
1358
  dragging
742
1359
  }),
743
1360
  cursor: dragging ? "grabbing" : "grab",
744
1361
  pointerEvents: "auto",
745
1362
  userSelect: "none",
1363
+ zIndex: props.orderIndex,
746
1364
  outline: focused ? "2px solid #2563eb" : "none",
747
- outlineOffset: 2
1365
+ outlineOffset: 2,
1366
+ opacity: attrs.opacity
748
1367
  },
749
1368
  children: props.renderNode ? props.renderNode({
750
1369
  flow: props.flow,
751
1370
  node,
752
1371
  bounds,
1372
+ attrs,
753
1373
  selected,
754
1374
  dragging,
755
1375
  hovered,
@@ -758,6 +1378,7 @@ const NodeView = memo(function NodeView(props) {
758
1378
  flow: props.flow,
759
1379
  node,
760
1380
  bounds,
1381
+ attrs,
761
1382
  selected,
762
1383
  dragging,
763
1384
  hovered,
@@ -766,6 +1387,7 @@ const NodeView = memo(function NodeView(props) {
766
1387
  flow: props.flow,
767
1388
  node,
768
1389
  bounds,
1390
+ attrs,
769
1391
  selected,
770
1392
  dragging,
771
1393
  hovered,
@@ -780,8 +1402,10 @@ const NodeView = memo(function NodeView(props) {
780
1402
  * `engine.getGroupGeometry`; custom renderers can do the same.
781
1403
  */
782
1404
  function DefaultGroupNode(props) {
783
- const geometry = props.flow.engine.getGroupGeometry(props.node.id);
1405
+ const geometry = props.flow.internals.engine.getGroupGeometry(props.node.id);
784
1406
  const label = props.node.data?.label;
1407
+ const badges = decorationsToArray(props.attrs.badge);
1408
+ const title = decorationTitle(props.attrs.tooltip);
785
1409
  if (!geometry) return /* @__PURE__ */ jsx(DefaultNode, { ...props });
786
1410
  const relative = (rect) => ({
787
1411
  left: rect.x - props.bounds.x,
@@ -794,13 +1418,14 @@ function DefaultGroupNode(props) {
794
1418
  position: "absolute",
795
1419
  ...relative(geometry.childArea),
796
1420
  boxSizing: "border-box",
797
- border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : "#cbd5e1"}`,
1421
+ border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : props.attrs.color ?? "#cbd5e1"}`,
798
1422
  ...dropTarget ? { background: "rgba(22, 163, 74, 0.08)" } : null,
799
1423
  borderRadius: 10,
800
1424
  background: "rgba(148, 163, 184, 0.07)",
801
1425
  pointerEvents: "none"
802
- } }), /* @__PURE__ */ jsx("div", {
1426
+ } }), /* @__PURE__ */ jsxs("div", {
803
1427
  "data-flow-drag-handle": true,
1428
+ title,
804
1429
  style: {
805
1430
  position: "absolute",
806
1431
  ...relative(geometry.contentBounds),
@@ -808,37 +1433,65 @@ function DefaultGroupNode(props) {
808
1433
  display: "flex",
809
1434
  alignItems: "center",
810
1435
  padding: "0 12px",
811
- border: `2px solid ${props.selected ? "#2563eb" : "#94a3b8"}`,
1436
+ border: `2px solid ${props.selected ? "#2563eb" : props.attrs.color ?? "#94a3b8"}`,
812
1437
  borderRadius: 8,
813
1438
  background: props.selected ? "#eff6ff" : "#f8fafc",
814
1439
  color: "#0f172a",
815
1440
  font: "600 13px/1.2 system-ui, sans-serif",
816
1441
  cursor: props.dragging ? "grabbing" : "grab",
817
- userSelect: "none"
1442
+ userSelect: "none",
1443
+ ...props.attrs.style
818
1444
  },
819
- children: String(label ?? props.node.id)
1445
+ children: [/* @__PURE__ */ jsx("span", { children: String(label ?? props.node.id) }), badges.length ? /* @__PURE__ */ jsx("span", {
1446
+ style: defaultBadgeListStyle,
1447
+ children: badges
1448
+ }) : null]
820
1449
  })] });
821
1450
  }
822
1451
  function DefaultNode(props) {
823
1452
  const label = props.node.data?.label;
824
- return /* @__PURE__ */ jsx("div", {
1453
+ const badges = decorationsToArray(props.attrs.badge);
1454
+ return /* @__PURE__ */ jsxs("div", {
1455
+ title: decorationTitle(props.attrs.tooltip),
825
1456
  style: {
1457
+ position: "relative",
826
1458
  display: "grid",
827
1459
  placeItems: "center",
828
1460
  width: "100%",
829
1461
  height: "100%",
830
1462
  boxSizing: "border-box",
831
1463
  padding: "10px 14px",
832
- border: `2px solid ${props.selected ? "#2563eb" : props.hovered ? "#94a3b8" : "#cbd5e1"}`,
1464
+ border: `2px solid ${props.selected ? "#2563eb" : props.attrs.color ?? (props.hovered ? "#94a3b8" : "#cbd5e1")}`,
833
1465
  borderRadius: 8,
834
1466
  background: props.selected ? "#eff6ff" : "#ffffff",
835
1467
  color: "#0f172a",
836
1468
  font: "600 14px/1.2 system-ui, sans-serif",
837
- boxShadow: props.selected ? "0 0 0 4px rgba(37, 99, 235, 0.16)" : "0 1px 2px rgba(15, 23, 42, 0.1)"
1469
+ boxShadow: props.selected ? "0 0 0 4px rgba(37, 99, 235, 0.16)" : "0 1px 2px rgba(15, 23, 42, 0.1)",
1470
+ ...props.attrs.style
838
1471
  },
839
- children: String(label ?? props.node.id)
1472
+ children: [/* @__PURE__ */ jsx("span", { children: String(label ?? props.node.id) }), badges.length ? /* @__PURE__ */ jsx("span", {
1473
+ style: defaultBadgeListStyle,
1474
+ children: badges
1475
+ }) : null]
840
1476
  });
841
1477
  }
1478
+ const defaultBadgeListStyle = {
1479
+ position: "absolute",
1480
+ top: -8,
1481
+ right: -8,
1482
+ display: "flex",
1483
+ gap: 4,
1484
+ alignItems: "center"
1485
+ };
1486
+ function decorationsToArray(value) {
1487
+ if (value === void 0) return [];
1488
+ return Array.isArray(value) ? value : [value];
1489
+ }
1490
+ function decorationTitle(value) {
1491
+ if (value === void 0) return void 0;
1492
+ const text = (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string" || typeof item === "number").map(String);
1493
+ return text.length ? text.join("\n") : void 0;
1494
+ }
842
1495
  //#endregion
843
1496
  //#region src/FlowToolbar.tsx
844
1497
  /** Border-box size of an element, kept current via ResizeObserver. */
@@ -878,7 +1531,7 @@ function useFlowToolbar(options = {}) {
878
1531
  const anchor = useFlowSelector((context) => {
879
1532
  const ids = options.ids ?? [...context.selection];
880
1533
  if (ids.length === 0) return null;
881
- const canvasRect = flow.engine.getToolbarAnchor(ids);
1534
+ const canvasRect = flow.internals.engine.getToolbarAnchor(ids);
882
1535
  if (!canvasRect) return null;
883
1536
  const zoom = context.viewport.zoom;
884
1537
  return {
@@ -973,7 +1626,7 @@ function MiniMap(props) {
973
1626
  x: event.clientX - rect.left,
974
1627
  y: event.clientY - rect.top
975
1628
  });
976
- flow.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
1629
+ flow.internals.store.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
977
1630
  };
978
1631
  return /* @__PURE__ */ jsxs("svg", {
979
1632
  "aria-label": "minimap",
@@ -1007,7 +1660,7 @@ function MiniMap(props) {
1007
1660
  ...props.style
1008
1661
  },
1009
1662
  children: [nodes.map((node) => {
1010
- const rect = projection.rectToMinimap(flow.engine.resolvedBounds(node.id));
1663
+ const rect = projection.rectToMinimap(flow.internals.engine.visualBounds(node.id));
1011
1664
  return props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
1012
1665
  x: rect.x,
1013
1666
  y: rect.y,
@@ -1028,6 +1681,60 @@ function MiniMap(props) {
1028
1681
  });
1029
1682
  }
1030
1683
  //#endregion
1684
+ //#region src/Panel.tsx
1685
+ const placement = {
1686
+ "top-left": {
1687
+ top: 12,
1688
+ left: 12
1689
+ },
1690
+ "top-center": {
1691
+ top: 12,
1692
+ left: "50%",
1693
+ transform: "translateX(-50%)"
1694
+ },
1695
+ "top-right": {
1696
+ top: 12,
1697
+ right: 12
1698
+ },
1699
+ "center-left": {
1700
+ top: "50%",
1701
+ left: 12,
1702
+ transform: "translateY(-50%)"
1703
+ },
1704
+ "center-right": {
1705
+ top: "50%",
1706
+ right: 12,
1707
+ transform: "translateY(-50%)"
1708
+ },
1709
+ "bottom-left": {
1710
+ bottom: 12,
1711
+ left: 12
1712
+ },
1713
+ "bottom-center": {
1714
+ bottom: 12,
1715
+ left: "50%",
1716
+ transform: "translateX(-50%)"
1717
+ },
1718
+ "bottom-right": {
1719
+ bottom: 12,
1720
+ right: 12
1721
+ }
1722
+ };
1723
+ function Panel(props) {
1724
+ const { position = "top-left", style, ...rest } = props;
1725
+ return /* @__PURE__ */ jsx("div", {
1726
+ "data-flow-panel": position,
1727
+ ...rest,
1728
+ style: {
1729
+ position: "absolute",
1730
+ pointerEvents: "auto",
1731
+ ...placement[position],
1732
+ ...style
1733
+ },
1734
+ children: props.children
1735
+ });
1736
+ }
1737
+ //#endregion
1031
1738
  //#region src/Controls.tsx
1032
1739
  const buttonStyle = {
1033
1740
  width: 28,
@@ -1047,18 +1754,15 @@ const buttonStyle = {
1047
1754
  */
1048
1755
  function Controls(props) {
1049
1756
  const flow = useFlowInstance();
1050
- return /* @__PURE__ */ jsxs("div", {
1757
+ return /* @__PURE__ */ jsxs(Panel, {
1051
1758
  role: "toolbar",
1052
1759
  "aria-label": "canvas controls",
1760
+ position: props.position ?? "bottom-left",
1053
1761
  className: props.className,
1054
1762
  style: {
1055
- position: "absolute",
1056
- left: 12,
1057
- bottom: 12,
1058
1763
  display: "flex",
1059
1764
  flexDirection: "column",
1060
1765
  gap: 4,
1061
- pointerEvents: "auto",
1062
1766
  ...props.style
1063
1767
  },
1064
1768
  children: [
@@ -1066,21 +1770,21 @@ function Controls(props) {
1066
1770
  type: "button",
1067
1771
  "aria-label": "zoom in",
1068
1772
  style: buttonStyle,
1069
- onClick: () => flow.trigger.zoomBy({ factor: 1.2 }),
1773
+ onClick: () => flow.internals.store.trigger.zoomBy({ factor: 1.2 }),
1070
1774
  children: "+"
1071
1775
  }),
1072
1776
  /* @__PURE__ */ jsx("button", {
1073
1777
  type: "button",
1074
1778
  "aria-label": "zoom out",
1075
1779
  style: buttonStyle,
1076
- onClick: () => flow.trigger.zoomBy({ factor: 1 / 1.2 }),
1780
+ onClick: () => flow.internals.store.trigger.zoomBy({ factor: 1 / 1.2 }),
1077
1781
  children: "−"
1078
1782
  }),
1079
1783
  /* @__PURE__ */ jsx("button", {
1080
1784
  type: "button",
1081
1785
  "aria-label": "fit view",
1082
1786
  style: buttonStyle,
1083
- onClick: () => flow.trigger.fitView({ padding: .1 }),
1787
+ onClick: () => flow.internals.store.trigger.fitView({ padding: .1 }),
1084
1788
  children: "⛶"
1085
1789
  }),
1086
1790
  props.children
@@ -1090,22 +1794,84 @@ function Controls(props) {
1090
1794
  //#endregion
1091
1795
  //#region src/EntityAnchor.tsx
1092
1796
  function EntityAnchor(props) {
1797
+ if (props.isVisible === false) return null;
1798
+ if ("bounds" in props && props.bounds) return /* @__PURE__ */ jsx(CanvasEntityAnchor, { ...props });
1799
+ return /* @__PURE__ */ jsx(RelativeEntityAnchor, { ...props });
1800
+ }
1801
+ function CanvasEntityAnchor(props) {
1093
1802
  const viewport = useFlowSelector((context) => context.viewport);
1094
1803
  const transform = getAnchorTransform(props.bounds, viewport, props.anchor);
1804
+ const { bounds: _bounds, anchor: _anchor, inverseScale, isVisible: _isVisible, children, className, style, ...divProps } = props;
1095
1805
  return /* @__PURE__ */ jsx("div", {
1096
- className: props.className,
1806
+ ...divProps,
1807
+ "data-flow-entity-anchor": "",
1808
+ className: classNames("stately-flow__entity-anchor", className),
1097
1809
  style: {
1098
1810
  position: "absolute",
1099
1811
  left: transform.x,
1100
1812
  top: transform.y,
1101
- transform: props.inverseScale ? `translate(-50%, -50%) scale(${transform.scale})` : "translate(-50%, -50%)",
1813
+ transform: inverseScale ? `translate(-50%, -50%) scale(${transform.scale})` : "translate(-50%, -50%)",
1102
1814
  transformOrigin: "center",
1103
1815
  pointerEvents: "auto",
1104
- ...props.style
1816
+ ...style
1105
1817
  },
1106
- children: props.children
1818
+ children
1819
+ });
1820
+ }
1821
+ function RelativeEntityAnchor(props) {
1822
+ const { side, anchor = defaultAnchorPoint(props.side), align = "center", offset = 0, inverseScale, isVisible: _isVisible, children, className, style, ...divProps } = props;
1823
+ const positionStyle = relativePositionStyle(side, align, anchor, offset, !!inverseScale);
1824
+ return /* @__PURE__ */ jsx("div", {
1825
+ ...divProps,
1826
+ "data-flow-entity-anchor": "",
1827
+ "data-flow-entity-anchor-side": side,
1828
+ className: classNames("stately-flow__entity-anchor", `stately-flow__entity-anchor-${side}`, className),
1829
+ style: {
1830
+ position: "absolute",
1831
+ zIndex: 20,
1832
+ pointerEvents: "auto",
1833
+ ...positionStyle,
1834
+ transformOrigin: inverseScale ? anchorTransformOrigin(anchor) : void 0,
1835
+ ...style
1836
+ },
1837
+ children
1107
1838
  });
1108
1839
  }
1840
+ function relativePositionStyle(side, align, anchor, offset, inverseScale) {
1841
+ const crossAxis = align === "start" ? "0%" : align === "end" ? "100%" : "50%";
1842
+ const translate = anchorTranslate(anchor);
1843
+ const signedOffset = side === "top" || side === "left" ? -offset : offset;
1844
+ const offsetValue = inverseScale ? `calc(${signedOffset}px * var(--flow-inverse-zoom, 1))` : `${signedOffset}px`;
1845
+ const offsetTransform = side === "top" || side === "bottom" ? `translateY(${offsetValue})` : `translateX(${offsetValue})`;
1846
+ const scaleTransform = inverseScale ? " scale(var(--flow-inverse-zoom, 1))" : "";
1847
+ return {
1848
+ left: side === "left" ? 0 : side === "right" ? "100%" : crossAxis,
1849
+ top: side === "top" ? 0 : side === "bottom" ? "100%" : crossAxis,
1850
+ transform: `translate(${translate.x}, ${translate.y}) ${offsetTransform}${scaleTransform}`
1851
+ };
1852
+ }
1853
+ function defaultAnchorPoint(side) {
1854
+ switch (side) {
1855
+ case "top": return "bottom-center";
1856
+ case "bottom": return "top-center";
1857
+ case "left": return "center-right";
1858
+ case "right": return "center-left";
1859
+ }
1860
+ }
1861
+ function anchorTranslate(anchor) {
1862
+ const [vertical, horizontal] = anchor === "center" ? ["center", "center"] : anchor.split("-");
1863
+ return {
1864
+ x: horizontal === "left" ? "0%" : horizontal === "right" ? "-100%" : "-50%",
1865
+ y: vertical === "top" ? "0%" : vertical === "bottom" ? "-100%" : "-50%"
1866
+ };
1867
+ }
1868
+ function anchorTransformOrigin(anchor) {
1869
+ const [vertical, horizontal] = anchor === "center" ? ["center", "center"] : anchor.split("-");
1870
+ return `${horizontal} ${vertical}`;
1871
+ }
1872
+ function classNames(...values) {
1873
+ return values.filter(Boolean).join(" ");
1874
+ }
1109
1875
  //#endregion
1110
1876
  //#region src/Port.tsx
1111
1877
  /**
@@ -1123,11 +1889,11 @@ function Port(props) {
1123
1889
  const { node } = useNodeRenderContext();
1124
1890
  const ref = useRef(null);
1125
1891
  const observerRef = useRef(null);
1892
+ const observer = useMeasurementObserver();
1126
1893
  const measured = props.measured ?? true;
1127
1894
  useLayoutEffect(() => {
1128
1895
  const element = ref.current;
1129
1896
  if (!element || !measured) return;
1130
- const observer = createMeasurementObserver(flow.store);
1131
1897
  observerRef.current = observer;
1132
1898
  observer.observe(element, {
1133
1899
  nodeId: node.id,
@@ -1135,10 +1901,10 @@ function Port(props) {
1135
1901
  });
1136
1902
  return () => {
1137
1903
  observerRef.current = null;
1138
- observer.disconnect();
1904
+ observer.unobserve(element);
1139
1905
  };
1140
1906
  }, [
1141
- flow.store,
1907
+ observer,
1142
1908
  node.id,
1143
1909
  props.name,
1144
1910
  measured
@@ -1166,6 +1932,128 @@ function Port(props) {
1166
1932
  });
1167
1933
  }
1168
1934
  //#endregion
1935
+ //#region src/DragHandle.tsx
1936
+ /**
1937
+ * Marks a region inside a custom node as the *only* draggable area. When a node
1938
+ * renders a `<DragHandle>`, pointer presses elsewhere on the node no longer
1939
+ * start a drag (they select/pass through) — the input runtime only begins a
1940
+ * move when the press lands inside an element carrying `data-flow-drag-handle`.
1941
+ *
1942
+ * Render it anywhere inside your node's markup:
1943
+ *
1944
+ * ```tsx
1945
+ * function MyNode(props) {
1946
+ * return (
1947
+ * <div>
1948
+ * <DragHandle>⠿ title bar</DragHandle>
1949
+ * <div>…non-draggable body…</div>
1950
+ * </div>
1951
+ * );
1952
+ * }
1953
+ * ```
1954
+ */
1955
+ function DragHandle({ children, style, ...rest }) {
1956
+ return /* @__PURE__ */ jsx("div", {
1957
+ "data-flow-drag-handle": "",
1958
+ style: {
1959
+ cursor: "grab",
1960
+ ...style
1961
+ },
1962
+ ...rest,
1963
+ children
1964
+ });
1965
+ }
1966
+ //#endregion
1967
+ //#region src/NodeResizer.tsx
1968
+ const DEFAULT_HANDLES = [
1969
+ "top",
1970
+ "right",
1971
+ "bottom",
1972
+ "left",
1973
+ "top-left",
1974
+ "top-right",
1975
+ "bottom-left",
1976
+ "bottom-right"
1977
+ ];
1978
+ const HANDLE_STYLE = {
1979
+ top: {
1980
+ left: "50%",
1981
+ top: 0,
1982
+ cursor: "ns-resize"
1983
+ },
1984
+ right: {
1985
+ left: "100%",
1986
+ top: "50%",
1987
+ cursor: "ew-resize"
1988
+ },
1989
+ bottom: {
1990
+ left: "50%",
1991
+ top: "100%",
1992
+ cursor: "ns-resize"
1993
+ },
1994
+ left: {
1995
+ left: 0,
1996
+ top: "50%",
1997
+ cursor: "ew-resize"
1998
+ },
1999
+ "top-left": {
2000
+ left: 0,
2001
+ top: 0,
2002
+ cursor: "nwse-resize"
2003
+ },
2004
+ "top-right": {
2005
+ left: "100%",
2006
+ top: 0,
2007
+ cursor: "nesw-resize"
2008
+ },
2009
+ "bottom-left": {
2010
+ left: 0,
2011
+ top: "100%",
2012
+ cursor: "nesw-resize"
2013
+ },
2014
+ "bottom-right": {
2015
+ left: "100%",
2016
+ top: "100%",
2017
+ cursor: "nwse-resize"
2018
+ }
2019
+ };
2020
+ function NodeResizer(props) {
2021
+ const { node } = useNodeRenderContext();
2022
+ const selected = useFlowSelector((context) => context.selection.has(node.id));
2023
+ if (!(props.isVisible ?? selected)) return null;
2024
+ return /* @__PURE__ */ jsx("div", {
2025
+ "aria-hidden": "true",
2026
+ className: props.className,
2027
+ "data-flow-node-resizer": "",
2028
+ style: {
2029
+ position: "absolute",
2030
+ inset: 0,
2031
+ pointerEvents: "none",
2032
+ ...props.style
2033
+ },
2034
+ children: (props.handles ?? DEFAULT_HANDLES).map((handle) => /* @__PURE__ */ jsx("div", {
2035
+ "data-entity-id": node.id,
2036
+ "data-flow-part": `resize-handle:${handle}`,
2037
+ "data-flow-resize-handle": handle,
2038
+ style: {
2039
+ position: "absolute",
2040
+ width: "calc(10px / var(--flow-zoom, 1))",
2041
+ height: "calc(10px / var(--flow-zoom, 1))",
2042
+ minWidth: 6,
2043
+ minHeight: 6,
2044
+ boxSizing: "border-box",
2045
+ border: "calc(1px / var(--flow-zoom, 1)) solid #ffffff",
2046
+ borderRadius: "calc(2px / var(--flow-zoom, 1))",
2047
+ background: "var(--flow-selection-color, #2563eb)",
2048
+ boxShadow: "0 0 0 calc(1px / var(--flow-zoom, 1)) rgba(15, 23, 42, 0.8)",
2049
+ pointerEvents: "auto",
2050
+ transform: "translate(-50%, -50%)",
2051
+ ...HANDLE_STYLE[handle]
2052
+ }
2053
+ }, handle))
2054
+ });
2055
+ }
2056
+ //#endregion
1169
2057
  //#region src/ColorMode.tsx
1170
2058
  function FlowColorModeScope(props) {
1171
2059
  const { colorMode, resolvedColorMode } = useFlowColorMode();
@@ -1210,4 +2098,4 @@ function useApplyFlowColorMode(element) {
1210
2098
  ]);
1211
2099
  }
1212
2100
  //#endregion
1213
- 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 };
2101
+ export { Background, Controls, CrossBackground, DotsBackground, DragHandle, EntityAnchor, FLOW_BASE_CSS, FLOW_BASE_STYLES_ID, FlowColorModeScope, FlowInstanceProvider, FlowToolbar, LinesBackground, MiniMap, NodeResizer, PanePortal, Panel, Port, StatelyFlow, ViewportPortal, createFlowInstance, useApplyFlowColorMode, useConnection, useCreateFlowInstance, useFlow, useFlowBaseStyles, useFlowColorMode, useFlowConnections, useFlowContext, useFlowEdge, useFlowEntity, useFlowInput, useFlowInstance, useFlowMeasurement, useFlowNode, useFlowPort, useFlowSelector, useFlowToolbar, useFlowValue, useFlowView, useFlowViewport, useKeyPress, useMeasuredSize, useNodesInitialized, useOptionalFlowInstance };