@statelyai/flow-react 0.5.0 → 0.7.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.
package/dist/esm/index.js CHANGED
@@ -1,44 +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";
8
+ export * from "@statelyai/flow-dom";
6
9
  //#region src/createFlowInstance.ts
7
10
  function createFlowInstance(options) {
11
+ const { keymap, ...createFlowSettings } = options.settings ?? {};
8
12
  const flow = createFlow({
9
- graph: options.graph,
10
- ...options.settings
13
+ graph: options.initialGraph,
14
+ ...createFlowSettings
11
15
  });
16
+ const inputSettings = { keymap };
12
17
  const cleanup = /* @__PURE__ */ new Set();
13
18
  function getSnapshot() {
14
- return flow.store.getSnapshot().context;
19
+ return flow.getSnapshot();
15
20
  }
16
- function clientToCanvas(point) {
17
- const rect = getAttachedContainer(flow.store)?.getBoundingClientRect();
18
- return flow.screenToCanvas({
19
- x: point.x - (rect?.left ?? 0),
20
- y: point.y - (rect?.top ?? 0)
21
- });
21
+ function clientToCanvas$1(point) {
22
+ return clientToCanvas(flow.internals.store, point);
22
23
  }
23
24
  function listen(selector, listener, listenerOptions = {}) {
24
25
  const isEqual = listenerOptions.isEqual ?? Object.is;
25
26
  let previousValue = selector(getSnapshot());
26
27
  if (listenerOptions.fireImmediately) listener(previousValue, previousValue, getSnapshot());
27
- const unsubscribe = flow.subscribe((context) => {
28
- const nextValue = selector(context);
29
- if (isEqual(previousValue, nextValue)) return;
30
- const currentPreviousValue = previousValue;
28
+ const unsubscribe = flow.subscribe(selector, (nextValue, currentPreviousValue) => {
31
29
  previousValue = nextValue;
32
- listener(nextValue, currentPreviousValue, context);
33
- });
30
+ listener(nextValue, currentPreviousValue, getSnapshot());
31
+ }, { equals: isEqual });
34
32
  cleanup.add(unsubscribe);
35
33
  return () => {
36
34
  cleanup.delete(unsubscribe);
37
35
  unsubscribe();
38
36
  };
39
37
  }
38
+ const configure = (next) => {
39
+ flow.configure(next);
40
+ };
40
41
  function subscribeEvent(event, listener) {
41
- const subscription = flow.on(event, listener);
42
+ const subscription = event === "commit" ? flow.on("commit", listener) : flow.on(event, listener);
42
43
  const unsubscribe = () => subscription.unsubscribe();
43
44
  cleanup.add(unsubscribe);
44
45
  return () => {
@@ -48,12 +49,16 @@ function createFlowInstance(options) {
48
49
  }
49
50
  if (options.events?.connectionEnded) subscribeEvent("connectionEnded", options.events.connectionEnded);
50
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);
51
54
  for (const listener of options.listeners ?? []) listen(listener.selector, listener.onChange, listener);
52
55
  return {
53
56
  ...flow,
54
57
  getSnapshot,
58
+ configure,
55
59
  listen,
56
- clientToCanvas,
60
+ clientToCanvas: clientToCanvas$1,
61
+ inputSettings,
57
62
  destroy() {
58
63
  for (const unsubscribe of cleanup) unsubscribe();
59
64
  cleanup.clear();
@@ -66,25 +71,19 @@ function createFlowInstance(options) {
66
71
  function useFlow(options) {
67
72
  return useCreateFlowInstance(options);
68
73
  }
74
+ /** @deprecated Use `useFlow(options)`. */
69
75
  function useCreateFlowInstance(options) {
70
76
  const instanceRef = useRef(null);
71
77
  const listenersRef = useRef(options.listeners);
72
78
  const eventsRef = useRef(options.events);
73
- const didMountRef = useRef(false);
79
+ const lifecycleRef = useRef(0);
74
80
  listenersRef.current = options.listeners;
75
81
  eventsRef.current = options.events;
76
82
  if (!instanceRef.current) instanceRef.current = createFlowInstance({
77
- graph: options.graph,
83
+ initialGraph: options.initialGraph,
78
84
  settings: options.settings
79
85
  });
80
86
  const flow = instanceRef.current;
81
- useEffect(() => {
82
- if (!didMountRef.current) {
83
- didMountRef.current = true;
84
- return;
85
- }
86
- flow.trigger.setGraph({ graph: options.graph });
87
- }, [flow, options.graph]);
88
87
  useEffect(() => {
89
88
  const cleanup = [];
90
89
  if (eventsRef.current?.connectionEnded) {
@@ -95,6 +94,14 @@ function useCreateFlowInstance(options) {
95
94
  const subscription = flow.on("layoutRequested", eventsRef.current.layoutRequested);
96
95
  cleanup.push(() => subscription.unsubscribe());
97
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
+ }
98
105
  for (const listener of listenersRef.current ?? []) cleanup.push(flow.listen(listener.selector, listener.onChange, listener));
99
106
  return () => {
100
107
  for (const dispose of cleanup) dispose();
@@ -102,40 +109,70 @@ function useCreateFlowInstance(options) {
102
109
  }, [
103
110
  flow,
104
111
  options.listeners,
105
- options.events
112
+ options.events,
113
+ options.onCommit
106
114
  ]);
107
- 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]);
108
126
  return flow;
109
127
  }
110
128
  function useFlowSelector(selector, options = {}) {
111
129
  const contextFlow = useOptionalFlowInstance();
112
130
  const flow = options.flow ?? contextFlow;
113
131
  if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
114
- const selectorRef = useRef(selector);
115
- const isEqualRef = useRef(options.isEqual ?? Object.is);
116
- selectorRef.current = selector;
117
- isEqualRef.current = options.isEqual ?? Object.is;
118
- const [selected, setSelected] = useState(() => selector(flow.getSnapshot()));
119
- useEffect(() => {
120
- setSelected((previous) => {
121
- const next = selectorRef.current(flow.getSnapshot());
122
- return isEqualRef.current(previous, next) ? previous : next;
123
- });
124
- return flow.listen((context) => selectorRef.current(context), (value) => {
125
- setSelected((previous) => isEqualRef.current(previous, value) ? previous : value);
126
- });
127
- }, [flow]);
128
- return selected;
132
+ return useSyncExternalStoreWithSelector(useCallback((listener) => flow.subscribe(() => listener()), [flow]), flow.getSnapshot, flow.getSnapshot, selector, options.isEqual);
129
133
  }
130
134
  function useFlowContext() {
131
135
  return useFlowSelector((context) => context);
132
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
+ }
133
170
  const entityNotifiers = /* @__PURE__ */ new WeakMap();
134
171
  function getEntityNotifier(flow) {
135
- let notifier = entityNotifiers.get(flow.store);
172
+ let notifier = entityNotifiers.get(flow.internals.store);
136
173
  if (!notifier) {
137
- notifier = createEntityChangeNotifier(flow.store);
138
- entityNotifiers.set(flow.store, notifier);
174
+ notifier = createEntityChangeNotifier(flow.internals.store, { getEdgeDependents: flow.internals.engine.getEdgeSourceDependents });
175
+ entityNotifiers.set(flow.internals.store, notifier);
139
176
  }
140
177
  return notifier;
141
178
  }
@@ -151,15 +188,23 @@ function useFlowEntity(id, selector, options = {}) {
151
188
  if (!flow) throw new Error("Flow instance is missing. Pass { flow } or render inside <FlowInstanceProvider>.");
152
189
  const selectorRef = useRef(selector);
153
190
  const isEqualRef = useRef(options.isEqual ?? Object.is);
191
+ const selectedRef = useRef(void 0);
192
+ const selectedInitializedRef = useRef(false);
154
193
  selectorRef.current = selector;
155
194
  isEqualRef.current = options.isEqual ?? Object.is;
156
- 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
+ });
157
201
  useEffect(() => {
158
202
  const update = () => {
159
- setSelected((previous) => {
160
- const next = selectorRef.current(flow.getSnapshot());
161
- return isEqualRef.current(previous, next) ? previous : next;
162
- });
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);
163
208
  };
164
209
  update();
165
210
  return getEntityNotifier(flow).subscribe(id, update);
@@ -204,7 +249,7 @@ function useFlowEdge(id, selector, isEqual) {
204
249
  function useFlowPort(id, selector, isEqual) {
205
250
  const flow = useFlowInstance();
206
251
  return useFlowSelector((context) => {
207
- const addressable = flow?.getAddressable(id);
252
+ const addressable = flow?.getEntityRef(id);
208
253
  if (!addressable || addressable.kind !== "port") throw new Error(`Flow port "${id}" was not found.`);
209
254
  const binding = {
210
255
  ...addressable.port,
@@ -243,24 +288,128 @@ function getDerivedFields(context, id, props) {
243
288
  * and applied to the flow store. This replaces hand-written gesture handlers.
244
289
  */
245
290
  function useFlowInput(flow, containerRef, options = {}) {
246
- const optionsRef = useRef(options);
247
- 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;
248
296
  useEffect(() => {
249
297
  const container = containerRef.current;
250
298
  if (!container) return;
251
- const { modes, profile } = optionsRef.current;
299
+ const activeKeymap = keymapRef.current;
252
300
  const runtime = createFlowInputRuntime({
253
- store: flow.store,
254
- engine: flow.engine,
255
- 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 } : {}
256
306
  });
257
307
  return attachInput({
258
308
  container,
259
- store: flow.store,
309
+ store: flow.internals.store,
260
310
  runtime,
261
- autoPan: (context, screenPoint) => getAutoPanDelta(context, screenPoint, flow.engine)
311
+ keyboardTarget,
312
+ autoPan: (context, screenPoint) => getAutoPanDelta(context, screenPoint, flow.internals.engine)
262
313
  });
263
- }, [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
+ }, []);
264
413
  }
265
414
  //#endregion
266
415
  //#region src/NodeContext.tsx
@@ -272,20 +421,56 @@ function useNodeRenderContext() {
272
421
  return context;
273
422
  }
274
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
275
448
  //#region src/StatelyFlow.tsx
276
449
  function stringArraysEqual(a, b) {
277
450
  if (a.length !== b.length) return false;
278
451
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
279
452
  return true;
280
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
+ }
281
466
  function useRenderableIds(instance, enabled) {
282
- return useFlowSelector((context) => computeRenderableIds(instance.engine, context, enabled), { isEqual: renderableIdsEqual });
467
+ return useFlowSelector((context) => computeRenderableIds(instance.internals.engine, context, enabled), { isEqual: renderableIdsEqual });
283
468
  }
284
469
  function useViewportTransform(ref, instance) {
285
470
  useLayoutEffect(() => {
286
471
  const el = ref.current;
287
472
  if (!el) return;
288
- return attachViewportTransform(instance.store, el);
473
+ return attachViewportTransform(instance.internals.store, el);
289
474
  }, [ref, instance]);
290
475
  }
291
476
  function StatelyFlow(props) {
@@ -296,11 +481,12 @@ function StatelyFlow(props) {
296
481
  return /* @__PURE__ */ jsx(OwnedStatelyFlow, { ...props });
297
482
  }
298
483
  function OwnedStatelyFlow(props) {
299
- const flow = useCreateFlowInstance({
300
- graph: props.graph,
484
+ const flow = useFlow({
485
+ initialGraph: props.initialGraph,
301
486
  settings: props.settings,
302
487
  listeners: props.listeners,
303
- events: props.events
488
+ events: props.events,
489
+ onCommit: props.onCommit
304
490
  });
305
491
  return /* @__PURE__ */ jsx(StatelyFlowView, {
306
492
  ...props,
@@ -309,61 +495,155 @@ function OwnedStatelyFlow(props) {
309
495
  }
310
496
  function StatelyFlowView(props) {
311
497
  const containerRef = useRef(null);
498
+ const setContainerRef = useCallback((element) => {
499
+ containerRef.current = element;
500
+ assignRef(props.rootRef, element);
501
+ }, [props.rootRef]);
312
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]);
313
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 ?? {};
314
519
  useLayoutEffect(() => {
315
520
  const element = containerRef.current;
316
521
  if (!element) return;
317
- return attachContainer(props.flow.store, element);
522
+ return attachContainer(props.flow.internals.store, element);
318
523
  }, [props.flow]);
319
524
  useFlowInput(props.flow, containerRef, {
320
525
  modes: props.inputModes,
321
- profile: props.settings?.store?.profile
526
+ profile: props.settings?.store?.profile,
527
+ keyboardTarget: props.keyboardTarget,
528
+ keymap: props.settings?.keymap
322
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
+ ]);
323
549
  return /* @__PURE__ */ jsx(FlowInstanceProvider, {
324
550
  flow: props.flow,
325
- children: /* @__PURE__ */ jsxs("div", {
326
- ref: containerRef,
327
- className: props.className,
328
- style: {
329
- position: "relative",
330
- overflow: "hidden",
331
- width: "100%",
332
- height: "100%",
333
- minHeight: 320,
334
- touchAction: "none",
335
- ...props.style
336
- },
337
- children: [
338
- props.background,
339
- /* @__PURE__ */ jsxs("div", {
340
- ref: viewportRef,
341
- "data-flow-viewport": true,
342
- style: viewportStyle(),
343
- children: [
344
- /* @__PURE__ */ jsx(EdgeLayer, {
345
- renderEdge: props.renderEdge,
346
- onlyRenderVisible: props.onlyRenderVisible
347
- }),
348
- /* @__PURE__ */ jsx(NodeLayer, {
349
- renderNode: props.renderNode,
350
- onlyRenderVisible: props.onlyRenderVisible
351
- }),
352
- /* @__PURE__ */ jsx(SnapLinesLayer, {}),
353
- /* @__PURE__ */ jsx(EdgeLabelLayer, { onlyRenderVisible: props.onlyRenderVisible })
354
- ]
355
- }),
356
- /* @__PURE__ */ jsx("div", {
357
- "data-flow-overlay": true,
358
- 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
+ })
359
635
  })
360
- ]
636
+ })
361
637
  })
362
638
  });
363
639
  }
640
+ function useActiveProjections(flow, projections) {
641
+ const flowProjections = useSyncExternalStore(flow.projections.subscribe, flow.projections.get, flow.projections.get);
642
+ return projections ?? flowProjections;
643
+ }
364
644
  function SnapLinesLayer() {
365
645
  const instance = useFlowInstance();
366
- const snapResult = useFlowSelector(() => instance.engine.getSnapResult());
646
+ const snapResult = useFlowSelector(() => instance.internals.engine.getSnapResult());
367
647
  const viewport = useFlowSelector((context) => context.viewport);
368
648
  const viewportSize = useFlowSelector((context) => context.viewportSize);
369
649
  if (!snapResult) return null;
@@ -373,11 +653,16 @@ function SnapLinesLayer() {
373
653
  width: viewportSize.width / viewport.zoom,
374
654
  height: viewportSize.height / viewport.zoom
375
655
  };
376
- const strokeWidth = 1.5 / viewport.zoom;
656
+ const strokeWidth = 1 / viewport.zoom;
377
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))";
378
662
  return /* @__PURE__ */ jsx("svg", {
379
663
  "aria-hidden": "true",
380
664
  "data-flow-canvas-layer": true,
665
+ "data-flow-snap-guides": "",
381
666
  style: {
382
667
  position: "absolute",
383
668
  inset: 0,
@@ -387,89 +672,128 @@ function SnapLinesLayer() {
387
672
  pointerEvents: "none"
388
673
  },
389
674
  children: /* @__PURE__ */ jsxs("g", { children: [snapResult.snapLines.map((line, index) => line.axis === "x" ? /* @__PURE__ */ jsx("line", {
675
+ "data-flow-snap-line": "x",
390
676
  x1: line.value,
391
677
  y1: visibleBounds.y,
392
678
  x2: line.value,
393
679
  y2: visibleBounds.y + visibleBounds.height,
394
- stroke: "#ef4444",
680
+ stroke: snapColor,
395
681
  strokeWidth,
396
- strokeDasharray: `${6 / viewport.zoom} ${4 / viewport.zoom}`
682
+ strokeDasharray: `${4 / viewport.zoom}`
397
683
  }, `snap-x-${index}`) : /* @__PURE__ */ jsx("line", {
684
+ "data-flow-snap-line": "y",
398
685
  x1: visibleBounds.x,
399
686
  y1: line.value,
400
687
  x2: visibleBounds.x + visibleBounds.width,
401
688
  y2: line.value,
402
- stroke: "#ef4444",
689
+ stroke: snapColor,
403
690
  strokeWidth,
404
- strokeDasharray: `${6 / viewport.zoom} ${4 / viewport.zoom}`
691
+ strokeDasharray: `${4 / viewport.zoom}`
405
692
  }, `snap-y-${index}`)), snapResult.spacingGuides?.flatMap((guide, guideIndex) => guide.ranges.flatMap((range, rangeIndex) => {
406
693
  const key = `spacing-${guideIndex}-${rangeIndex}`;
407
694
  if (guide.axis === "x") return [
408
695
  /* @__PURE__ */ jsx("line", {
696
+ "data-flow-spacing-guide": "x",
409
697
  x1: range.start,
410
698
  y1: guide.position,
411
699
  x2: range.end,
412
700
  y2: guide.position,
413
- stroke: "#ef4444",
701
+ stroke: spacingColor,
414
702
  strokeWidth
415
703
  }, `${key}-line`),
416
704
  /* @__PURE__ */ jsx("line", {
417
705
  x1: range.start,
418
- y1: guide.position - capSize,
706
+ y1: guide.position,
419
707
  x2: range.start,
420
- y2: guide.position + capSize,
421
- stroke: "#ef4444",
422
- strokeWidth,
708
+ y2: guide.position,
709
+ stroke: spacingColor,
710
+ strokeWidth: capSize,
423
711
  strokeLinecap: "round"
424
712
  }, `${key}-start`),
425
713
  /* @__PURE__ */ jsx("line", {
426
714
  x1: range.end,
427
- y1: guide.position - capSize,
715
+ y1: guide.position,
428
716
  x2: range.end,
429
- y2: guide.position + capSize,
430
- stroke: "#ef4444",
431
- strokeWidth,
717
+ y2: guide.position,
718
+ stroke: spacingColor,
719
+ strokeWidth: capSize,
432
720
  strokeLinecap: "round"
433
- }, `${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`)
434
731
  ];
435
732
  return [
436
733
  /* @__PURE__ */ jsx("line", {
734
+ "data-flow-spacing-guide": "y",
437
735
  x1: guide.position,
438
736
  y1: range.start,
439
737
  x2: guide.position,
440
738
  y2: range.end,
441
- stroke: "#ef4444",
739
+ stroke: spacingColor,
442
740
  strokeWidth
443
741
  }, `${key}-line`),
444
742
  /* @__PURE__ */ jsx("line", {
445
- x1: guide.position - capSize,
743
+ x1: guide.position,
446
744
  y1: range.start,
447
- x2: guide.position + capSize,
745
+ x2: guide.position,
448
746
  y2: range.start,
449
- stroke: "#ef4444",
450
- strokeWidth,
747
+ stroke: spacingColor,
748
+ strokeWidth: capSize,
451
749
  strokeLinecap: "round"
452
750
  }, `${key}-start`),
453
751
  /* @__PURE__ */ jsx("line", {
454
- x1: guide.position - capSize,
752
+ x1: guide.position,
455
753
  y1: range.end,
456
- x2: guide.position + capSize,
754
+ x2: guide.position,
457
755
  y2: range.end,
458
- stroke: "#ef4444",
459
- strokeWidth,
756
+ stroke: spacingColor,
757
+ strokeWidth: capSize,
460
758
  strokeLinecap: "round"
461
- }, `${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`)
462
770
  ];
463
771
  }))] })
464
772
  });
465
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
+ }
466
789
  function EdgeLayer(props) {
467
790
  const instance = useFlowInstance();
468
791
  const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
469
- 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 });
470
793
  const renderEdge = props.renderEdge;
471
794
  return /* @__PURE__ */ jsx("svg", {
472
795
  "aria-label": "edges",
796
+ className: "stately-flow__edges",
473
797
  "data-flow-canvas-layer": true,
474
798
  style: {
475
799
  position: "absolute",
@@ -477,34 +801,77 @@ function EdgeLayer(props) {
477
801
  width: "100%",
478
802
  height: "100%",
479
803
  overflow: "visible",
480
- pointerEvents: "auto"
804
+ pointerEvents: "none"
481
805
  },
482
806
  children: /* @__PURE__ */ jsxs("g", { children: [edgeIds.map((id) => {
483
807
  if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
484
808
  return /* @__PURE__ */ jsx(EdgeView, {
485
809
  flow: instance,
810
+ geometryObserver: props.geometryObserver,
486
811
  id,
812
+ projections: props.projections,
487
813
  renderEdge
488
814
  }, id);
489
815
  }), /* @__PURE__ */ jsx(ConnectionPreviewEdge, {
490
816
  flow: instance,
491
- renderEdge
817
+ renderEdge,
818
+ renderConnectionLine: props.renderConnectionLine
492
819
  })] })
493
820
  });
494
821
  }
495
822
  const EdgeView = memo(function EdgeView(props) {
496
- 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,
497
826
  edge: props.flow.getEdge(props.id) ?? null,
498
827
  pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
828
+ geometryActive: context.canvasState === "dragging" || context.canvasState === "resizing" || context.layoutAnimation.status === "running",
499
829
  selected: context.selection.has(props.id),
830
+ focused: context.focusedEntityId === props.id,
500
831
  hovered: context.hoveredEntityIds.has(props.id),
501
- highlights: props.flow.engine.getHighlights(props.id)
502
- }), { 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
+ ]);
503
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
+ };
504
865
  if (props.renderEdge) return /* @__PURE__ */ jsxs("g", {
866
+ ref,
867
+ ...focusProps,
505
868
  "data-entity-id": props.id,
506
869
  "data-flow-selected": selected ? "true" : void 0,
507
- 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
+ },
508
875
  children: [/* @__PURE__ */ jsx(EdgeHitTarget, {
509
876
  flow: props.flow,
510
877
  edge,
@@ -514,19 +881,28 @@ const EdgeView = memo(function EdgeView(props) {
514
881
  flow: props.flow,
515
882
  edge,
516
883
  pathData,
884
+ attrs,
517
885
  selected,
518
886
  hovered,
519
887
  highlights
520
888
  })]
521
889
  });
522
890
  return /* @__PURE__ */ jsx("g", {
891
+ ref,
892
+ ...focusProps,
523
893
  "data-entity-id": props.id,
524
894
  "data-flow-selected": selected ? "true" : void 0,
525
- 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
+ },
526
901
  children: /* @__PURE__ */ jsx(DefaultEdge, {
527
902
  flow: props.flow,
528
903
  edge,
529
904
  pathData,
905
+ attrs,
530
906
  selected
531
907
  })
532
908
  });
@@ -534,17 +910,22 @@ const EdgeView = memo(function EdgeView(props) {
534
910
  /** The in-progress connection gesture's preview edge (renders only mid-gesture). */
535
911
  function ConnectionPreviewEdge(props) {
536
912
  const connection = useFlowSelector((context) => context.connection);
537
- if (!connection?.active) return null;
913
+ if (!connection?.active || connection.preview === false) return null;
538
914
  const previewGraph = createConnectionPreviewGraph(props.flow.getSnapshot().graph, connection);
539
915
  const pathData = props.flow.getAllEdgePaths({ graph: previewGraph }).get(CONNECTION_PREVIEW_EDGE_ID);
540
916
  const edge = previewGraph.edges.find((candidate) => candidate.id === CONNECTION_PREVIEW_EDGE_ID);
541
917
  if (!edge || !pathData) return null;
542
918
  return /* @__PURE__ */ jsx("g", {
543
919
  style: { pointerEvents: "none" },
544
- children: props.renderEdge ? props.renderEdge({
920
+ children: props.renderConnectionLine ? props.renderConnectionLine({
921
+ connection,
922
+ pathData,
923
+ isValid: connection.isValid
924
+ }) : props.renderEdge ? props.renderEdge({
545
925
  flow: props.flow,
546
926
  edge,
547
927
  pathData,
928
+ attrs: {},
548
929
  selected: false,
549
930
  hovered: false,
550
931
  highlights: []
@@ -552,22 +933,26 @@ function ConnectionPreviewEdge(props) {
552
933
  flow: props.flow,
553
934
  edge,
554
935
  pathData,
936
+ attrs: {},
555
937
  selected: false,
556
938
  preview: true
557
939
  })
558
940
  });
559
941
  }
560
942
  function DefaultEdge(props) {
561
- return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
943
+ return /* @__PURE__ */ jsxs(Fragment, { children: [props.pathData.segments.map((segment, index) => {
562
944
  const d = pathToSVG(segment);
563
945
  return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("path", {
564
946
  d,
947
+ "data-flow-edge-segment": index,
565
948
  fill: "none",
566
- 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"),
567
950
  strokeDasharray: props.preview ? "6 6" : void 0,
568
- strokeWidth: props.selected ? 3 : 2
951
+ strokeWidth: props.attrs.strokeWidth ?? (props.selected ? 3 : 2),
952
+ style: props.attrs.style
569
953
  }), props.preview ? null : /* @__PURE__ */ jsx("path", {
570
954
  d,
955
+ "data-flow-edge-segment": index,
571
956
  "data-entity-id": props.edge.id,
572
957
  fill: "none",
573
958
  stroke: "#000",
@@ -575,12 +960,18 @@ function DefaultEdge(props) {
575
960
  strokeWidth: 18,
576
961
  pointerEvents: "stroke"
577
962
  })] }, index);
578
- }) });
963
+ }), props.preview ? null : /* @__PURE__ */ jsx(EdgeEndpointHandles, {
964
+ flow: props.flow,
965
+ edge: props.edge,
966
+ pathData: props.pathData,
967
+ selected: props.selected
968
+ })] });
579
969
  }
580
970
  function EdgeHitTarget(props) {
581
- return /* @__PURE__ */ jsx(Fragment, { children: props.pathData.segments.map((segment, index) => {
971
+ return /* @__PURE__ */ jsxs(Fragment, { children: [props.pathData.segments.map((segment, index) => {
582
972
  return /* @__PURE__ */ jsx("path", {
583
973
  d: pathToSVG(segment),
974
+ "data-flow-edge-segment": index,
584
975
  "data-entity-id": props.edge.id,
585
976
  fill: "none",
586
977
  stroke: "#000",
@@ -588,88 +979,225 @@ function EdgeHitTarget(props) {
588
979
  strokeWidth: 18,
589
980
  pointerEvents: "stroke"
590
981
  }, index);
591
- }) });
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)) });
592
1015
  }
593
1016
  function EdgeLabelLayer(props) {
594
1017
  const instance = useFlowInstance();
595
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 });
596
1022
  return /* @__PURE__ */ jsx("div", {
597
1023
  "aria-label": "edge labels",
1024
+ className: "stately-flow__edge-labels",
598
1025
  style: {
599
1026
  position: "absolute",
600
1027
  inset: 0,
601
1028
  pointerEvents: "none"
602
1029
  },
603
- 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) => {
604
1040
  if (renderableIds && !renderableIds.edgeIds.has(id)) return null;
605
1041
  return /* @__PURE__ */ jsx(EdgeLabelItem, {
606
1042
  flow: instance,
607
- id
1043
+ geometryObserver: props.geometryObserver,
1044
+ id,
1045
+ projections: props.projections,
1046
+ renderEdgeLabel
608
1047
  }, id);
609
1048
  })
610
1049
  });
611
1050
  }
612
1051
  const EdgeLabelItem = memo(function EdgeLabelItem(props) {
613
- 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,
614
1054
  edge: props.flow.getEdge(props.id) ?? null,
615
1055
  pathData: props.flow.getAllEdgePaths().get(props.id) ?? null,
616
- selected: context.selection.has(props.id)
617
- }), { isEqual: (a, b) => a.edge === b.edge && a.pathData === b.pathData && a.selected === b.selected });
618
- 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;
619
1074
  return /* @__PURE__ */ jsx(EdgeLabelView, {
620
1075
  flow: props.flow,
1076
+ geometryObserver: props.geometryObserver,
621
1077
  edge,
622
- bounds: pathData.labelBounds,
623
- selected
1078
+ attrs,
1079
+ bounds,
1080
+ pathData,
1081
+ selected,
1082
+ dragging,
1083
+ hovered,
1084
+ highlights,
1085
+ projections: props.projections,
1086
+ renderEdgeLabel: props.renderEdgeLabel
624
1087
  });
625
1088
  });
626
1089
  function EdgeLabelView(props) {
627
1090
  const ref = useRef(null);
628
- const observer = useMemo(() => createMeasurementObserver(props.flow.store), [props.flow.store]);
1091
+ const observer = useMeasurementObserver();
629
1092
  useLayoutEffect(() => {
630
1093
  const element = ref.current;
631
1094
  if (!element) return;
632
1095
  observer.observe(element, { edgeId: props.edge.id });
633
- return () => observer.disconnect();
1096
+ return () => observer.unobserve(element);
634
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
+ ]);
635
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
+ };
636
1148
  return /* @__PURE__ */ jsx("div", {
637
1149
  ref,
638
1150
  "data-entity-id": props.edge.id,
639
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),
640
1155
  style: {
641
1156
  position: "absolute",
642
- left: props.bounds.x,
643
- top: props.bounds.y,
644
- width: props.bounds.width,
645
- 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,
646
1162
  boxSizing: "border-box",
647
- padding: "4px 8px",
648
- border: `1px solid ${props.selected ? "#2563eb" : "#cbd5e1"}`,
649
- borderRadius: 999,
650
- background: "#fff",
651
- color: "#334155",
652
- display: "grid",
653
- placeItems: "center",
654
- cursor: "grab",
655
- font: "600 12px/1 system-ui, sans-serif",
1163
+ ...defaultStyle,
656
1164
  pointerEvents: "auto",
657
- userSelect: "none"
1165
+ userSelect: "none",
1166
+ opacity: props.attrs.opacity,
1167
+ ...props.attrs.style
658
1168
  },
659
- children: String(text ?? props.edge.id)
1169
+ children: props.renderEdgeLabel ? customLabel : label
660
1170
  });
661
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
+ }
662
1184
  function NodeLayer(props) {
663
1185
  const instance = useFlowInstance();
664
1186
  const renderableIds = useRenderableIds(instance, props.onlyRenderVisible);
665
- const nodeIds = useFlowSelector((context) => {
666
- const depths = nodeDepths(context.graph.nodes);
667
- return context.graph.nodes.map((node) => node.id).sort((a, b) => (depths.get(a) ?? 0) - (depths.get(b) ?? 0));
668
- }, { isEqual: stringArraysEqual });
669
- const observer = useMemo(() => createMeasurementObserver(instance.store), [instance]);
670
- 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();
671
1198
  return /* @__PURE__ */ jsx("div", {
672
1199
  "aria-label": "nodes",
1200
+ className: "stately-flow__nodes",
673
1201
  "data-flow-canvas-layer": true,
674
1202
  style: {
675
1203
  position: "absolute",
@@ -677,35 +1205,100 @@ function NodeLayer(props) {
677
1205
  transformOrigin: "0 0",
678
1206
  pointerEvents: "none"
679
1207
  },
680
- children: nodeIds.map((id) => {
1208
+ children: nodePaintEntries.map(({ id, orderIndex }) => {
681
1209
  if (renderableIds && !renderableIds.nodeIds.has(id)) return null;
682
1210
  return /* @__PURE__ */ jsx(NodeView, {
683
1211
  flow: instance,
1212
+ geometryObserver: props.geometryObserver,
684
1213
  id,
1214
+ orderIndex,
685
1215
  observer,
1216
+ projections: props.projections,
686
1217
  renderNode: props.renderNode
687
1218
  }, id);
688
1219
  })
689
1220
  });
690
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
+ }
691
1250
  function rectsShallowEqual(a, b) {
692
1251
  return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
693
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
+ }
694
1280
  const NodeView = memo(function NodeView(props) {
695
1281
  const ref = useRef(null);
696
- 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) => {
697
1283
  const current = props.flow.getNode(props.id) ?? null;
698
1284
  return {
1285
+ graph: context.graph,
699
1286
  node: current,
700
- 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,
701
1289
  selected: context.selection.has(props.id),
702
- dragging: context.draggedEntityId === props.id,
1290
+ dragging: context.canvasState === "dragging" && context.selection.has(props.id),
703
1291
  focused: context.focusedEntityId === props.id,
704
1292
  hovered: context.hoveredEntityIds.has(props.id),
705
- highlights: props.flow.engine.getHighlights(props.id),
706
- 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)
707
1295
  };
708
- }, { 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);
709
1302
  useLayoutEffect(() => {
710
1303
  const element = ref.current;
711
1304
  if (!element) return;
@@ -719,7 +1312,30 @@ const NodeView = memo(function NodeView(props) {
719
1312
  props.flow,
720
1313
  hidden
721
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
+ ]);
722
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;
723
1339
  return /* @__PURE__ */ jsx(NodeRenderContextProvider, {
724
1340
  value: {
725
1341
  node,
@@ -729,26 +1345,31 @@ const NodeView = memo(function NodeView(props) {
729
1345
  ref,
730
1346
  "data-entity-id": props.id,
731
1347
  "data-flow-selected": selected ? "true" : void 0,
1348
+ className: classNames$1("stately-flow__node", attrs.className),
732
1349
  role: "group",
733
1350
  "aria-roledescription": "flow node",
734
- "aria-label": props.flow.engine.getAriaLabel(props.id) + (selected ? ", selected" : ""),
735
- tabIndex: 0,
736
- 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 }),
737
1355
  style: {
738
1356
  ...nodePositionStyle(bounds, {
739
- autoSize: node.autoSize,
1357
+ autoSize: contentOwnsSize,
740
1358
  dragging
741
1359
  }),
742
1360
  cursor: dragging ? "grabbing" : "grab",
743
1361
  pointerEvents: "auto",
744
1362
  userSelect: "none",
1363
+ zIndex: props.orderIndex,
745
1364
  outline: focused ? "2px solid #2563eb" : "none",
746
- outlineOffset: 2
1365
+ outlineOffset: 2,
1366
+ opacity: attrs.opacity
747
1367
  },
748
1368
  children: props.renderNode ? props.renderNode({
749
1369
  flow: props.flow,
750
1370
  node,
751
1371
  bounds,
1372
+ attrs,
752
1373
  selected,
753
1374
  dragging,
754
1375
  hovered,
@@ -757,6 +1378,7 @@ const NodeView = memo(function NodeView(props) {
757
1378
  flow: props.flow,
758
1379
  node,
759
1380
  bounds,
1381
+ attrs,
760
1382
  selected,
761
1383
  dragging,
762
1384
  hovered,
@@ -765,6 +1387,7 @@ const NodeView = memo(function NodeView(props) {
765
1387
  flow: props.flow,
766
1388
  node,
767
1389
  bounds,
1390
+ attrs,
768
1391
  selected,
769
1392
  dragging,
770
1393
  hovered,
@@ -779,8 +1402,10 @@ const NodeView = memo(function NodeView(props) {
779
1402
  * `engine.getGroupGeometry`; custom renderers can do the same.
780
1403
  */
781
1404
  function DefaultGroupNode(props) {
782
- const geometry = props.flow.engine.getGroupGeometry(props.node.id);
1405
+ const geometry = props.flow.internals.engine.getGroupGeometry(props.node.id);
783
1406
  const label = props.node.data?.label;
1407
+ const badges = decorationsToArray(props.attrs.badge);
1408
+ const title = decorationTitle(props.attrs.tooltip);
784
1409
  if (!geometry) return /* @__PURE__ */ jsx(DefaultNode, { ...props });
785
1410
  const relative = (rect) => ({
786
1411
  left: rect.x - props.bounds.x,
@@ -793,13 +1418,14 @@ function DefaultGroupNode(props) {
793
1418
  position: "absolute",
794
1419
  ...relative(geometry.childArea),
795
1420
  boxSizing: "border-box",
796
- border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : "#cbd5e1"}`,
1421
+ border: `2px dashed ${dropTarget ? "#16a34a" : props.selected ? "#93c5fd" : props.attrs.color ?? "#cbd5e1"}`,
797
1422
  ...dropTarget ? { background: "rgba(22, 163, 74, 0.08)" } : null,
798
1423
  borderRadius: 10,
799
1424
  background: "rgba(148, 163, 184, 0.07)",
800
1425
  pointerEvents: "none"
801
- } }), /* @__PURE__ */ jsx("div", {
1426
+ } }), /* @__PURE__ */ jsxs("div", {
802
1427
  "data-flow-drag-handle": true,
1428
+ title,
803
1429
  style: {
804
1430
  position: "absolute",
805
1431
  ...relative(geometry.contentBounds),
@@ -807,37 +1433,65 @@ function DefaultGroupNode(props) {
807
1433
  display: "flex",
808
1434
  alignItems: "center",
809
1435
  padding: "0 12px",
810
- border: `2px solid ${props.selected ? "#2563eb" : "#94a3b8"}`,
1436
+ border: `2px solid ${props.selected ? "#2563eb" : props.attrs.color ?? "#94a3b8"}`,
811
1437
  borderRadius: 8,
812
1438
  background: props.selected ? "#eff6ff" : "#f8fafc",
813
1439
  color: "#0f172a",
814
1440
  font: "600 13px/1.2 system-ui, sans-serif",
815
1441
  cursor: props.dragging ? "grabbing" : "grab",
816
- userSelect: "none"
1442
+ userSelect: "none",
1443
+ ...props.attrs.style
817
1444
  },
818
- 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]
819
1449
  })] });
820
1450
  }
821
1451
  function DefaultNode(props) {
822
1452
  const label = props.node.data?.label;
823
- return /* @__PURE__ */ jsx("div", {
1453
+ const badges = decorationsToArray(props.attrs.badge);
1454
+ return /* @__PURE__ */ jsxs("div", {
1455
+ title: decorationTitle(props.attrs.tooltip),
824
1456
  style: {
1457
+ position: "relative",
825
1458
  display: "grid",
826
1459
  placeItems: "center",
827
1460
  width: "100%",
828
1461
  height: "100%",
829
1462
  boxSizing: "border-box",
830
1463
  padding: "10px 14px",
831
- border: `2px solid ${props.selected ? "#2563eb" : props.hovered ? "#94a3b8" : "#cbd5e1"}`,
1464
+ border: `2px solid ${props.selected ? "#2563eb" : props.attrs.color ?? (props.hovered ? "#94a3b8" : "#cbd5e1")}`,
832
1465
  borderRadius: 8,
833
1466
  background: props.selected ? "#eff6ff" : "#ffffff",
834
1467
  color: "#0f172a",
835
1468
  font: "600 14px/1.2 system-ui, sans-serif",
836
- 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
837
1471
  },
838
- 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]
839
1476
  });
840
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
+ }
841
1495
  //#endregion
842
1496
  //#region src/FlowToolbar.tsx
843
1497
  /** Border-box size of an element, kept current via ResizeObserver. */
@@ -877,7 +1531,7 @@ function useFlowToolbar(options = {}) {
877
1531
  const anchor = useFlowSelector((context) => {
878
1532
  const ids = options.ids ?? [...context.selection];
879
1533
  if (ids.length === 0) return null;
880
- const canvasRect = flow.engine.getToolbarAnchor(ids);
1534
+ const canvasRect = flow.internals.engine.getToolbarAnchor(ids);
881
1535
  if (!canvasRect) return null;
882
1536
  const zoom = context.viewport.zoom;
883
1537
  return {
@@ -972,7 +1626,7 @@ function MiniMap(props) {
972
1626
  x: event.clientX - rect.left,
973
1627
  y: event.clientY - rect.top
974
1628
  });
975
- flow.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
1629
+ flow.internals.store.trigger.setViewport(viewportCenteredOn(canvasPoint, viewportSize, viewport.zoom));
976
1630
  };
977
1631
  return /* @__PURE__ */ jsxs("svg", {
978
1632
  "aria-label": "minimap",
@@ -1006,7 +1660,7 @@ function MiniMap(props) {
1006
1660
  ...props.style
1007
1661
  },
1008
1662
  children: [nodes.map((node) => {
1009
- const rect = projection.rectToMinimap(flow.engine.resolvedBounds(node.id));
1663
+ const rect = projection.rectToMinimap(flow.internals.engine.visualBounds(node.id));
1010
1664
  return props.renderNode?.(node, rect) ?? /* @__PURE__ */ jsx("rect", {
1011
1665
  x: rect.x,
1012
1666
  y: rect.y,
@@ -1027,6 +1681,60 @@ function MiniMap(props) {
1027
1681
  });
1028
1682
  }
1029
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
1030
1738
  //#region src/Controls.tsx
1031
1739
  const buttonStyle = {
1032
1740
  width: 28,
@@ -1046,18 +1754,15 @@ const buttonStyle = {
1046
1754
  */
1047
1755
  function Controls(props) {
1048
1756
  const flow = useFlowInstance();
1049
- return /* @__PURE__ */ jsxs("div", {
1757
+ return /* @__PURE__ */ jsxs(Panel, {
1050
1758
  role: "toolbar",
1051
1759
  "aria-label": "canvas controls",
1760
+ position: props.position ?? "bottom-left",
1052
1761
  className: props.className,
1053
1762
  style: {
1054
- position: "absolute",
1055
- left: 12,
1056
- bottom: 12,
1057
1763
  display: "flex",
1058
1764
  flexDirection: "column",
1059
1765
  gap: 4,
1060
- pointerEvents: "auto",
1061
1766
  ...props.style
1062
1767
  },
1063
1768
  children: [
@@ -1065,21 +1770,21 @@ function Controls(props) {
1065
1770
  type: "button",
1066
1771
  "aria-label": "zoom in",
1067
1772
  style: buttonStyle,
1068
- onClick: () => flow.trigger.zoomBy({ factor: 1.2 }),
1773
+ onClick: () => flow.internals.store.trigger.zoomBy({ factor: 1.2 }),
1069
1774
  children: "+"
1070
1775
  }),
1071
1776
  /* @__PURE__ */ jsx("button", {
1072
1777
  type: "button",
1073
1778
  "aria-label": "zoom out",
1074
1779
  style: buttonStyle,
1075
- onClick: () => flow.trigger.zoomBy({ factor: 1 / 1.2 }),
1780
+ onClick: () => flow.internals.store.trigger.zoomBy({ factor: 1 / 1.2 }),
1076
1781
  children: "−"
1077
1782
  }),
1078
1783
  /* @__PURE__ */ jsx("button", {
1079
1784
  type: "button",
1080
1785
  "aria-label": "fit view",
1081
1786
  style: buttonStyle,
1082
- onClick: () => flow.trigger.fitView({ padding: .1 }),
1787
+ onClick: () => flow.internals.store.trigger.fitView({ padding: .1 }),
1083
1788
  children: "⛶"
1084
1789
  }),
1085
1790
  props.children
@@ -1089,22 +1794,84 @@ function Controls(props) {
1089
1794
  //#endregion
1090
1795
  //#region src/EntityAnchor.tsx
1091
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) {
1092
1802
  const viewport = useFlowSelector((context) => context.viewport);
1093
1803
  const transform = getAnchorTransform(props.bounds, viewport, props.anchor);
1804
+ const { bounds: _bounds, anchor: _anchor, inverseScale, isVisible: _isVisible, children, className, style, ...divProps } = props;
1094
1805
  return /* @__PURE__ */ jsx("div", {
1095
- className: props.className,
1806
+ ...divProps,
1807
+ "data-flow-entity-anchor": "",
1808
+ className: classNames("stately-flow__entity-anchor", className),
1096
1809
  style: {
1097
1810
  position: "absolute",
1098
1811
  left: transform.x,
1099
1812
  top: transform.y,
1100
- transform: props.inverseScale ? `translate(-50%, -50%) scale(${transform.scale})` : "translate(-50%, -50%)",
1813
+ transform: inverseScale ? `translate(-50%, -50%) scale(${transform.scale})` : "translate(-50%, -50%)",
1101
1814
  transformOrigin: "center",
1102
1815
  pointerEvents: "auto",
1103
- ...props.style
1816
+ ...style
1104
1817
  },
1105
- 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
1106
1838
  });
1107
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
+ }
1108
1875
  //#endregion
1109
1876
  //#region src/Port.tsx
1110
1877
  /**
@@ -1122,11 +1889,11 @@ function Port(props) {
1122
1889
  const { node } = useNodeRenderContext();
1123
1890
  const ref = useRef(null);
1124
1891
  const observerRef = useRef(null);
1892
+ const observer = useMeasurementObserver();
1125
1893
  const measured = props.measured ?? true;
1126
1894
  useLayoutEffect(() => {
1127
1895
  const element = ref.current;
1128
1896
  if (!element || !measured) return;
1129
- const observer = createMeasurementObserver(flow.store);
1130
1897
  observerRef.current = observer;
1131
1898
  observer.observe(element, {
1132
1899
  nodeId: node.id,
@@ -1134,10 +1901,10 @@ function Port(props) {
1134
1901
  });
1135
1902
  return () => {
1136
1903
  observerRef.current = null;
1137
- observer.disconnect();
1904
+ observer.unobserve(element);
1138
1905
  };
1139
1906
  }, [
1140
- flow.store,
1907
+ observer,
1141
1908
  node.id,
1142
1909
  props.name,
1143
1910
  measured
@@ -1165,6 +1932,128 @@ function Port(props) {
1165
1932
  });
1166
1933
  }
1167
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
1168
2057
  //#region src/ColorMode.tsx
1169
2058
  function FlowColorModeScope(props) {
1170
2059
  const { colorMode, resolvedColorMode } = useFlowColorMode();
@@ -1209,4 +2098,4 @@ function useApplyFlowColorMode(element) {
1209
2098
  ]);
1210
2099
  }
1211
2100
  //#endregion
1212
- 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 };