@foblex/flow 19.1.4 → 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)
@@ -925,11 +925,13 @@ class CenterGroupOrNodeRequest {
925
925
  id;
926
926
  animated;
927
927
  emitCanvasChange;
928
+ resetScale;
928
929
  static fToken = Symbol('CenterGroupOrNodeRequest');
929
- constructor(id, animated, emitCanvasChange = true) {
930
+ constructor(id, animated, emitCanvasChange = true, resetScale = false) {
930
931
  this.id = id;
931
932
  this.animated = animated;
932
933
  this.emitCanvasChange = emitCanvasChange;
934
+ this.resetScale = resetScale;
933
935
  }
934
936
  }
935
937
 
@@ -942,17 +944,21 @@ let CenterGroupOrNode = class CenterGroupOrNode {
942
944
  get _transform() {
943
945
  return this._store.transform;
944
946
  }
945
- handle({ id, animated, emitCanvasChange }) {
947
+ handle({ id, animated, emitCanvasChange, resetScale }) {
946
948
  const node = this._store.nodes.get(id);
947
949
  if (!node) {
948
950
  return;
949
951
  }
950
- this._toCenter(this._getNodeRect(node), this._getFlowRect(), node._position);
952
+ this._toCenter(this._getNodeRect(node), this._getFlowRect(), node._position, resetScale);
951
953
  this._mediator.execute(new RedrawCanvasWithAnimationRequest(animated, ECanvasRedrawContext.VIEWPORT_ONLY, emitCanvasChange));
952
954
  }
953
- _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;
954
959
  this._transform.scaledPosition = PointExtensions.initialize();
955
- 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);
956
962
  }
957
963
  _getNodeRect(fNode) {
958
964
  return RectExtensions.fromElement(fNode.hostElement);
@@ -21013,9 +21019,17 @@ class FCanvasComponent extends FCanvasBase {
21013
21019
  }
21014
21020
  centerGroupOrNode(groupOrNodeId, animated = true, emitCanvasChange = true) {
21015
21021
  this._warnWhenCalledBeforeNodesRender('centerGroupOrNode()');
21016
- this._afterRedraw(() => {
21017
- this._mediator.execute(new CenterGroupOrNodeRequest(groupOrNodeId, animated, emitCanvasChange));
21018
- });
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);
21019
21033
  }
21020
21034
  /**
21021
21035
  * Fits the canvas to the screen by adjusting the scale and position.
@@ -21072,6 +21086,11 @@ class FCanvasComponent extends FCanvasBase {
21072
21086
  _afterRedraw(callback) {
21073
21087
  this._mediator.execute(new WaitForConnectionsRenderedRequest(this._components.connectionsRevision, this._components.nodesRevision, () => afterNextRender(callback, { injector: this._injector }), this.destroyRef));
21074
21088
  }
21089
+ _centerGroupOrNode(groupOrNodeId, animated, emitCanvasChange, resetScale) {
21090
+ this._afterRedraw(() => {
21091
+ this._mediator.execute(new CenterGroupOrNodeRequest(groupOrNodeId, animated, emitCanvasChange, resetScale));
21092
+ });
21093
+ }
21075
21094
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: FCanvasComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
21076
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 });
21077
21096
  }
@@ -21780,6 +21799,7 @@ class FA11yController {
21780
21799
  const direction = ARROW_DIRECTIONS[event.key];
21781
21800
  if (direction) {
21782
21801
  event.preventDefault();
21802
+ this._syncActiveItemFromSelection();
21783
21803
  event.ctrlKey || event.metaKey
21784
21804
  ? this._walkConnections(direction, event.shiftKey)
21785
21805
  : this._navigate(direction, event.shiftKey);
@@ -21855,6 +21875,49 @@ class FA11yController {
21855
21875
  !!target.closest('[fLockedContext]'));
21856
21876
  }
21857
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
+ }
21858
21921
  /**
21859
21922
  * Plain arrows travel over nodes AND connections as equal stops — a connection is
21860
21923
  * represented by its midpoint, so walking a chain reads node → connection → node.
@@ -22644,14 +22707,15 @@ class FFlowState {
22644
22707
  this._undoStack.length = 0;
22645
22708
  this._redoStack.length = 0;
22646
22709
  this._batchDirty = false;
22710
+ const selection = _copySelection(data.selection ?? EMPTY_SELECTION);
22647
22711
  const transform = _copyTransform(data.transform ?? DEFAULT_TRANSFORM);
22648
- this._liveSelection.set(EMPTY_SELECTION);
22712
+ this._liveSelection.set(selection);
22649
22713
  this._liveTransform.set(transform);
22650
22714
  this._shape.set({
22651
22715
  nodes: _byId(data.nodes ?? []),
22652
22716
  groups: _byId(data.groups ?? []),
22653
22717
  connections: _byId(data.connections ?? []),
22654
- selection: EMPTY_SELECTION,
22718
+ selection,
22655
22719
  transform,
22656
22720
  });
22657
22721
  this._syncHistorySignals();
@@ -22667,6 +22731,7 @@ class FFlowState {
22667
22731
  nodes: Object.values(nodes).map(_copyBox),
22668
22732
  groups: Object.values(groups).map(_copyBox),
22669
22733
  connections: Object.values(connections).map((connection) => ({ ...connection })),
22734
+ selection: _copySelection(this.selection()),
22670
22735
  transform: _copyTransform(this.transform()),
22671
22736
  };
22672
22737
  }
@@ -23167,6 +23232,13 @@ function _copyBox(box) {
23167
23232
  size: box.size ? { ...box.size } : undefined,
23168
23233
  };
23169
23234
  }
23235
+ function _copySelection(selection) {
23236
+ return {
23237
+ nodeIds: [...selection.nodeIds],
23238
+ groupIds: [...selection.groupIds],
23239
+ connectionIds: [...selection.connectionIds],
23240
+ };
23241
+ }
23170
23242
  /** Clears `parentId` on records that pointed at a removed group. */
23171
23243
  function _clearParent(records, removedGroups) {
23172
23244
  if (!removedGroups.size) {
@@ -23262,9 +23334,7 @@ class FFlowStateController {
23262
23334
  this._wireCanvasEvents();
23263
23335
  this._wireSizeChanges();
23264
23336
  }));
23265
- if (this._config.selectionInHistory) {
23266
- this._wireSelectionRestore();
23267
- }
23337
+ this._wireSelectionRestore();
23268
23338
  }
23269
23339
  destroy() {
23270
23340
  this._disposers.forEach((dispose) => dispose());
@@ -23472,9 +23542,9 @@ class FFlowStateController {
23472
23542
  }
23473
23543
  }
23474
23544
  /**
23475
- * When selection is part of the history, `undo`/`redo` land on a shape with
23476
- * its own selection — push it back into the flow so the highlight follows.
23477
- * `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.
23478
23548
  */
23479
23549
  _wireSelectionRestore() {
23480
23550
  const state = this._state;
@@ -23531,7 +23601,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImpor
23531
23601
  * connection, node moves, drops into groups, external-item drops, delete
23532
23602
  * requests) are applied to the state automatically, each as one undoable
23533
23603
  * step. `undo()`/`redo()` with `canUndo`/`canRedo` signals come built in;
23534
- * `snapshot()` returns the graph as plain arrays for persistence.
23604
+ * `snapshot()` returns the graph, selection and viewport for persistence.
23535
23605
  *
23536
23606
  * Every store behavior is overridable: subclass `FFlowState` (any CRUD
23537
23607
  * method, any `apply*` gesture handler, any protected building block) and