@angflow/angular 0.0.18 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +108 -15
  2. package/dist/base.css +8 -0
  3. package/dist/esm/index.d.ts +0 -1
  4. package/dist/esm/lib/agent/agent-bridge.service.d.ts +3 -1
  5. package/dist/esm/lib/agent/agent-bridge.service.js +130 -23
  6. package/dist/esm/lib/agent/agent-bridge.service.js.map +1 -1
  7. package/dist/esm/lib/agent/chat/agent-chat.component.d.ts +0 -1
  8. package/dist/esm/lib/agent/chat/agent-chat.service.d.ts +0 -1
  9. package/dist/esm/lib/agent/chat/agent-chat.service.js +11 -2
  10. package/dist/esm/lib/agent/chat/agent-chat.service.js.map +1 -1
  11. package/dist/esm/lib/agent/chat/default-system-prompt.d.ts +1 -2
  12. package/dist/esm/lib/agent/chat/default-system-prompt.js +2 -1
  13. package/dist/esm/lib/agent/chat/default-system-prompt.js.map +1 -1
  14. package/dist/esm/lib/agent/chat/index.d.ts +0 -1
  15. package/dist/esm/lib/agent/chat/provide-agent-chat.d.ts +0 -1
  16. package/dist/esm/lib/agent/chat/types.d.ts +0 -1
  17. package/dist/esm/lib/agent/history.d.ts +0 -1
  18. package/dist/esm/lib/agent/index.d.ts +0 -1
  19. package/dist/esm/lib/agent/provide-agent-bridge.d.ts +0 -1
  20. package/dist/esm/lib/agent/tool-schemas.d.ts +0 -1
  21. package/dist/esm/lib/agent/tool-schemas.js +3 -1
  22. package/dist/esm/lib/agent/tool-schemas.js.map +1 -1
  23. package/dist/esm/lib/agent/transports/websocket.d.ts +8 -1
  24. package/dist/esm/lib/agent/transports/websocket.js +14 -2
  25. package/dist/esm/lib/agent/transports/websocket.js.map +1 -1
  26. package/dist/esm/lib/agent/transports/window.d.ts +0 -1
  27. package/dist/esm/lib/agent/transports/window.js +2 -1
  28. package/dist/esm/lib/agent/transports/window.js.map +1 -1
  29. package/dist/esm/lib/agent/types.d.ts +0 -1
  30. package/dist/esm/lib/components/a11y-descriptions/a11y-descriptions.component.d.ts +0 -1
  31. package/dist/esm/lib/components/attribution/attribution.component.d.ts +0 -1
  32. package/dist/esm/lib/components/background/background.component.d.ts +0 -1
  33. package/dist/esm/lib/components/connection-line/connection-line.component.d.ts +0 -1
  34. package/dist/esm/lib/components/controls/controls.component.d.ts +0 -1
  35. package/dist/esm/lib/components/controls/controls.component.js +2 -1
  36. package/dist/esm/lib/components/controls/controls.component.js.map +1 -1
  37. package/dist/esm/lib/components/edge-label-renderer/edge-label-renderer.component.d.ts +0 -1
  38. package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.d.ts +7 -2
  39. package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.js +11 -4
  40. package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.js.map +1 -1
  41. package/dist/esm/lib/components/edges/base-edge.component.d.ts +0 -1
  42. package/dist/esm/lib/components/edges/bezier-edge.component.d.ts +0 -1
  43. package/dist/esm/lib/components/edges/bezier-edge.component.js +2 -1
  44. package/dist/esm/lib/components/edges/bezier-edge.component.js.map +1 -1
  45. package/dist/esm/lib/components/edges/edge-text.component.d.ts +0 -1
  46. package/dist/esm/lib/components/edges/simple-bezier-edge.component.d.ts +0 -1
  47. package/dist/esm/lib/components/edges/simple-bezier-edge.component.js +2 -1
  48. package/dist/esm/lib/components/edges/simple-bezier-edge.component.js.map +1 -1
  49. package/dist/esm/lib/components/edges/smooth-step-edge.component.d.ts +0 -1
  50. package/dist/esm/lib/components/edges/smooth-step-edge.component.js +2 -1
  51. package/dist/esm/lib/components/edges/smooth-step-edge.component.js.map +1 -1
  52. package/dist/esm/lib/components/edges/step-edge.component.d.ts +0 -1
  53. package/dist/esm/lib/components/edges/step-edge.component.js +2 -1
  54. package/dist/esm/lib/components/edges/step-edge.component.js.map +1 -1
  55. package/dist/esm/lib/components/edges/straight-edge.component.d.ts +0 -1
  56. package/dist/esm/lib/components/edges/straight-edge.component.js +2 -1
  57. package/dist/esm/lib/components/edges/straight-edge.component.js.map +1 -1
  58. package/dist/esm/lib/components/handle/handle.component.d.ts +1 -1
  59. package/dist/esm/lib/components/handle/handle.component.js +13 -1
  60. package/dist/esm/lib/components/handle/handle.component.js.map +1 -1
  61. package/dist/esm/lib/components/handle-group/handle-group.component.d.ts +0 -1
  62. package/dist/esm/lib/components/handle-group/handle-row.component.d.ts +0 -1
  63. package/dist/esm/lib/components/minimap/minimap.component.d.ts +36 -25
  64. package/dist/esm/lib/components/minimap/minimap.component.js +197 -213
  65. package/dist/esm/lib/components/minimap/minimap.component.js.map +1 -1
  66. package/dist/esm/lib/components/ng-flow-provider/ng-flow-provider.component.d.ts +13 -5
  67. package/dist/esm/lib/components/ng-flow-provider/ng-flow-provider.component.js +13 -4
  68. package/dist/esm/lib/components/ng-flow-provider/ng-flow-provider.component.js.map +1 -1
  69. package/dist/esm/lib/components/node-resizer/node-resizer.component.d.ts +2 -1
  70. package/dist/esm/lib/components/node-resizer/node-resizer.component.js +43 -26
  71. package/dist/esm/lib/components/node-resizer/node-resizer.component.js.map +1 -1
  72. package/dist/esm/lib/components/node-toolbar/node-toolbar.component.d.ts +0 -1
  73. package/dist/esm/lib/components/node-toolbar/node-toolbar.component.js +1 -1
  74. package/dist/esm/lib/components/node-toolbar/node-toolbar.component.js.map +1 -1
  75. package/dist/esm/lib/components/nodes/default-node.component.d.ts +6 -15
  76. package/dist/esm/lib/components/nodes/default-node.component.js +13 -22
  77. package/dist/esm/lib/components/nodes/default-node.component.js.map +1 -1
  78. package/dist/esm/lib/components/nodes/group-node.component.d.ts +5 -16
  79. package/dist/esm/lib/components/nodes/group-node.component.js +8 -16
  80. package/dist/esm/lib/components/nodes/group-node.component.js.map +1 -1
  81. package/dist/esm/lib/components/nodes/input-node.component.d.ts +6 -15
  82. package/dist/esm/lib/components/nodes/input-node.component.js +11 -20
  83. package/dist/esm/lib/components/nodes/input-node.component.js.map +1 -1
  84. package/dist/esm/lib/components/nodes/output-node.component.d.ts +6 -15
  85. package/dist/esm/lib/components/nodes/output-node.component.js +11 -20
  86. package/dist/esm/lib/components/nodes/output-node.component.js.map +1 -1
  87. package/dist/esm/lib/components/nodes/template-node.component.d.ts +2 -14
  88. package/dist/esm/lib/components/nodes/template-node.component.js +16 -26
  89. package/dist/esm/lib/components/nodes/template-node.component.js.map +1 -1
  90. package/dist/esm/lib/components/panel/panel.component.d.ts +0 -1
  91. package/dist/esm/lib/components/selection-box/selection-box.component.d.ts +11 -1
  92. package/dist/esm/lib/components/selection-box/selection-box.component.js +108 -3
  93. package/dist/esm/lib/components/selection-box/selection-box.component.js.map +1 -1
  94. package/dist/esm/lib/components/viewport-portal/viewport-portal.component.d.ts +0 -1
  95. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.d.ts +27 -3
  96. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.js +151 -61
  97. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.js.map +1 -1
  98. package/dist/esm/lib/container/ng-flow/ng-flow.component.d.ts +4 -8
  99. package/dist/esm/lib/container/ng-flow/ng-flow.component.js +151 -54
  100. package/dist/esm/lib/container/ng-flow/ng-flow.component.js.map +1 -1
  101. package/dist/esm/lib/container/node-renderer/node-renderer.component.d.ts +7 -1
  102. package/dist/esm/lib/container/node-renderer/node-renderer.component.js +70 -14
  103. package/dist/esm/lib/container/node-renderer/node-renderer.component.js.map +1 -1
  104. package/dist/esm/lib/container/pane/pane.component.d.ts +0 -1
  105. package/dist/esm/lib/container/pane/pane.component.js +23 -3
  106. package/dist/esm/lib/container/pane/pane.component.js.map +1 -1
  107. package/dist/esm/lib/container/viewport/viewport.component.d.ts +0 -1
  108. package/dist/esm/lib/directives/drag.directive.d.ts +1 -2
  109. package/dist/esm/lib/directives/drag.directive.js +1 -1
  110. package/dist/esm/lib/directives/drag.directive.js.map +1 -1
  111. package/dist/esm/lib/directives/drop-zone.directive.d.ts +0 -1
  112. package/dist/esm/lib/directives/key-handler.directive.d.ts +0 -1
  113. package/dist/esm/lib/directives/key-handler.directive.js +6 -2
  114. package/dist/esm/lib/directives/key-handler.directive.js.map +1 -1
  115. package/dist/esm/lib/directives/node-type.directive.d.ts +0 -1
  116. package/dist/esm/lib/graph/collapse.d.ts +35 -0
  117. package/dist/esm/lib/graph/collapse.js +102 -0
  118. package/dist/esm/lib/graph/collapse.js.map +1 -0
  119. package/dist/esm/lib/graph/group-bounds.d.ts +42 -0
  120. package/dist/esm/lib/graph/group-bounds.js +34 -0
  121. package/dist/esm/lib/graph/group-bounds.js.map +1 -0
  122. package/dist/esm/lib/layout/dagre-layout.d.ts +0 -1
  123. package/dist/esm/lib/layout/index.d.ts +1 -2
  124. package/dist/esm/lib/layout/index.js.map +1 -1
  125. package/dist/esm/lib/layout/layout-nodes.d.ts +39 -5
  126. package/dist/esm/lib/layout/layout-nodes.js +94 -7
  127. package/dist/esm/lib/layout/layout-nodes.js.map +1 -1
  128. package/dist/esm/lib/public-api.d.ts +4 -2
  129. package/dist/esm/lib/public-api.js +2 -1
  130. package/dist/esm/lib/public-api.js.map +1 -1
  131. package/dist/esm/lib/services/flow-store.service.d.ts +25 -2
  132. package/dist/esm/lib/services/flow-store.service.js +101 -14
  133. package/dist/esm/lib/services/flow-store.service.js.map +1 -1
  134. package/dist/esm/lib/services/ng-flow.service.d.ts +82 -7
  135. package/dist/esm/lib/services/ng-flow.service.js +210 -26
  136. package/dist/esm/lib/services/ng-flow.service.js.map +1 -1
  137. package/dist/esm/lib/services/tokens.d.ts +0 -1
  138. package/dist/esm/lib/types/edges.d.ts +0 -1
  139. package/dist/esm/lib/types/general.d.ts +0 -1
  140. package/dist/esm/lib/types/index.d.ts +0 -1
  141. package/dist/esm/lib/types/node-template.d.ts +2 -1
  142. package/dist/esm/lib/types/nodes.d.ts +4 -1
  143. package/dist/esm/lib/types/store.d.ts +0 -1
  144. package/dist/esm/lib/utils/changes.d.ts +28 -2
  145. package/dist/esm/lib/utils/changes.js +57 -1
  146. package/dist/esm/lib/utils/changes.js.map +1 -1
  147. package/dist/esm/lib/utils/index.d.ts +2 -2
  148. package/dist/esm/lib/utils/index.js +2 -1
  149. package/dist/esm/lib/utils/index.js.map +1 -1
  150. package/dist/esm/lib/utils/inject-flow-store.d.ts +16 -0
  151. package/dist/esm/lib/utils/inject-flow-store.js +23 -0
  152. package/dist/esm/lib/utils/inject-flow-store.js.map +1 -0
  153. package/dist/esm/lib/utils/inject-ng-flow-node.d.ts +0 -1
  154. package/dist/esm/lib/utils/position-tween.d.ts +0 -1
  155. package/dist/esm/lib/utils/template-interpolation.d.ts +0 -1
  156. package/dist/esm/lib/utils/type-guards.d.ts +0 -1
  157. package/dist/esm/lib/utils/type-guards.js +2 -0
  158. package/dist/esm/lib/utils/type-guards.js.map +1 -1
  159. package/dist/esm/test-setup.d.ts +0 -1
  160. package/dist/style.css +8 -0
  161. package/package.json +83 -78
  162. package/dist/esm/index.d.ts.map +0 -1
  163. package/dist/esm/lib/agent/agent-bridge.service.d.ts.map +0 -1
  164. package/dist/esm/lib/agent/chat/agent-chat.component.d.ts.map +0 -1
  165. package/dist/esm/lib/agent/chat/agent-chat.service.d.ts.map +0 -1
  166. package/dist/esm/lib/agent/chat/default-system-prompt.d.ts.map +0 -1
  167. package/dist/esm/lib/agent/chat/index.d.ts.map +0 -1
  168. package/dist/esm/lib/agent/chat/provide-agent-chat.d.ts.map +0 -1
  169. package/dist/esm/lib/agent/chat/types.d.ts.map +0 -1
  170. package/dist/esm/lib/agent/history.d.ts.map +0 -1
  171. package/dist/esm/lib/agent/index.d.ts.map +0 -1
  172. package/dist/esm/lib/agent/provide-agent-bridge.d.ts.map +0 -1
  173. package/dist/esm/lib/agent/tool-schemas.d.ts.map +0 -1
  174. package/dist/esm/lib/agent/transports/websocket.d.ts.map +0 -1
  175. package/dist/esm/lib/agent/transports/window.d.ts.map +0 -1
  176. package/dist/esm/lib/agent/types.d.ts.map +0 -1
  177. package/dist/esm/lib/components/a11y-descriptions/a11y-descriptions.component.d.ts.map +0 -1
  178. package/dist/esm/lib/components/attribution/attribution.component.d.ts.map +0 -1
  179. package/dist/esm/lib/components/background/background.component.d.ts.map +0 -1
  180. package/dist/esm/lib/components/connection-line/connection-line.component.d.ts.map +0 -1
  181. package/dist/esm/lib/components/controls/controls.component.d.ts.map +0 -1
  182. package/dist/esm/lib/components/edge-label-renderer/edge-label-renderer.component.d.ts.map +0 -1
  183. package/dist/esm/lib/components/edge-toolbar/edge-toolbar.component.d.ts.map +0 -1
  184. package/dist/esm/lib/components/edges/base-edge.component.d.ts.map +0 -1
  185. package/dist/esm/lib/components/edges/bezier-edge.component.d.ts.map +0 -1
  186. package/dist/esm/lib/components/edges/edge-text.component.d.ts.map +0 -1
  187. package/dist/esm/lib/components/edges/simple-bezier-edge.component.d.ts.map +0 -1
  188. package/dist/esm/lib/components/edges/smooth-step-edge.component.d.ts.map +0 -1
  189. package/dist/esm/lib/components/edges/step-edge.component.d.ts.map +0 -1
  190. package/dist/esm/lib/components/edges/straight-edge.component.d.ts.map +0 -1
  191. package/dist/esm/lib/components/handle/handle.component.d.ts.map +0 -1
  192. package/dist/esm/lib/components/handle-group/handle-group.component.d.ts.map +0 -1
  193. package/dist/esm/lib/components/handle-group/handle-row.component.d.ts.map +0 -1
  194. package/dist/esm/lib/components/minimap/minimap.component.d.ts.map +0 -1
  195. package/dist/esm/lib/components/ng-flow-provider/ng-flow-provider.component.d.ts.map +0 -1
  196. package/dist/esm/lib/components/node-resizer/node-resizer.component.d.ts.map +0 -1
  197. package/dist/esm/lib/components/node-toolbar/node-toolbar.component.d.ts.map +0 -1
  198. package/dist/esm/lib/components/nodes/default-node.component.d.ts.map +0 -1
  199. package/dist/esm/lib/components/nodes/group-node.component.d.ts.map +0 -1
  200. package/dist/esm/lib/components/nodes/input-node.component.d.ts.map +0 -1
  201. package/dist/esm/lib/components/nodes/output-node.component.d.ts.map +0 -1
  202. package/dist/esm/lib/components/nodes/template-node.component.d.ts.map +0 -1
  203. package/dist/esm/lib/components/panel/panel.component.d.ts.map +0 -1
  204. package/dist/esm/lib/components/selection-box/selection-box.component.d.ts.map +0 -1
  205. package/dist/esm/lib/components/viewport-portal/viewport-portal.component.d.ts.map +0 -1
  206. package/dist/esm/lib/container/edge-renderer/edge-renderer.component.d.ts.map +0 -1
  207. package/dist/esm/lib/container/ng-flow/ng-flow.component.d.ts.map +0 -1
  208. package/dist/esm/lib/container/node-renderer/node-renderer.component.d.ts.map +0 -1
  209. package/dist/esm/lib/container/pane/pane.component.d.ts.map +0 -1
  210. package/dist/esm/lib/container/viewport/viewport.component.d.ts.map +0 -1
  211. package/dist/esm/lib/directives/drag.directive.d.ts.map +0 -1
  212. package/dist/esm/lib/directives/drop-zone.directive.d.ts.map +0 -1
  213. package/dist/esm/lib/directives/key-handler.directive.d.ts.map +0 -1
  214. package/dist/esm/lib/directives/node-type.directive.d.ts.map +0 -1
  215. package/dist/esm/lib/layout/dagre-layout.d.ts.map +0 -1
  216. package/dist/esm/lib/layout/index.d.ts.map +0 -1
  217. package/dist/esm/lib/layout/layout-nodes.d.ts.map +0 -1
  218. package/dist/esm/lib/public-api.d.ts.map +0 -1
  219. package/dist/esm/lib/services/flow-store.service.d.ts.map +0 -1
  220. package/dist/esm/lib/services/ng-flow.service.d.ts.map +0 -1
  221. package/dist/esm/lib/services/tokens.d.ts.map +0 -1
  222. package/dist/esm/lib/types/edges.d.ts.map +0 -1
  223. package/dist/esm/lib/types/general.d.ts.map +0 -1
  224. package/dist/esm/lib/types/index.d.ts.map +0 -1
  225. package/dist/esm/lib/types/node-template.d.ts.map +0 -1
  226. package/dist/esm/lib/types/nodes.d.ts.map +0 -1
  227. package/dist/esm/lib/types/store.d.ts.map +0 -1
  228. package/dist/esm/lib/utils/changes.d.ts.map +0 -1
  229. package/dist/esm/lib/utils/index.d.ts.map +0 -1
  230. package/dist/esm/lib/utils/inject-ng-flow-node.d.ts.map +0 -1
  231. package/dist/esm/lib/utils/position-tween.d.ts.map +0 -1
  232. package/dist/esm/lib/utils/template-interpolation.d.ts.map +0 -1
  233. package/dist/esm/lib/utils/type-guards.d.ts.map +0 -1
  234. package/dist/esm/test-setup.d.ts.map +0 -1
package/README.md CHANGED
@@ -118,38 +118,55 @@ The app ships three sections:
118
118
 
119
119
  ## Custom Nodes
120
120
 
121
- Create any Angular component and register it as a node type. Use the `nodrag` CSS class on interactive elements (inputs, dropdowns) to prevent drag interference.
121
+ Create any Angular component and register it as a node type. The preferred way to read per-node state is `injectNgFlowNode<TData>()`, which returns reactive read-only signals for every property the library tracks (`id`, `data`, `selected`, `dragging`, `zIndex`, `isConnectable`, `position`, `sourcePosition`, `targetPosition`, `dragHandle`, `type`, `collapsed`). Use the `nodrag` CSS class on interactive elements (inputs, dropdowns) to prevent drag interference.
122
122
 
123
123
  ```typescript
124
+ import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
125
+ import {
126
+ HandleComponent,
127
+ Position,
128
+ NgFlowService,
129
+ injectNgFlowNode,
130
+ } from '@angflow/angular';
131
+
132
+ interface FormData { title: string; name: string; type: string }
133
+
124
134
  @Component({
125
135
  selector: 'app-form-node',
126
136
  standalone: true,
137
+ changeDetection: ChangeDetectionStrategy.OnPush,
127
138
  imports: [HandleComponent],
128
139
  template: `
129
- <ng-flow-handle type="target" [position]="Position.Top" />
130
- <div class="my-node">
131
- <h3>{{ data()?.title }}</h3>
140
+ <ng-flow-handle type="target" [position]="Position.Top" [isConnectable]="node.isConnectable()" />
141
+ <div class="my-node" [class.selected]="node.selected()">
142
+ <h3>{{ node.data()?.title }}</h3>
132
143
  <div class="nodrag">
133
- <input [value]="data()?.name" (input)="onNameChange($event)" />
134
- <select [value]="data()?.type" (change)="onTypeChange($event)">
144
+ <input [value]="node.data()?.name" (input)="onNameChange($event)" />
145
+ <select [value]="node.data()?.type" (change)="onTypeChange($event)">
135
146
  <option value="string">String</option>
136
147
  <option value="number">Number</option>
137
148
  </select>
138
149
  </div>
139
150
  </div>
140
- <ng-flow-handle type="source" [position]="Position.Bottom" />
151
+ <ng-flow-handle type="source" [position]="Position.Bottom" [isConnectable]="node.isConnectable()" />
141
152
  `,
142
153
  })
143
154
  export class FormNodeComponent {
144
155
  readonly Position = Position;
145
- readonly id = input.required<string>();
146
- readonly data = input<any>();
147
- // ... other standard inputs: type, selected, dragging, zIndex, etc.
156
+ readonly node = injectNgFlowNode<FormData>();
148
157
 
149
158
  private flowService = inject(NgFlowService);
150
159
 
151
160
  onNameChange(event: Event) {
152
- this.flowService.updateNodeData(this.id(), { name: (event.target as HTMLInputElement).value });
161
+ this.flowService.updateNodeData(this.node.id(), {
162
+ name: (event.target as HTMLInputElement).value,
163
+ });
164
+ }
165
+
166
+ onTypeChange(event: Event) {
167
+ this.flowService.updateNodeData(this.node.id(), {
168
+ type: (event.target as HTMLSelectElement).value,
169
+ });
153
170
  }
154
171
  }
155
172
  ```
@@ -164,6 +181,24 @@ nodeTypes = { formNode: FormNodeComponent };
164
181
  <ng-flow [nodes]="nodes" [edges]="edges" [nodeTypes]="nodeTypes" ...>
165
182
  ```
166
183
 
184
+ #### Legacy: flat `@Input()`s
185
+
186
+ The original API — declaring one `@Input()` per tracked property — is still fully supported, so existing custom nodes keep working without changes:
187
+
188
+ ```typescript
189
+ export class FormNodeComponent {
190
+ readonly Position = Position;
191
+ readonly id = input.required<string>();
192
+ readonly data = input<FormData>();
193
+ readonly selected = input(false);
194
+ readonly isConnectable = input(true);
195
+ // ...plus type, dragging, zIndex, positionAbsoluteX/Y,
196
+ // sourcePosition, targetPosition, dragHandle as needed
197
+ }
198
+ ```
199
+
200
+ Prefer `injectNgFlowNode()` for new code: one injection call replaces ~13 input declarations, the signals are reactive in the same way, and you only pull the fields you actually use.
201
+
167
202
  ## Programmatic API
168
203
 
169
204
  Inject `NgFlowService` for viewport control, node/edge manipulation, and spatial queries:
@@ -212,10 +247,30 @@ flow.applyLayout(layoutNodes, { direction: 'LR' });
212
247
  ```
213
248
 
214
249
  Options: `direction` (`'TB' | 'LR' | 'BT' | 'RL'`, default `'TB'`), `nodeSep`
215
- (default 50), `rankSep` (default 80). Node dimensions resolve from
216
- `measured` → `width`/`height` → `initialWidth`/`initialHeight` → 150×40. Any
217
- function with the same shape plugs
218
- into `applyLayout` (elk, custom grids, …).
250
+ (default 50), `rankSep` (default 80). Node dimensions resolve from `measured` → `width`/`height` →
251
+ `initialWidth`/`initialHeight` → 150×40, and edge labels reserve dagre space from
252
+ `labelWidth`/`labelHeight` (auto-measured by `applyLayout`) or a default box when an
253
+ edge has a non-empty `label`. Any function with the same shape plugs into
254
+ `applyLayout` (elk, custom grids, …).
255
+
256
+ ### Controlled mode and `measured`
257
+
258
+ In controlled mode (`[nodes]` bound, re-emitted from `(nodesChange)`), re-emitting
259
+ nodes that don't carry `measured` resets the stored dimensions. `applyLayout` reads
260
+ live DOM sizes so layout stays correct regardless — but floating edges and `fitView`
261
+ read the stored `measured` directly. If your app hand-handles only some changes (e.g.
262
+ keeps `position` authority in a journal), forward dimension changes with
263
+ `applyDimensionChanges` so `measured` stays current:
264
+
265
+ ```ts
266
+ import { applyDimensionChanges } from '@angflow/angular';
267
+
268
+ onNodesChange(changes: NodeChange[]) {
269
+ // keep measured flowing back: floating edges, fitView, layout stay correct
270
+ this.nodes.update((ns) => applyDimensionChanges(changes, ns));
271
+ // ...your own position/data handling on top...
272
+ }
273
+ ```
219
274
 
220
275
  ## Animations
221
276
 
@@ -233,6 +288,44 @@ Animated paths: `setNodePositions`, `applyLayout`, and the agent bridge's
233
288
  tween on that node, and everything is disabled under `prefers-reduced-motion`.
234
289
  Per-call override: `flow.setNodePositions(positions, { animate: false })`.
235
290
 
291
+ ## Group Collapse
292
+
293
+ Set `collapsed: true` on a group/parent node and angflow folds it: descendants stop
294
+ rendering, the box drops to a header strip (`.collapsed` CSS class), and edges crossing
295
+ the boundary reroute onto the box (parallels merge). It is nesting-aware — edges reroute
296
+ to the outermost collapsed ancestor.
297
+
298
+ ```ts
299
+ flow.setNodeCollapsed(groupId, true); // or toggleNodeCollapsed(groupId)
300
+ ```
301
+
302
+ `collapsed` lives on the node, so in controlled mode it round-trips through
303
+ `(nodesChange)` like any other field. A merged (deduped) display edge is render-only and
304
+ carries its underlying edge ids on `collapsedFrom`; a 1:1 rerouted edge keeps its original
305
+ identity. Rerouted edges attach to the box via the normal edge geometry — cleanest under
306
+ `edgeMode="floating"`. The collapsed box renders at `--xy-node-collapsed-height` (40px
307
+ default); auto-sizing the expanded box to its children is separate.
308
+
309
+ ## Group Auto-Size
310
+
311
+ Compute a box that wraps a group's members, or have the service size a group for you:
312
+
313
+ ```ts
314
+ import { getGroupBounds } from '@angflow/angular';
315
+
316
+ // Pure: bounds in the same coordinate space as the members you pass.
317
+ const box = getGroupBounds(members, { padding: 24, headerHeight: 40 });
318
+
319
+ // Imperative: size + position a group to wrap its children, keeping them pinned.
320
+ await flow.sizeGroupToChildren(groupId, { padding: 24, headerHeight: 40 });
321
+ ```
322
+
323
+ `getGroupBounds` resolves member sizes `measured → width → 0` and applies an asymmetric top
324
+ (`headerHeight`) vs. other-sides (`padding`) inset. `sizeGroupToChildren` sets the group's
325
+ `width`/`height` and moves its top-left to wrap its `parentId` children, re-basing the children so
326
+ they stay visually fixed (nested groups handled via absolute-coordinate translation). It is a no-op
327
+ for a childless group.
328
+
236
329
  ## Architecture
237
330
 
238
331
  - **Signal-based state** — Angular 17+ signals for fine-grained reactivity (no RxJS in the store)
package/dist/base.css CHANGED
@@ -955,6 +955,14 @@ svg.xy-flow__connectionline {
955
955
  }
956
956
 
957
957
 
958
+ .xy-flow__node.collapsed {
959
+ /* Folded group: drop the expanded footprint to a header strip. Overrides an
960
+ inline style.height set in controlled mode; apps may extend this rule. */
961
+ height: var(--xy-node-collapsed-height, 40px) !important;
962
+ overflow: hidden;
963
+ }
964
+
965
+
958
966
  /* Entry animation — applied by the node renderer when [animate] is enabled.
959
967
  Uses the standalone `scale` property so it composes with the inline
960
968
  translate() transform instead of overriding it. */
@@ -1,2 +1 @@
1
1
  export * from './lib/public-api';
2
- //# sourceMappingURL=index.d.ts.map
@@ -48,6 +48,7 @@ export declare class AngflowAgentBridge {
48
48
  private readonly history;
49
49
  private readonly handlers;
50
50
  private readonly injector;
51
+ private readonly destroyRef;
51
52
  private readonly layoutFn;
52
53
  private started;
53
54
  private nextInProcessId;
@@ -82,6 +83,8 @@ export declare class AngflowAgentBridge {
82
83
  */
83
84
  callTool(method: string, params?: Record<string, unknown>): Promise<unknown>;
84
85
  private start;
86
+ /** Stop all transports. Invoked automatically when the owning injector is destroyed. */
87
+ private stop;
85
88
  private dispatch;
86
89
  private findFlowId;
87
90
  private emitHistory;
@@ -92,4 +95,3 @@ export declare class AngflowAgentBridge {
92
95
  static ɵfac: i0.ɵɵFactoryDeclaration<AngflowAgentBridge, [{ optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }]>;
93
96
  static ɵprov: i0.ɵɵInjectableDeclaration<AngflowAgentBridge>;
94
97
  }
95
- //# sourceMappingURL=agent-bridge.service.d.ts.map
@@ -1,4 +1,4 @@
1
- import { Inject, Injectable, InjectionToken, Injector, Optional, effect, inject, runInInjectionContext, signal, } from '@angular/core';
1
+ import { DestroyRef, Inject, Injectable, InjectionToken, Injector, Optional, effect, inject, runInInjectionContext, signal, } from '@angular/core';
2
2
  import { AgentHistory } from './history';
3
3
  import { AGENT_TOOL_SCHEMAS } from './tool-schemas';
4
4
  import * as i0 from "@angular/core";
@@ -27,6 +27,9 @@ const MUTATING_TOOLS = new Set([
27
27
  'set_nodes',
28
28
  'set_edges',
29
29
  ]);
30
+ // apply_changes is treated specially — see dispatch logic.
31
+ /** Max flow.state emission rate while any node drag is in progress. */
32
+ const DRAG_STATE_EMIT_INTERVAL_MS = 100;
30
33
  /**
31
34
  * Routes JSON-RPC requests from one or more transports to registered
32
35
  * `NgFlowService` instances, and pushes change events back to the agent.
@@ -58,6 +61,7 @@ export class AngflowAgentBridge {
58
61
  this.flows = new Map();
59
62
  this.handlers = new Map();
60
63
  this.injector = inject(Injector);
64
+ this.destroyRef = inject(DestroyRef);
61
65
  this.started = false;
62
66
  this.nextInProcessId = 1;
63
67
  this.warnedOnBeforeDeleteBypass = false;
@@ -70,6 +74,7 @@ export class AngflowAgentBridge {
70
74
  this.layoutFn = layoutFn ?? null;
71
75
  this.installHandlers();
72
76
  this.start();
77
+ this.destroyRef.onDestroy(() => this.stop());
73
78
  }
74
79
  reportError(err, ctx) {
75
80
  if (!this.onError)
@@ -161,6 +166,20 @@ export class AngflowAgentBridge {
161
166
  }
162
167
  }
163
168
  }
169
+ /** Stop all transports. Invoked automatically when the owning injector is destroyed. */
170
+ stop() {
171
+ if (!this.started)
172
+ return;
173
+ this.started = false;
174
+ for (const t of this.transports) {
175
+ try {
176
+ t.stop();
177
+ }
178
+ catch {
179
+ // A transport that throws during teardown must not break the others.
180
+ }
181
+ }
182
+ }
164
183
  async dispatch(req) {
165
184
  const handler = this.handlers.get(req.method);
166
185
  if (!handler) {
@@ -172,6 +191,7 @@ export class AngflowAgentBridge {
172
191
  try {
173
192
  const params = req.params ?? {};
174
193
  if (req.method === 'list_flows') {
194
+ // list_flows ignores the service arg; null stub avoids resolving a flow.
175
195
  const result = await handler(null, params);
176
196
  return { id: req.id, result };
177
197
  }
@@ -297,6 +317,35 @@ export class AngflowAgentBridge {
297
317
  let pending = false;
298
318
  let destroyed = false;
299
319
  let lastSignature = '';
320
+ let lastEmitTime = Number.NEGATIVE_INFINITY;
321
+ let trailingTimer = null;
322
+ const emitState = (isDrag) => {
323
+ // Re-read at emit time so coalesced/throttled bursts see the latest state.
324
+ const params = {
325
+ flowId: id,
326
+ nodes: flow.getNodes(),
327
+ edges: flow.getEdges(),
328
+ viewport: flow.getViewport(),
329
+ selection: {
330
+ nodeIds: flow.selectedNodes().map((n) => n.id),
331
+ edgeIds: flow.selectedEdges().map((e) => e.id),
332
+ },
333
+ };
334
+ const sig = signatureOf(params);
335
+ if (sig === lastSignature)
336
+ return;
337
+ lastSignature = sig;
338
+ // Only record the drag-emission timestamp when actually in a drag — so a
339
+ // non-drag emission (register, mutation tool) does not push back the next
340
+ // drag frame's throttle window.
341
+ if (isDrag) {
342
+ lastEmitTime = Date.now();
343
+ }
344
+ else {
345
+ lastEmitTime = Number.NEGATIVE_INFINITY;
346
+ }
347
+ this.emit({ event: 'flow.state', params });
348
+ };
300
349
  const ref = runInInjectionContext(this.injector, () => effect(() => {
301
350
  // Touch every signal we want to broadcast so the effect re-runs on change.
302
351
  flow.nodes();
@@ -314,29 +363,39 @@ export class AngflowAgentBridge {
314
363
  // push state for a flowId that's no longer registered.
315
364
  if (destroyed)
316
365
  return;
317
- // Re-read on flush so coalesced bursts see the latest state.
318
- const params = {
319
- flowId: id,
320
- nodes: flow.getNodes(),
321
- edges: flow.getEdges(),
322
- viewport: flow.getViewport(),
323
- selection: {
324
- nodeIds: flow.selectedNodes().map((n) => n.id),
325
- edgeIds: flow.selectedEdges().map((e) => e.id),
326
- },
327
- };
328
- // Suppress duplicate emissions when controlled-mode round-trips bounce
329
- // identical state through the store twice. See signatureOf for the
330
- // field set; it must cover anything a mutating tool can change.
331
- const sig = signatureOf(params);
332
- if (sig === lastSignature)
333
- return;
334
- lastSignature = sig;
335
- this.emit({ event: 'flow.state', params });
366
+ // While a drag is in flight the store re-emits nodes per pointer
367
+ // frame; serializing the whole graph at that rate floods every
368
+ // transport. Throttle to one emission per interval with a trailing
369
+ // emit so the final drag state is never lost. The timer only
370
+ // schedules the emission — no view state is written here.
371
+ const dragging = flow.getNodes().some((n) => n.dragging === true);
372
+ if (dragging) {
373
+ const elapsed = Date.now() - lastEmitTime;
374
+ if (elapsed < DRAG_STATE_EMIT_INTERVAL_MS) {
375
+ if (trailingTimer === null) {
376
+ trailingTimer = setTimeout(() => {
377
+ trailingTimer = null;
378
+ if (destroyed)
379
+ return;
380
+ emitState(true);
381
+ }, DRAG_STATE_EMIT_INTERVAL_MS - elapsed);
382
+ }
383
+ return;
384
+ }
385
+ }
386
+ if (trailingTimer !== null) {
387
+ clearTimeout(trailingTimer);
388
+ trailingTimer = null;
389
+ }
390
+ emitState(dragging);
336
391
  });
337
392
  }));
338
393
  return () => {
339
394
  destroyed = true;
395
+ if (trailingTimer !== null) {
396
+ clearTimeout(trailingTimer);
397
+ trailingTimer = null;
398
+ }
340
399
  ref.destroy();
341
400
  };
342
401
  }
@@ -586,7 +645,6 @@ export class AngflowAgentBridge {
586
645
  const hasDelete = ops.some((o) => o['op'] === 'delete_elements');
587
646
  if (hasDelete) {
588
647
  this.warnedOnBeforeDeleteBypass = true;
589
- // eslint-disable-next-line no-console
590
648
  console.warn('[angflow] apply_changes/delete_elements bypasses onBeforeDelete. ' +
591
649
  'Call the standalone `delete_elements` tool if you need the veto hook.');
592
650
  }
@@ -740,6 +798,10 @@ export class AngflowAgentBridge {
740
798
  width: internal?.measured?.width ?? n.width ?? 150,
741
799
  height: internal?.measured?.height ?? n.height ?? 40,
742
800
  position: { x: n.position.x, y: n.position.y },
801
+ // Forward parentId only when the parent is also being laid out, so the
802
+ // layout fn clusters grouped children within their group. Excluding the
803
+ // parent (e.g. via nodeIds) drops it — the child is then a free node.
804
+ parentId: n.parentId != null && idSet.has(n.parentId) ? n.parentId : undefined,
743
805
  };
744
806
  });
745
807
  // Induced subgraph: only edges with BOTH endpoints in the target set.
@@ -774,7 +836,6 @@ export class AngflowAgentBridge {
774
836
  applied[id] = { x: pos.x, y: pos.y };
775
837
  }
776
838
  if (unknownIds.length > 0) {
777
- // eslint-disable-next-line no-console
778
839
  console.warn(`[angflow] layout_nodes: layout function returned positions for unknown node ids ` +
779
840
  `(ignored): ${unknownIds.join(', ')}`);
780
841
  }
@@ -789,7 +850,10 @@ export class AngflowAgentBridge {
789
850
  }
790
851
  // Honors the host's [animate] input: positions tween when it's on, and
791
852
  // the await keeps the subsequent fitView measuring settled positions.
792
- await flow.setNodePositions(actuallyApplied);
853
+ // Layout fns emit flow-absolute coordinates, so apply in absolute space:
854
+ // setNodePositions resolves each parented child against its parent's new
855
+ // position, keeping grouped children inside their group.
856
+ await flow.setNodePositions(actuallyApplied, { coordinateSpace: 'absolute' });
793
857
  const shouldFit = params['fitView'] !== false;
794
858
  if (shouldFit && Object.keys(actuallyApplied).length > 0) {
795
859
  try {
@@ -899,6 +963,9 @@ function executeOp(flow, op) {
899
963
  const nodes = op['nodes'];
900
964
  if (!Array.isArray(nodes))
901
965
  throw new InvalidParamsError('add_nodes: "nodes" must be an array.');
966
+ if (nodes.length > MAX_BULK_ELEMENTS) {
967
+ throw new InvalidParamsError(`add_nodes: "nodes" exceeds the maximum of ${MAX_BULK_ELEMENTS} elements per call (got ${nodes.length}).`);
968
+ }
902
969
  const validated = nodes.map((n, i) => validateNodeShape(n, `apply_changes/add_nodes[${i}]`));
903
970
  flow.addNodes(validated);
904
971
  return validated.map((n) => flow.getNode(n.id)).filter((n) => !!n);
@@ -912,6 +979,9 @@ function executeOp(flow, op) {
912
979
  const edges = op['edges'];
913
980
  if (!Array.isArray(edges))
914
981
  throw new InvalidParamsError('add_edges: "edges" must be an array.');
982
+ if (edges.length > MAX_BULK_ELEMENTS) {
983
+ throw new InvalidParamsError(`add_edges: "edges" exceeds the maximum of ${MAX_BULK_ELEMENTS} elements per call (got ${edges.length}).`);
984
+ }
915
985
  const validated = edges.map((e, i) => validateEdgeShape(e, `apply_changes/add_edges[${i}]`));
916
986
  flow.addEdges(validated);
917
987
  return validated.map((e) => flow.getEdge(e.id)).filter((e) => !!e);
@@ -1020,10 +1090,15 @@ function requireObject(params, key) {
1020
1090
  }
1021
1091
  return value;
1022
1092
  }
1093
+ /** Hard cap on elements per bulk call (nodes, edges, apply_changes ops). */
1094
+ const MAX_BULK_ELEMENTS = 5000;
1023
1095
  function requireArray(params, key) {
1024
1096
  const value = params[key];
1025
1097
  if (!Array.isArray(value))
1026
1098
  throw new InvalidParamsError(`Param "${key}" must be an array.`);
1099
+ if (value.length > MAX_BULK_ELEMENTS) {
1100
+ throw new InvalidParamsError(`Param "${key}" exceeds the maximum of ${MAX_BULK_ELEMENTS} elements per call (got ${value.length}).`);
1101
+ }
1027
1102
  return value;
1028
1103
  }
1029
1104
  function optionalStringArray(params, key) {
@@ -1033,6 +1108,9 @@ function optionalStringArray(params, key) {
1033
1108
  if (!Array.isArray(value) || value.some((v) => typeof v !== 'string')) {
1034
1109
  throw new InvalidParamsError(`Param "${key}" must be an array of strings.`);
1035
1110
  }
1111
+ if (value.length > MAX_BULK_ELEMENTS) {
1112
+ throw new InvalidParamsError(`Param "${key}" exceeds the maximum of ${MAX_BULK_ELEMENTS} elements per call (got ${value.length}).`);
1113
+ }
1036
1114
  return value;
1037
1115
  }
1038
1116
  const BADGE_COLOR_SET = new Set(['slate', 'indigo', 'emerald', 'amber', 'rose']);
@@ -1116,6 +1194,33 @@ function validateTemplateSpec(value, ctx) {
1116
1194
  }
1117
1195
  return value;
1118
1196
  }
1197
+ /**
1198
+ * CSS values that can fetch remote resources (`url(`) or execute in legacy
1199
+ * engines (`expression(`). Angular's style sanitization already blocks
1200
+ * script execution, so this is a narrow redressing/beaconing guard rather
1201
+ * than a full CSS allowlist.
1202
+ */
1203
+ const CSS_VALUE_BLOCKLIST = /url\s*\(|expression\s*\(/i;
1204
+ /** Shared style/className validation for agent-supplied nodes and edges. */
1205
+ function validateStyleAndClassName(o, ctx, kind) {
1206
+ const style = o['style'];
1207
+ if (style !== undefined) {
1208
+ if (!style || typeof style !== 'object' || Array.isArray(style)) {
1209
+ throw new InvalidParamsError(`${ctx}: ${kind}.style must be a plain object of CSS property/value pairs.`);
1210
+ }
1211
+ for (const [prop, raw] of Object.entries(style)) {
1212
+ if (typeof raw !== 'string' && typeof raw !== 'number') {
1213
+ throw new InvalidParamsError(`${ctx}: ${kind}.style["${prop}"] must be a string or number.`);
1214
+ }
1215
+ if (typeof raw === 'string' && CSS_VALUE_BLOCKLIST.test(raw)) {
1216
+ throw new InvalidParamsError(`${ctx}: ${kind}.style["${prop}"] must not contain "url(" or "expression(".`);
1217
+ }
1218
+ }
1219
+ }
1220
+ if (o['className'] !== undefined && typeof o['className'] !== 'string') {
1221
+ throw new InvalidParamsError(`${ctx}: ${kind}.className must be a string.`);
1222
+ }
1223
+ }
1119
1224
  /** Validate that `value` is a structurally valid Node payload for add_*. */
1120
1225
  function validateNodeShape(value, ctx) {
1121
1226
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -1136,6 +1241,7 @@ function validateNodeShape(value, ctx) {
1136
1241
  if (!Number.isFinite(p['x']) || !Number.isFinite(p['y'])) {
1137
1242
  throw new InvalidParamsError(`${ctx}: node.position.{x,y} must be finite (no NaN/Infinity).`);
1138
1243
  }
1244
+ validateStyleAndClassName(n, ctx, 'node');
1139
1245
  return value;
1140
1246
  }
1141
1247
  /** Validate that `value` is a structurally valid Edge payload for add_*. */
@@ -1153,6 +1259,7 @@ function validateEdgeShape(value, ctx) {
1153
1259
  if (typeof e['target'] !== 'string' || e['target'].length === 0) {
1154
1260
  throw new InvalidParamsError(`${ctx}: edge.target must be a non-empty string.`);
1155
1261
  }
1262
+ validateStyleAndClassName(e, ctx, 'edge');
1156
1263
  return value;
1157
1264
  }
1158
1265
  //# sourceMappingURL=agent-bridge.service.js.map