@foblex/flow 19.1.3 → 19.1.5

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/AI.md CHANGED
@@ -5,12 +5,13 @@ Use this file as a strict control layer for code generation. Prefer verified pac
5
5
  ## What This Library Is
6
6
 
7
7
  `@foblex/flow` is an Angular-native library for building node-based editors, workflow builders, and interactive graph UIs.
8
- It provides rendering, connectors, interactions, selection, zoom, and connection drawing. By default your app owns the graph data; the optional `withFlowState()` feature can own the data bookkeeping and undo/redo for an editor.
8
+ It provides rendering, connectors, interactions, selection, zoom, and connection drawing. By default your app owns graph records; the optional `withFlowState()` feature maintains typed records and undo/redo that your app explicitly loads and renders.
9
9
 
10
10
  ## Core Mental Model
11
11
 
12
- - **Classic mode (default):** your app owns nodes, groups, connections, validation and persistence. User actions emit events; your handlers update app state; Angular rerenders.
13
- - **Managed mode (opt-in):** `provideFFlow(withFlowState())` provides `FFlowState`; supported completed gestures update its signals and history automatically. Your app still owns domain fields, validation policy and persistence.
12
+ - **Classic mode (default):** your app owns nodes, groups, and connections. User actions emit events; your handlers validate them, update app state, and Angular rerenders.
13
+ - **Managed mode (opt-in):** `provideFFlow(withFlowState())` provides `FFlowState`; supported completed gestures update its typed records and history automatically.
14
+ - **Application responsibility in both modes:** your app defines domain fields and owns validation policy, permissions, persistence, backend integration, and business meaning.
14
15
  - Both modes render records through normal Angular templates. `withFlowState()` is optional and does not change classic event behavior when absent.
15
16
 
16
17
  ## Minimal Working Setup
@@ -136,11 +137,12 @@ export class Editor {
136
137
 
137
138
  - Bind `state.nodes()`, `state.groups()` and `state.connections()` with `@for`; bind canvas `[position]` and `[scale]` to `state.transform()` when viewport undo/redo is enabled.
138
139
  - Supported v1 gestures: create/reassign connection, move nodes/groups, delete selection, external-item creation, optional drop-to-group, selection, and canvas pan/zoom.
139
- - Rotation, connection waypoint editing, and user resize are not captured by managed state in v1.
140
+ - Node and group geometry emitted through `fNodeSizeChange` / `fGroupSizeChange`, including user resize and auto-expand or auto-fit updates, changes the managed records without creating a separate history step. Arbitrary content measurement that emits neither output is not written to the store. Rotation and connection waypoint editing are not captured by managed state in v1.
140
141
  - `state.changes()` increments once when a standalone mutation or outer batch settles. A drag can emit selection at start and move/drop at end while remaining one history step and one `changes()` increment.
141
- - Use `state.snapshot()` for persistence; `load()` replaces data and resets history.
142
+ - Use `state.snapshot()` for persistence; it includes graph records, selection, and viewport transform. `load()` restores them and resets history. Snapshots created before selection persistence remain valid and load with an empty selection.
142
143
  - `canvasTransformDebounce` defaults to `350ms`. It waits for wheel/pinch zoom events to settle, then records the current canvas transform as one change and one undo item. Setting it to `0` disables batching: every emitted `fCanvasChange` is applied immediately, increments `state.changes()`, and normally creates a separate undo item. Keep the canvas `[debounceTime]` unset when managed state owns viewport history; do not stack canvas and State debounce layers.
143
144
  - For initial or other application-driven viewport positioning, suppress the event at the canvas helper: `canvas.resetScaleAndCenter(false, false)`. The second `false` means `emitCanvasChange = false`, so managed history is untouched.
145
+ - `canvas.centerGroupOrNode(id)` preserves the current zoom. Use `canvas.resetScaleAndCenterGroupOrNode(id)` when focusing the item must also reset zoom to `1`; both methods emit `fCanvasChange` by default.
144
146
  - Connection endpoints are connector ids. Automatic cascade from node/group deletion uses the rendered connector registry; before connectors render, remove known attached connection ids explicitly in the same `state.batch(...)`.
145
147
 
146
148
  ### Async reflow must stay inside an open transaction
@@ -180,7 +182,7 @@ When the flow compiles but looks wrong, verify in this order:
180
182
  10. **Connections attach to the wrong place** (`FF1006`): a connector is hidden with CSS (`display: none`) — its geometry is a 0×0 point. Conditionally render instead of hiding.
181
183
  11. **Node moves but its bindings never fire** (`FF1007`): an `fNode` element is nested inside another node element. One `fNode` per node; hierarchy is id-based (`fNodeParentId`), not DOM-based.
182
184
  12. **Group behaviors don't apply** (`FF1008`): `fNodeParentId` / `fGroupParentId` references an id no rendered group has.
183
- 13. **Wrong initial viewport** (`FF1009`): `fitToScreen()` / `resetScaleAndCenter()` / `centerGroupOrNode()` called before nodes were rendered — call them from `(fNodesRendered)` (earliest safe) or `(fFullRendered)`.
185
+ 13. **Wrong initial viewport** (`FF1009`): `fitToScreen()` / `resetScaleAndCenter()` / `centerGroupOrNode()` / `resetScaleAndCenterGroupOrNode()` called before nodes were rendered — call them from `(fNodesRendered)` (earliest safe) or `(fFullRendered)`.
184
186
  14. **Initial centering appears in managed undo history**: call `resetScaleAndCenter(false, false)` (or pass `emitCanvasChange: false` to another viewport helper) for an application-driven transform.
185
187
 
186
188
  To verify programmatically: listen to `(fFullRendered)` on `<f-flow>`, then call `flow.getState()` and assert every declared connection resolved to existing connectors.
@@ -192,9 +194,9 @@ To verify programmatically: listen to `(fFullRendered)` on `<f-flow>`, then call
192
194
  - Style flow internals (connection paths, minimap, markers) in global styles or via `::ng-deep` — component-scoped CSS never reaches them. Wire the default theme via `ng add`.
193
195
  - Do not combine `fAutoSizeToFitChildren` with restoring a persisted group size in the same render: pass `false` while restoring, enable it afterwards.
194
196
  - With several `<f-flow>` instances on one page, keep `fDraggable` enabled only on the active flow.
195
- - Connections define `SELECTED_START` / `SELECTED_END` marker variants in addition to `START` / `END`, or markers disappear when the connection is selected.
197
+ - For custom markers, either provide `SELECTED_START` / `SELECTED_END` alongside `START` / `END`, or use `START_ALL_STATES` / `END_ALL_STATES`; the all-states variants create both normal and selected markers.
196
198
  - An empty `fCanBeConnectedTo` allow-list means "no restriction", not "allow nothing"; category strings must match exactly.
197
- - Above ~500 nodes enable `[fCache]` on `<f-flow>` and render nodes with `*fVirtualFor` inside `<ng-container ngProjectAs="[fNodes]">`.
199
+ - Start with regular `@for` rendering and measure a production build. Enable `[fCache]` when geometry reads are a demonstrated cost; use `*fVirtualFor` inside `<ng-container ngProjectAs="[fNodes]">` when progressive creation materially improves startup. `fVirtualFor` is not viewport culling.
198
200
 
199
201
  ## Naming Distinction
200
202
 
@@ -217,7 +219,11 @@ See [STYLING.md](./STYLING.md).
217
219
  - Human docs: https://flow.foblex.com/docs/get-started
218
220
  - Live examples with source: https://flow.foblex.com/examples/overview
219
221
  - Managed state example and contract: https://flow.foblex.com/examples/state
222
+ - Managed state guide: https://flow.foblex.com/docs/managed-flow-state
220
223
  - Managed state + asynchronous reflow recipe: https://flow.foblex.com/examples/state
224
+ - Large-flow performance guide: https://flow.foblex.com/docs/large-flow-performance
225
+ - Shadow DOM and Angular Elements: https://flow.foblex.com/docs/shadow-dom-and-angular-elements
226
+ - Unified connector migration: https://flow.foblex.com/docs/migrating-to-unified-connectors
221
227
 
222
228
  ## Fallback Rule
223
229
 
package/README.md CHANGED
@@ -30,7 +30,7 @@
30
30
 
31
31
  Foblex Flow gives Angular teams a simple way to start building graph-based products without adopting a React-first mental model. Begin with `f-flow`, `f-canvas`, nodes, and connections, then add richer editor features only when your product needs them.
32
32
 
33
- Use it to create workflow builders, AI low-code tools, call-flow editors, UML diagrams, internal back-office tools, and other node-based interfaces while keeping your own state, validation, persistence, and domain logic.
33
+ Use it to create workflow builders, AI low-code tools, call-flow editors, UML diagrams, internal back-office tools, and other node-based interfaces. Start with app-owned graph records, or opt into managed records and undo/redo with `withFlowState()`; validation, persistence, permissions, and domain logic remain application concerns in both modes.
34
34
 
35
35
  <p align="center">
36
36
  <a href="https://flow.foblex.com/examples/overview">
@@ -47,7 +47,8 @@ Current `19.x` releases target Angular `17.3+`. If your app is on Angular 12-17.
47
47
  - Built for real editor interactions: drag to connect, drag to reassign, selection, zoom, minimap, snapping, alignment helpers, and waypoints.
48
48
  - Advanced modules are optional: caching and virtualization are scaling tools, not day-one requirements.
49
49
  - Custom nodes, connectors, and connections for domain-specific graph UIs.
50
- - Your app stays in control of graph state, validation rules, permissions, and persistence.
50
+ - Choose the state boundary that fits your editor: app-owned records by default, or optional typed `FFlowState` records with snapshots and undo/redo.
51
+ - Your app stays in control of validation rules, permissions, persistence, and business meaning in either state mode.
51
52
  - Suitable for both lightweight diagrams and full workflow-builder products.
52
53
 
53
54
  ## Feature Overview
@@ -61,11 +62,19 @@ Current `19.x` releases target Angular `17.3+`. If your app is on Angular 12-17.
61
62
  | Layout | Dagre and ELK auto-layout packages, reflow-on-resize, layer ordering |
62
63
  | Scale | Node virtualization, render caching, background workers — optional, for large scenes |
63
64
  | Customization | Fully templated nodes/connections/connectors, themable via CSS tokens/SCSS mixins, custom markers, connection gradients and labels |
64
- | Integration | Event-driven API (`fCreateConnection`, `fMoveNodes`, `fDeleteSelected`, …) the library never mutates your data; SSR-safe, zoneless-ready |
65
+ | Integration | Classic event-driven API with app-owned records, or optional `withFlowState()` managed records, snapshots, and undo/redo; SSR-safe, zoneless-ready |
65
66
  | AI tooling | `llms.txt`, bundled `AI.md`, `ng add` writes agent rules, dev diagnostics with stable `FFxxxx` error codes |
66
67
 
67
68
  Coming from React Flow? Read the honest comparison: [React Flow vs Foblex Flow for Angular teams](https://flow.foblex.com/docs/react-flow-vs-foblex-flow-for-angular-teams).
68
69
 
70
+ ## Choose Your State Integration
71
+
72
+ The default **classic mode** renders arrays or signals owned by your application. Foblex Flow emits final interaction events such as `fCreateConnection` and `fMoveNodes`; your handlers validate them and update application state.
73
+
74
+ The optional **managed mode** installs `provideFFlow(withFlowState())`. Its typed `FFlowState` records apply supported gestures automatically and provide batching, snapshots, undo, and redo. Your application still defines domain fields and decides how records are validated and persisted.
75
+
76
+ [Managed Flow State guide](https://flow.foblex.com/docs/managed-flow-state) · [Live state example](https://flow.foblex.com/examples/state)
77
+
69
78
  ## What You Can Build
70
79
 
71
80
  - Angular node editors
@@ -157,7 +166,7 @@ Full guide: [Default Theme and Styling](https://flow.foblex.com/docs/default-the
157
166
  </f-flow>
158
167
  ```
159
168
 
160
- That is the whole mental model: `f-flow` hosts the editor, `f-canvas` pans and zooms, any element becomes a node with `fNode`, connectors attach edges. Everything below is opt-in.
169
+ That is the rendering mental model: `f-flow` hosts the editor, `f-canvas` pans and zooms, any element becomes a node with `fNode`, and `fConnector` endpoints attach connections. State management and every advanced feature remain explicit choices.
161
170
 
162
171
  ## Quick FAQ
163
172
 
@@ -169,6 +178,10 @@ That is the whole mental model: `f-flow` hosts the editor, `f-canvas` pans and z
169
178
 
170
179
  - [Get Started](https://flow.foblex.com/docs/get-started)
171
180
  - [Angular Version Compatibility](https://flow.foblex.com/docs/angular-version-compatibility)
181
+ - [Managed Flow State](https://flow.foblex.com/docs/managed-flow-state)
182
+ - [Large Flow Performance](https://flow.foblex.com/docs/large-flow-performance)
183
+ - [Shadow DOM and Angular Elements](https://flow.foblex.com/docs/shadow-dom-and-angular-elements)
184
+ - [Migrating to Unified Connectors](https://flow.foblex.com/docs/migrating-to-unified-connectors)
172
185
  - [Documentation](https://flow.foblex.com/docs/intro)
173
186
  - [Examples](https://flow.foblex.com/examples/overview)
174
187
  - [Articles](https://flow.foblex.com/blog/overview)
@@ -370,7 +370,13 @@ class FChannelHub {
370
370
  return result;
371
371
  }
372
372
  listen(destroyRef, callback) {
373
- let current = callback;
373
+ let tornDown = false;
374
+ let current = () => {
375
+ if (tornDown || destroyRef.destroyed) {
376
+ return;
377
+ }
378
+ callback();
379
+ };
374
380
  const cleanups = [];
375
381
  const onSubscribes = [];
376
382
  const teardownSetters = [];
@@ -384,9 +390,8 @@ class FChannelHub {
384
390
  if (res.setTeardown)
385
391
  teardownSetters.push(res.setTeardown);
386
392
  }
387
- const unsubs = this._channels.map((ch) => ch.listen(() => current()));
393
+ const unsubs = [];
388
394
  let unregisterOnDestroy = null;
389
- let tornDown = false;
390
395
  const teardown = () => {
391
396
  if (tornDown)
392
397
  return;
@@ -400,11 +405,31 @@ class FChannelHub {
400
405
  .slice()
401
406
  .reverse()
402
407
  .forEach((set) => set(teardown));
408
+ if (destroyRef.destroyed) {
409
+ teardown();
410
+ return;
411
+ }
412
+ this._channels.forEach((channel) => {
413
+ unsubs.push(channel.listen(() => {
414
+ if (tornDown || destroyRef.destroyed) {
415
+ return;
416
+ }
417
+ current();
418
+ }));
419
+ });
420
+ if (destroyRef.destroyed) {
421
+ teardown();
422
+ return;
423
+ }
424
+ unregisterOnDestroy = destroyRef.onDestroy(teardown);
403
425
  onSubscribes
404
426
  .slice()
405
427
  .reverse()
406
- .forEach((fn) => fn(current));
407
- unregisterOnDestroy = destroyRef.onDestroy(teardown);
428
+ .forEach((fn) => {
429
+ if (!tornDown && !destroyRef.destroyed) {
430
+ fn(current);
431
+ }
432
+ });
408
433
  }
409
434
  }
410
435
 
@@ -900,11 +925,13 @@ class CenterGroupOrNodeRequest {
900
925
  id;
901
926
  animated;
902
927
  emitCanvasChange;
928
+ resetScale;
903
929
  static fToken = Symbol('CenterGroupOrNodeRequest');
904
- constructor(id, animated, emitCanvasChange = true) {
930
+ constructor(id, animated, emitCanvasChange = true, resetScale = false) {
905
931
  this.id = id;
906
932
  this.animated = animated;
907
933
  this.emitCanvasChange = emitCanvasChange;
934
+ this.resetScale = resetScale;
908
935
  }
909
936
  }
910
937
 
@@ -917,17 +944,21 @@ let CenterGroupOrNode = class CenterGroupOrNode {
917
944
  get _transform() {
918
945
  return this._store.transform;
919
946
  }
920
- handle({ id, animated, emitCanvasChange }) {
947
+ handle({ id, animated, emitCanvasChange, resetScale }) {
921
948
  const node = this._store.nodes.get(id);
922
949
  if (!node) {
923
950
  return;
924
951
  }
925
- this._toCenter(this._getNodeRect(node), this._getFlowRect(), node._position);
952
+ this._toCenter(this._getNodeRect(node), this._getFlowRect(), node._position, resetScale);
926
953
  this._mediator.execute(new RedrawCanvasWithAnimationRequest(animated, ECanvasRedrawContext.VIEWPORT_ONLY, emitCanvasChange));
927
954
  }
928
- _toCenter(fNodeRect, fFlowRect, position) {
955
+ _toCenter(fNodeRect, fFlowRect, position, resetScale) {
956
+ const currentScale = this._transform.scale;
957
+ const targetScale = resetScale ? 1 : currentScale;
958
+ const sizeScale = currentScale === 0 ? 1 : targetScale / currentScale;
929
959
  this._transform.scaledPosition = PointExtensions.initialize();
930
- this._transform.position = PointExtensions.initialize((fFlowRect.width - fNodeRect.width) / 2 - position.x * this._transform.scale, (fFlowRect.height - fNodeRect.height) / 2 - position.y * this._transform.scale);
960
+ this._transform.scale = targetScale;
961
+ this._transform.position = PointExtensions.initialize((fFlowRect.width - fNodeRect.width * sizeScale) / 2 - position.x * targetScale, (fFlowRect.height - fNodeRect.height * sizeScale) / 2 - position.y * targetScale);
931
962
  }
932
963
  _getNodeRect(fNode) {
933
964
  return RectExtensions.fromElement(fNode.hostElement);
@@ -20988,9 +21019,17 @@ class FCanvasComponent extends FCanvasBase {
20988
21019
  }
20989
21020
  centerGroupOrNode(groupOrNodeId, animated = true, emitCanvasChange = true) {
20990
21021
  this._warnWhenCalledBeforeNodesRender('centerGroupOrNode()');
20991
- this._afterRedraw(() => {
20992
- this._mediator.execute(new CenterGroupOrNodeRequest(groupOrNodeId, animated, emitCanvasChange));
20993
- });
21022
+ this._centerGroupOrNode(groupOrNodeId, animated, emitCanvasChange, false);
21023
+ }
21024
+ /**
21025
+ * Resets the canvas scale to `1` and centers the specified group or node.
21026
+ * @param groupOrNodeId - The ID of the group or node to center.
21027
+ * @param animated - If true, the centering will be animated; otherwise, it will be instantaneous.
21028
+ * @param emitCanvasChange - If false, does not emit `fCanvasChange` for this programmatic move.
21029
+ */
21030
+ resetScaleAndCenterGroupOrNode(groupOrNodeId, animated = true, emitCanvasChange = true) {
21031
+ this._warnWhenCalledBeforeNodesRender('resetScaleAndCenterGroupOrNode()');
21032
+ this._centerGroupOrNode(groupOrNodeId, animated, emitCanvasChange, true);
20994
21033
  }
20995
21034
  /**
20996
21035
  * Fits the canvas to the screen by adjusting the scale and position.
@@ -21047,6 +21086,11 @@ class FCanvasComponent extends FCanvasBase {
21047
21086
  _afterRedraw(callback) {
21048
21087
  this._mediator.execute(new WaitForConnectionsRenderedRequest(this._components.connectionsRevision, this._components.nodesRevision, () => afterNextRender(callback, { injector: this._injector }), this.destroyRef));
21049
21088
  }
21089
+ _centerGroupOrNode(groupOrNodeId, animated, emitCanvasChange, resetScale) {
21090
+ this._afterRedraw(() => {
21091
+ this._mediator.execute(new CenterGroupOrNodeRequest(groupOrNodeId, animated, emitCanvasChange, resetScale));
21092
+ });
21093
+ }
21050
21094
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: FCanvasComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
21051
21095
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: FCanvasComponent, isStandalone: true, selector: "f-canvas", inputs: { position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, scale: { classPropertyName: "scale", publicName: "scale", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null }, fLayers: { classPropertyName: "fLayers", publicName: "fLayers", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fCanvasChange: "fCanvasChange" }, host: { classAttribute: "f-component f-canvas" }, providers: [{ provide: F_CANVAS, useExisting: FCanvasComponent }], viewQueries: [{ propertyName: "fGroupsContainer", first: true, predicate: ["fGroupsContainer"], descendants: true, isSignal: true }, { propertyName: "fNodesContainer", first: true, predicate: ["fNodesContainer"], descendants: true, isSignal: true }, { propertyName: "fConnectionsContainer", first: true, predicate: ["fConnectionsContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<ng-container>\n @for (layer of resolvedLayers(); track layer) {\n @switch (layer) {\n @case ('groups') {\n <div #fGroupsContainer class=\"f-groups-container\">\n <ng-content select=\"[fGroup], [fGroups]\" />\n </div>\n }\n @case ('connections') {\n <div #fConnectionsContainer class=\"f-connections-container\">\n <ng-content select=\"f-snap-connection\" />\n <ng-content select=\"f-connection, [fConnections]\" />\n <ng-content select=\"f-connection-for-create\" />\n </div>\n }\n @case ('nodes') {\n <div #fNodesContainer class=\"f-nodes-container\">\n <ng-content select=\"[fNode], [fNodes]\" />\n </div>\n }\n }\n }\n</ng-container>\n", styles: [":host{position:absolute;overflow:visible;width:100%;height:100%;left:0;top:0;transform-origin:0 0;pointer-events:none}:host.f-canvas-dragging,:host.canvas-dragging{transform:translateZ(0)}.f-groups-container,.f-connections-container,.f-nodes-container{isolation:isolate}.f-connections-container{position:absolute}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
21052
21096
  }
@@ -21755,6 +21799,7 @@ class FA11yController {
21755
21799
  const direction = ARROW_DIRECTIONS[event.key];
21756
21800
  if (direction) {
21757
21801
  event.preventDefault();
21802
+ this._syncActiveItemFromSelection();
21758
21803
  event.ctrlKey || event.metaKey
21759
21804
  ? this._walkConnections(direction, event.shiftKey)
21760
21805
  : this._navigate(direction, event.shiftKey);
@@ -21830,6 +21875,49 @@ class FA11yController {
21830
21875
  !!target.closest('[fLockedContext]'));
21831
21876
  }
21832
21877
  // ------------------------------------------------------------------ navigation
21878
+ /**
21879
+ * Selection can be restored from state or replaced by a pointer-driven render before
21880
+ * this layer receives a stable DOM target. In that case there is no active descendant,
21881
+ * so use the single selected item as the spatial-navigation anchor.
21882
+ */
21883
+ _syncActiveItemFromSelection() {
21884
+ const selection = this._selection();
21885
+ const selectedNodeIds = [...selection.fNodeIds, ...selection.fGroupIds];
21886
+ if (selectedNodeIds.length + selection.fConnectionIds.length !== 1) {
21887
+ return;
21888
+ }
21889
+ const selectedNodeId = selectedNodeIds[0];
21890
+ if (selectedNodeId) {
21891
+ if (this._activeNodeId === selectedNodeId && this._activeNode()) {
21892
+ return;
21893
+ }
21894
+ const node = this._store.nodes.getAll().find((x) => x.fId() === selectedNodeId);
21895
+ if (!node) {
21896
+ return;
21897
+ }
21898
+ this._activeNodeId = selectedNodeId;
21899
+ this._activeConnectionId = undefined;
21900
+ this._walkTargetNodeId = undefined;
21901
+ this._lastMove = undefined;
21902
+ this._setActiveDescendant(node.hostElement);
21903
+ return;
21904
+ }
21905
+ const selectedConnectionId = selection.fConnectionIds[0];
21906
+ if (this._activeConnectionId === selectedConnectionId && this._activeConnection()) {
21907
+ return;
21908
+ }
21909
+ const connection = this._store.connections
21910
+ .getAll()
21911
+ .find((x) => x.fId() === selectedConnectionId);
21912
+ if (!connection) {
21913
+ return;
21914
+ }
21915
+ this._activeConnectionId = selectedConnectionId;
21916
+ this._activeNodeId = undefined;
21917
+ this._walkTargetNodeId = undefined;
21918
+ this._lastMove = undefined;
21919
+ this._setActiveDescendant(connection.hostElement);
21920
+ }
21833
21921
  /**
21834
21922
  * Plain arrows travel over nodes AND connections as equal stops — a connection is
21835
21923
  * represented by its midpoint, so walking a chain reads node → connection → node.
@@ -22619,14 +22707,15 @@ class FFlowState {
22619
22707
  this._undoStack.length = 0;
22620
22708
  this._redoStack.length = 0;
22621
22709
  this._batchDirty = false;
22710
+ const selection = _copySelection(data.selection ?? EMPTY_SELECTION);
22622
22711
  const transform = _copyTransform(data.transform ?? DEFAULT_TRANSFORM);
22623
- this._liveSelection.set(EMPTY_SELECTION);
22712
+ this._liveSelection.set(selection);
22624
22713
  this._liveTransform.set(transform);
22625
22714
  this._shape.set({
22626
22715
  nodes: _byId(data.nodes ?? []),
22627
22716
  groups: _byId(data.groups ?? []),
22628
22717
  connections: _byId(data.connections ?? []),
22629
- selection: EMPTY_SELECTION,
22718
+ selection,
22630
22719
  transform,
22631
22720
  });
22632
22721
  this._syncHistorySignals();
@@ -22642,6 +22731,7 @@ class FFlowState {
22642
22731
  nodes: Object.values(nodes).map(_copyBox),
22643
22732
  groups: Object.values(groups).map(_copyBox),
22644
22733
  connections: Object.values(connections).map((connection) => ({ ...connection })),
22734
+ selection: _copySelection(this.selection()),
22645
22735
  transform: _copyTransform(this.transform()),
22646
22736
  };
22647
22737
  }
@@ -23142,6 +23232,13 @@ function _copyBox(box) {
23142
23232
  size: box.size ? { ...box.size } : undefined,
23143
23233
  };
23144
23234
  }
23235
+ function _copySelection(selection) {
23236
+ return {
23237
+ nodeIds: [...selection.nodeIds],
23238
+ groupIds: [...selection.groupIds],
23239
+ connectionIds: [...selection.connectionIds],
23240
+ };
23241
+ }
23145
23242
  /** Clears `parentId` on records that pointed at a removed group. */
23146
23243
  function _clearParent(records, removedGroups) {
23147
23244
  if (!removedGroups.size) {
@@ -23237,9 +23334,7 @@ class FFlowStateController {
23237
23334
  this._wireCanvasEvents();
23238
23335
  this._wireSizeChanges();
23239
23336
  }));
23240
- if (this._config.selectionInHistory) {
23241
- this._wireSelectionRestore();
23242
- }
23337
+ this._wireSelectionRestore();
23243
23338
  }
23244
23339
  destroy() {
23245
23340
  this._disposers.forEach((dispose) => dispose());
@@ -23447,9 +23542,9 @@ class FFlowStateController {
23447
23542
  }
23448
23543
  }
23449
23544
  /**
23450
- * When selection is part of the history, `undo`/`redo` land on a shape with
23451
- * its own selection — push it back into the flow so the highlight follows.
23452
- * `isSelectedChanged: false` keeps this from re-emitting a selection change.
23545
+ * Pushes state selection into the flow after `load`, `undo` or `redo` so the
23546
+ * rendered highlight follows the persisted state. `isSelectedChanged: false`
23547
+ * keeps this from re-emitting a selection change.
23453
23548
  */
23454
23549
  _wireSelectionRestore() {
23455
23550
  const state = this._state;
@@ -23506,7 +23601,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
23506
23601
  * connection, node moves, drops into groups, external-item drops, delete
23507
23602
  * requests) are applied to the state automatically, each as one undoable
23508
23603
  * step. `undo()`/`redo()` with `canUndo`/`canRedo` signals come built in;
23509
- * `snapshot()` returns the graph as plain arrays for persistence.
23604
+ * `snapshot()` returns the graph, selection and viewport for persistence.
23510
23605
  *
23511
23606
  * Every store behavior is overridable: subclass `FFlowState` (any CRUD
23512
23607
  * method, any `apply*` gesture handler, any protected building block) and