@bemedev/mind-flow 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +10 -10
  2. package/lib/helpers/createContext.d.ts +7 -2
  3. package/lib/helpers/createContext.d.ts.map +1 -1
  4. package/lib/index.cjs.js +1 -1
  5. package/lib/index.es.js +556 -510
  6. package/lib/output.css +1 -1
  7. package/lib/server.cjs.js +653 -397
  8. package/lib/server.es.js +657 -401
  9. package/lib/services/main.machine.d.ts +4857 -218
  10. package/lib/services/main.machine.d.ts.map +1 -1
  11. package/lib/{ui/components/FlowChart.data.d.ts → services/main.machine.data.d.ts} +8 -3
  12. package/lib/services/main.machine.data.d.ts.map +1 -0
  13. package/lib/services/main.machine.helpers.d.ts +32 -0
  14. package/lib/services/main.machine.helpers.d.ts.map +1 -0
  15. package/lib/services/main.machine.typings.d.ts +126 -0
  16. package/lib/services/main.machine.typings.d.ts.map +1 -0
  17. package/lib/ui/Flow.d.ts +2 -0
  18. package/lib/ui/Flow.d.ts.map +1 -1
  19. package/lib/ui/components/Bounds.d.ts +2 -0
  20. package/lib/ui/components/Bounds.d.ts.map +1 -1
  21. package/lib/ui/components/EdgeComponent.d.ts +3 -2
  22. package/lib/ui/components/EdgeComponent.d.ts.map +1 -1
  23. package/lib/ui/components/EdgesBoard.d.ts +2 -0
  24. package/lib/ui/components/EdgesBoard.d.ts.map +1 -1
  25. package/lib/ui/components/FlowChart.context.d.ts +5608 -996
  26. package/lib/ui/components/FlowChart.context.d.ts.map +1 -1
  27. package/lib/ui/components/FlowChart.d.ts +3 -1
  28. package/lib/ui/components/FlowChart.d.ts.map +1 -1
  29. package/lib/ui/components/NodeComponent.d.ts +2 -10
  30. package/lib/ui/components/NodeComponent.d.ts.map +1 -1
  31. package/lib/ui/components/NodesBoard.d.ts +2 -0
  32. package/lib/ui/components/NodesBoard.d.ts.map +1 -1
  33. package/package.json +9 -15
  34. package/src/helpers/createContext.ts +7 -2
  35. package/src/{ui/components/FlowChart.data.ts → services/main.machine.data.ts} +7 -2
  36. package/src/services/main.machine.helpers.ts +55 -0
  37. package/src/services/main.machine.ts +288 -70
  38. package/src/services/main.machine.typings.ts +98 -0
  39. package/src/ui/Flow.tsx +2 -0
  40. package/src/ui/components/Bounds.tsx +13 -8
  41. package/src/ui/components/EdgeComponent.tsx +78 -56
  42. package/src/ui/components/EdgesBoard.tsx +13 -36
  43. package/src/ui/components/FlowChart.context.ts +204 -291
  44. package/src/ui/components/FlowChart.tsx +16 -16
  45. package/src/ui/components/NodeComponent.tsx +60 -104
  46. package/src/ui/components/NodesBoard.tsx +85 -47
  47. package/src/ui/components/classes.ts +1 -1
  48. package/lib/services/main.typings.d.ts +0 -68
  49. package/lib/services/main.typings.d.ts.map +0 -1
  50. package/lib/ui/components/FlowChart.data.d.ts.map +0 -1
  51. package/src/services/main.typings.ts +0 -50
package/lib/server.es.js CHANGED
@@ -1,13 +1,11 @@
1
1
  import { createComponent, escape, ssr, ssrAttribute, ssrHydrationKey, ssrStyleProperty } from "solid-js/web";
2
- import { For, Show, createContext, createEffect, createMemo, createSignal, on, onCleanup, onMount, useContext } from "solid-js";
3
- import { createMachine, interpret } from "@bemedev/app";
4
- import { dequal } from "dequal/lite";
5
- import { produce } from "solid-js/store";
6
- import { type } from "@bemedev/app/bemedev";
2
+ import { createState } from "@bemedev/app-solidjs";
3
+ import { For, Show, createContext, createEffect, createSignal, on, onCleanup, onMount, useContext } from "solid-js";
4
+ import { createMachine, interpret, toArray } from "@bemedev/app";
5
+ import { toArray as toArray$1, type } from "@bemedev/app/bemedev";
7
6
  import { nanoid } from "nanoid";
8
- import { useState } from "@bemedev/app-solidjs";
9
7
  import { DragDropProvider, DragDropSensors, DragOverlay, createDraggable, useDragDropContext } from "@thisbeyond/solid-dnd";
10
- import { dequal as dequal$1 } from "dequal";
8
+ import { dequal } from "dequal";
11
9
  //#region src/ui/components/classes.ts
12
10
  /**
13
11
  * List of Tailwind CSS class names used across flowchart UI components to ensure
@@ -34,7 +32,7 @@ var CLASSES = [
34
32
  "w-full",
35
33
  "h-full",
36
34
  "overflow-hidden",
37
- "overflow-scroll",
35
+ "overflow-auto",
38
36
  "h-[150vh]",
39
37
  "w-[2160px]",
40
38
  "bg-size-[30px_30px]",
@@ -94,12 +92,56 @@ var CLASSES = [
94
92
  "cursor-crosshair",
95
93
  "mt-3"
96
94
  ];
95
+ /** X-axis center offset in pixels from the node container edge. */
96
+ var HANDLE_CENTER_X_OFFSET = 10.5;
97
+ /** Default 2D offset coordinates for input connection handles. */
98
+ var DEFAULT_INPUT_OFFSET = {
99
+ x: -10.5,
100
+ y: 18
101
+ };
102
+ /**
103
+ * Computes default output handle offset coordinates given a node width.
104
+ *
105
+ * @param width - Node width in pixels, defaults to `0`.
106
+ *
107
+ * @returns 2D offset coordinates for the output handle.
108
+ *
109
+ * @see {@linkcode HANDLE_CENTER_X_OFFSET}, {@linkcode HANDLE_CENTER_Y}
110
+ */
111
+ var getDefaultOutputOffset = (width = 0) => ({
112
+ x: width + HANDLE_CENTER_X_OFFSET,
113
+ y: 18
114
+ });
115
+ /**
116
+ * Boundary padding constraints for node dragging and positioning within the
117
+ * viewport.
118
+ */
119
+ var BOUNDS_CONSTRAINTS = {
120
+ horizontal: 55,
121
+ vertical: 40
122
+ };
123
+ /**
124
+ * Default node items of type {@linkcode NodeProps} provided when no initial
125
+ * configuration is given.
126
+ */
127
+ var DEFAULT_NODES = [{
128
+ id: "node-0",
129
+ data: {
130
+ content: "Some text",
131
+ label: "Root node"
132
+ },
133
+ input: false,
134
+ position: {
135
+ x: 350,
136
+ y: 100
137
+ }
138
+ }];
97
139
  //#endregion
98
140
  //#region src/helpers/createContext.ts
99
141
  /**
100
142
  * Creates a Solid context provider component and a custom hook to consume it.
101
143
  *
102
- * @template T - The type of the context value.
144
+ * @template `T` - The type of the context value.
103
145
  *
104
146
  * @param context - Factory function returning a non-nullable context value of type
105
147
  * `NonNullable<T>`.
@@ -107,6 +149,8 @@ var CLASSES = [
107
149
  *
108
150
  * @returns A tuple containing the Provider component of type
109
151
  * {@linkcode ParentComponent}, the accessor hook, and the Solid context object.
152
+ *
153
+ * @see {@linkcode createSolidContext}, {@linkcode useContext}
110
154
  */
111
155
  var createContext$1 = (context, options) => {
112
156
  const _context = createContext(context(), options);
@@ -122,7 +166,66 @@ var createContext$1 = (context, options) => {
122
166
  ];
123
167
  };
124
168
  //#endregion
125
- //#region src/services/main.typings.ts
169
+ //#region src/services/main.machine.helpers.ts
170
+ /**
171
+ * Constructs a unique edge identifier string from source and destination node IDs.
172
+ *
173
+ * @param out - The source node ID string.
174
+ * @param _in - The destination node ID string.
175
+ *
176
+ * @returns Formatted edge identifier string.
177
+ */
178
+ var buildEdgeId = (out, _in) => {
179
+ return `edge = ${out} => ${_in}`;
180
+ };
181
+ /**
182
+ * Constructs a formatted node identifier string from a generated ID.
183
+ *
184
+ * @param generated - The generated unique ID string or `null`/`undefined`.
185
+ *
186
+ * @returns Formatted node identifier string.
187
+ */
188
+ var buildNodeID = (generated) => {
189
+ return `node-${generated}`;
190
+ };
191
+ /**
192
+ * Calculates node dimensions, connection points, and handle offsets from position
193
+ * and optional parent dimension.
194
+ *
195
+ * @param position - Node position in board coordinates of type {@linkcode Point}.
196
+ * @param parentDimension - Optional parent node dimension of type
197
+ * {@linkcode Dimension} to inherit sizing and offsets from.
198
+ *
199
+ * @returns Node layout dimension object of type {@linkcode Dimension}.
200
+ *
201
+ * @see {@linkcode getDefaultOutputOffset}, {@linkcode DEFAULT_INPUT_OFFSET}
202
+ */
203
+ var calculateDimensions = (position, parentDimension = {
204
+ width: 192,
205
+ height: 50,
206
+ inputOffset: DEFAULT_INPUT_OFFSET
207
+ }) => {
208
+ const width = parentDimension.width;
209
+ const height = parentDimension.height;
210
+ const outputOffset = parentDimension.outputOffset ?? getDefaultOutputOffset(width);
211
+ const inputOffset = parentDimension.inputOffset;
212
+ return {
213
+ width,
214
+ height,
215
+ output: {
216
+ x: position.x + outputOffset.x,
217
+ y: position.y + outputOffset.y
218
+ },
219
+ input: {
220
+ x: position.x + inputOffset.x,
221
+ y: position.y + inputOffset.y
222
+ },
223
+ outputOffset,
224
+ inputOffset
225
+ };
226
+ };
227
+ //#endregion
228
+ //#region src/services/main.machine.typings.ts
126
229
  /** Schema definition for 2D coordinates `(x, y)`. */
127
230
  var point = type({
128
231
  x: "number",
@@ -132,12 +235,16 @@ type(({ optional, use }) => ({
132
235
  input: optional(use(point)),
133
236
  output: use(point)
134
237
  }));
135
- /** Schema definition for edge extremities */
238
+ /** Schema definition for edge extremities. */
136
239
  var extremities = type({
137
240
  from: "string",
138
241
  to: "string"
139
242
  });
140
- /** Schema definition for a serialized flowchart node entity. */
243
+ /**
244
+ * Schema definition for a serialized flowchart node entity.
245
+ *
246
+ * @see {@linkcode point}
247
+ */
141
248
  var nodeJSON = type(({ optional, use }) => ({
142
249
  position: use(point),
143
250
  data: {
@@ -146,50 +253,68 @@ var nodeJSON = type(({ optional, use }) => ({
146
253
  },
147
254
  input: "boolean"
148
255
  }));
149
- /** Schema definition for a serialized flowchart edge entity. */
256
+ /**
257
+ * Schema definition for a serialized flowchart edge entity.
258
+ *
259
+ * @see {@linkcode extremities}
260
+ */
150
261
  var edgeJSON = extremities;
151
- type(({ optional, use }) => ({
262
+ /**
263
+ * Schema definition for layout dimensions and connection points of a node.
264
+ *
265
+ * @see {@linkcode point}
266
+ */
267
+ var dimension = type(({ optional, use }) => ({
152
268
  width: "number",
153
269
  height: "number",
154
- id: "string",
155
270
  output: use(point),
156
- input: optional(use(point))
271
+ input: optional(use(point)),
272
+ inputOffset: optional(use(point)),
273
+ outputOffset: optional(use(point))
157
274
  }));
158
- type({
275
+ /**
276
+ * Schema definition for a 2D line vector representing edge coordinates `(x0, y0)` to
277
+ * `(x1, y1)`.
278
+ */
279
+ var vector = type({
159
280
  x0: "number",
160
281
  y0: "number",
161
282
  x1: "number",
162
283
  y1: "number"
163
284
  });
164
- //#endregion
165
- //#region src/services/main.machine.ts
166
285
  /**
167
- * Constructs a unique edge identifier string from source and destination node IDs.
168
- *
169
- * @param out - The source node ID string.
170
- * @param _in - The destination node ID string.
171
- *
172
- * @returns Formatted edge identifier string.
173
- */
174
- var buildEdgeId = (out, _in) => {
175
- return `edge = ${out} => ${_in}`;
176
- };
177
- /**
178
- * Constructs a formatted node identifier string from a generated ID.
179
- *
180
- * @param generated - The generated unique ID string or `null`/`undefined`.
286
+ * Schema definition for an ongoing new connection edge creation preview between
287
+ * source node and cursor position.
181
288
  *
182
- * @returns Formatted node identifier string.
289
+ * @see {@linkcode vector}
183
290
  */
184
- var buildNodeID = (generated) => {
185
- return `node-${generated}`;
186
- };
291
+ var newEdge = type(({ intersection, use }) => intersection({ from: "string" }, use(vector)));
292
+ /** Schema definition for flowchart board geometry and container scroll dimensions. */
293
+ var board = type(({ optional }) => ({
294
+ self: {
295
+ left: "number",
296
+ top: "number",
297
+ width: "number",
298
+ height: "number"
299
+ },
300
+ parent: optional({
301
+ scrollLeft: "number",
302
+ scrollTop: "number",
303
+ height: "number",
304
+ width: "number"
305
+ })
306
+ }));
307
+ //#endregion
308
+ //#region src/services/main.machine.ts
187
309
  /**
188
310
  * State machine managing flowchart state transitions, nodes, edges, selection, and
189
311
  * layout actions.
312
+ *
313
+ * @see {@linkcode calculateDimensions}, {@linkcode buildEdgeId}, {@linkcode buildNodeID}, {@linkcode getDefaultOutputOffset}, {@linkcode DEFAULT_INPUT_OFFSET}
190
314
  */
191
315
  var machine = createMachine({
192
316
  initial: "idle",
317
+ on: { SET_BOARD: { actions: ["setBoard"] } },
193
318
  states: {
194
319
  idle: { on: {
195
320
  CONFIGURE: {
@@ -204,7 +329,7 @@ var machine = createMachine({
204
329
  } },
205
330
  working: { on: {
206
331
  MOVE_IMMEDIATE: { actions: [{
207
- name: "buildImmediateUI",
332
+ name: "buildUI",
208
333
  description: "Must be in the ui"
209
334
  }] },
210
335
  ADD_CHILD: {
@@ -229,6 +354,7 @@ var machine = createMachine({
229
354
  ],
230
355
  target: "/construction"
231
356
  },
357
+ ADD_DIMENSION: { actions: ["addDimension"] },
232
358
  ADD_SIBLING: {
233
359
  actions: [
234
360
  "generateID",
@@ -248,12 +374,18 @@ var machine = createMachine({
248
374
  actions: ["addEdge"],
249
375
  target: "/construction"
250
376
  },
377
+ START_NEW_EDGE: { actions: ["startNewEdge"] },
378
+ MOVE_NEW_EDGE: { actions: ["moveNewEdge"] },
379
+ CLEAR_NEW_EDGE: { actions: ["clearNewEdge"] },
251
380
  DELETE: {
252
381
  actions: ["delete"],
253
382
  target: "/construction"
254
383
  },
255
384
  SELECT: { actions: ["select"] },
256
- DESELECT: { actions: ["deselect"] }
385
+ DESELECT: { actions: ["deselect"] },
386
+ ZOOM: { actions: ["zoom"] },
387
+ TOGGLE_ZOOM: { actions: ["toggleZoom"] },
388
+ SET_BOARD: { actions: ["setBoard"] }
257
389
  } }
258
390
  }
259
391
  }, {
@@ -262,6 +394,7 @@ var machine = createMachine({
262
394
  nodes: array(intersection(use(nodeJSON), { id: "string" })),
263
395
  edges: array(intersection(use(edgeJSON), { id: "string" }))
264
396
  },
397
+ SET_BOARD: use(board),
265
398
  CONFIGURE_EMPTY: "never",
266
399
  MOVE: {
267
400
  id: "string",
@@ -279,11 +412,27 @@ var machine = createMachine({
279
412
  DELETE: "string",
280
413
  SELECT: "string",
281
414
  DESELECT: "never",
282
- ADD_EDGE: use(extremities)
415
+ ADD_EDGE: use(extremities),
416
+ START_NEW_EDGE: "string",
417
+ MOVE_NEW_EDGE: use(point),
418
+ CLEAR_NEW_EDGE: "never",
419
+ ZOOM: "number",
420
+ TOGGLE_ZOOM: "never",
421
+ ADD_DIMENSION: {
422
+ id: "string",
423
+ dimension: use(dimension)
424
+ }
283
425
  })),
284
426
  sync: true,
285
- pContext: type(({ union }) => ({ generatedId: union("string", "null") })),
286
- context: type(({ optional, use, array }) => ({
427
+ pContext: type(({ union, optional, record, use, custom }) => ({
428
+ generatedId: union("string", "null"),
429
+ previousZoom: optional("number"),
430
+ dimensions: record(use(dimension)),
431
+ getBoardPosition: custom(),
432
+ clampPosition: custom(),
433
+ calculateDimensions: custom()
434
+ })),
435
+ context: type(({ optional, use, array, record }) => ({
287
436
  data: optional({
288
437
  nodes: array({
289
438
  ...use(nodeJSON),
@@ -294,46 +443,145 @@ var machine = createMachine({
294
443
  id: "string"
295
444
  })
296
445
  }),
446
+ board: optional(use(board)),
447
+ edgesPositions: record(use(vector)),
448
+ newEdge: optional(use(newEdge)),
297
449
  selected: optional("string"),
298
- updatingUI: optional("boolean")
450
+ updatingUI: optional("boolean"),
451
+ zoom: "number",
452
+ bounds: use(point)
299
453
  }))
300
- }).provideOptions(({ assign, batch, erase }) => ({ actions: {
301
- configure: batch(assign("context.data", () => ({
302
- nodes: [],
303
- edges: []
304
- })), assign("context.data.nodes", { CONFIGURE: ({ payload: { nodes } }) => nodes }), assign("context.data.edges", { CONFIGURE: ({ payload: { edges } }) => edges }), assign("context.updatingUI", () => false), assign("pContext.generatedId", () => null)),
305
- generateID: assign("pContext.generatedId", () => nanoid()),
306
- linkChild: batch(assign("context.data.edges", { ADD_CHILD: ({ context, pContext, payload }) => {
307
- const data = context.data;
454
+ }).provideOptions(({ assign, batch, erase, filter, action }) => ({ actions: {
455
+ configure: batch(assign("data", { CONFIGURE: ({ payload }) => payload }), assign("newEdge", () => void 0), assign("updatingUI", () => false), action(({ pContext }) => {
456
+ pContext.generatedId = null;
457
+ }), action({ CONFIGURE: ({ payload: { nodes }, pContext: { dimensions } }) => {
458
+ nodes.forEach(({ id, position }) => {
459
+ dimensions[id] = calculateDimensions(position);
460
+ });
461
+ } })),
462
+ startNewEdge: assign("newEdge", { START_NEW_EDGE: ({ payload: from, pContext: { dimensions } }) => {
463
+ const { x, y } = dimensions[from].output;
464
+ return {
465
+ from,
466
+ x0: x,
467
+ y0: y,
468
+ x1: x,
469
+ y1: y
470
+ };
471
+ } }),
472
+ setBoard: assign("board", { SET_BOARD: ({ payload }) => payload }),
473
+ buildUI: batch(assign("edgesPositions", {
474
+ MOVE: ({ context: { data, edgesPositions }, payload, pContext: { dimensions } }) => {
475
+ const edges = data?.edges;
476
+ const dimension = dimensions[payload.id];
477
+ const outputOffset = dimension?.outputOffset ?? getDefaultOutputOffset(dimension?.width);
478
+ const inputOffset = dimension?.inputOffset ?? DEFAULT_INPUT_OFFSET;
479
+ const output = {
480
+ x: payload.x + outputOffset.x,
481
+ y: payload.y + outputOffset.y
482
+ };
483
+ const input = {
484
+ x: payload.x + inputOffset.x,
485
+ y: payload.y + inputOffset.y
486
+ };
487
+ if (dimension) {
488
+ dimension.output = output;
489
+ dimension.input = input;
490
+ }
491
+ edges?.forEach(({ from, to, id }) => {
492
+ if (from === payload.id) {
493
+ edgesPositions[id].x0 = output.x;
494
+ edgesPositions[id].y0 = output.y;
495
+ }
496
+ if (to === payload.id) {
497
+ edgesPositions[id].x1 = input.x;
498
+ edgesPositions[id].y1 = input.y;
499
+ }
500
+ });
501
+ return edgesPositions;
502
+ },
503
+ MOVE_IMMEDIATE: ({ context: { data, edgesPositions }, payload, pContext: { dimensions } }) => {
504
+ (data?.edges)?.forEach(({ from, to, id }) => {
505
+ if (from === payload.id) {
506
+ const dimension = dimensions[payload.id];
507
+ const offset = dimension?.outputOffset ?? getDefaultOutputOffset(dimension?.width);
508
+ const x0 = payload.x + offset.x;
509
+ const y0 = payload.y + offset.y;
510
+ edgesPositions[id].x0 = x0;
511
+ edgesPositions[id].y0 = y0;
512
+ if (dimension) {
513
+ dimension.output.x = x0;
514
+ dimension.output.y = y0;
515
+ }
516
+ }
517
+ if (to === payload.id) {
518
+ const dimension = dimensions[payload.id];
519
+ const offset = dimension?.inputOffset ?? DEFAULT_INPUT_OFFSET;
520
+ const x1 = payload.x + offset.x;
521
+ const y1 = payload.y + offset.y;
522
+ edgesPositions[id].x1 = x1;
523
+ edgesPositions[id].y1 = y1;
524
+ if (dimension) dimension.input = {
525
+ x: x1,
526
+ y: y1
527
+ };
528
+ }
529
+ });
530
+ return edgesPositions;
531
+ },
532
+ else: ({ context: { data, edgesPositions }, pContext: { dimensions } }) => {
533
+ const edges = data?.edges ?? [];
534
+ edgesPositions = {};
535
+ edges.forEach(({ from, id, to }) => {
536
+ const output = dimensions[from]?.output;
537
+ const input = dimensions[to]?.input;
538
+ if (output && input) edgesPositions[id] = {
539
+ x0: output.x,
540
+ y0: output.y,
541
+ x1: input.x,
542
+ y1: input.y
543
+ };
544
+ else delete edgesPositions[id];
545
+ });
546
+ return edgesPositions;
547
+ }
548
+ }), assign("updatingUI", () => true)),
549
+ addDimension: action({ ADD_DIMENSION: ({ payload: { id, dimension }, pContext: { dimensions } }) => {
550
+ dimensions[id] = dimension;
551
+ } }),
552
+ generateID: action(({ pContext }) => {
553
+ pContext.generatedId = nanoid();
554
+ }),
555
+ linkChild: batch(assign("data.edges", { ADD_CHILD: ({ context: { data }, pContext, payload }) => {
556
+ const edges = toArray$1.typed(data?.edges);
308
557
  const from = payload;
309
558
  const generatedId = pContext?.generatedId;
310
559
  const to = buildNodeID(generatedId);
311
560
  const id = buildEdgeId(from, to);
312
- return [...data?.edges ?? [], {
561
+ edges.push({
313
562
  id,
314
563
  from,
315
564
  to
316
- }];
317
- } }), assign("context.selected", ({ pContext: { generatedId } }) => buildNodeID(generatedId))),
318
- linkSibling: batch(assign("context.data.edges", { ADD_SIBLING: ({ pContext, payload, context }) => {
319
- const edges = context.data?.edges;
565
+ });
566
+ return edges;
567
+ } }), assign("selected", ({ pContext: { generatedId } }) => buildNodeID(generatedId))),
568
+ linkSibling: batch(assign("data.edges", { ADD_SIBLING: ({ pContext, payload, context: { data } }) => {
569
+ const edges = toArray$1.typed(data?.edges);
320
570
  const generatedId = pContext?.generatedId;
321
- const out = [...edges ?? []];
322
- const from = edges?.find(({ to }) => to === payload)?.from;
323
- if (!from) return out;
571
+ const from = edges.find(({ to }) => to === payload)?.from;
572
+ if (!from) return edges;
324
573
  const to = buildNodeID(generatedId);
325
574
  const id = buildEdgeId(from, to);
326
- out.push({
575
+ edges.push({
327
576
  from,
328
577
  to,
329
578
  id
330
579
  });
331
- return out;
332
- } }), assign("context.selected", ({ pContext: { generatedId } }) => buildNodeID(generatedId))),
333
- selectParent: assign("context.selected", ({ pContext: { generatedId } }) => buildNodeID(generatedId)),
334
- moveNode: assign("context.data.nodes", { MOVE: ({ context: { data }, payload }) => {
580
+ return edges;
581
+ } }), assign("selected", ({ pContext: { generatedId } }) => buildNodeID(generatedId))),
582
+ selectParent: assign("selected", ({ pContext: { generatedId } }) => buildNodeID(generatedId)),
583
+ moveNode: assign("data.nodes", { MOVE: ({ context: { data }, payload }) => {
335
584
  const { id, x, y } = payload;
336
- if (!id) return data?.nodes ?? [];
337
585
  return data?.nodes?.map((d) => {
338
586
  if (d.id === id) return {
339
587
  ...d,
@@ -345,11 +593,11 @@ var machine = createMachine({
345
593
  return d;
346
594
  }) ?? [];
347
595
  } }),
348
- select: assign("context.selected", { SELECT: ({ payload }) => payload }),
349
- delete: assign(["context.data.nodes", "context.data.edges"], { DELETE: ({ context: { data }, payload }) => {
350
- return [data?.nodes?.filter(({ id }) => id !== payload), data?.edges?.filter(({ id, from, to }) => id !== payload && from !== payload && to !== payload)];
351
- } }),
352
- addEdge: assign("context.data.edges", { ADD_EDGE: ({ context, payload: { from, to } }) => {
596
+ select: assign("selected", { SELECT: ({ payload }) => payload }),
597
+ delete: batch(filter("data.edges", { DELETE: ({ id, from, to }, _, { payload }) => {
598
+ return id !== payload && from !== payload && to !== payload;
599
+ } }), filter("data.nodes", { DELETE: ({ id }, _, { payload }) => id !== payload })),
600
+ addEdge: batch(assign("data.edges", { ADD_EDGE: ({ context, payload: { from, to } }) => {
353
601
  const edges = context.data?.edges ?? [];
354
602
  const id = buildEdgeId(from, to);
355
603
  if (edges.some((e) => e.id === id)) return edges;
@@ -358,282 +606,218 @@ var machine = createMachine({
358
606
  from,
359
607
  to
360
608
  }];
609
+ } }), erase("newEdge")),
610
+ clearNewEdge: erase("newEdge"),
611
+ deselect: erase("selected"),
612
+ zoom: assign("zoom", { ZOOM: ({ context: { zoom }, payload, pContext }) => {
613
+ const next = zoom + payload;
614
+ const clamped = Math.min(Math.max(Number(next.toFixed(2)), .2), 3);
615
+ pContext.previousZoom = void 0;
616
+ return clamped;
361
617
  } }),
362
- deselect: erase("context.selected")
618
+ toggleZoom: assign("zoom", { TOGGLE_ZOOM: ({ context: { zoom }, pContext }) => {
619
+ const previous = pContext.previousZoom;
620
+ if (previous !== void 0) {
621
+ pContext.previousZoom = void 0;
622
+ return previous;
623
+ }
624
+ pContext.previousZoom = zoom;
625
+ return 1;
626
+ } })
363
627
  } }));
364
- /** X-axis center offset in pixels from the node container edge. */
365
- var HANDLE_CENTER_X_OFFSET = 10.5;
366
- /** Default 2D offset coordinates for input connection handles. */
367
- var DEFAULT_INPUT_OFFSET = {
368
- x: -10.5,
369
- y: 18
370
- };
371
- /**
372
- * Computes default output handle offset coordinates given a node width.
373
- *
374
- * @param width - Node width in pixels, defaults to `0`.
375
- *
376
- * @returns 2D offset coordinates for the output handle.
377
- */
378
- var getDefaultOutputOffset = (width = 0) => ({
379
- x: width + HANDLE_CENTER_X_OFFSET,
380
- y: 18
381
- });
382
- /**
383
- * Boundary padding constraints for node dragging and positioning within the
384
- * viewport.
385
- */
386
- var BOUNDS_CONSTRAINTS = {
387
- horizontal: 55,
388
- vertical: 40
389
- };
390
- /** Default node items provided when no initial configuration is given. */
391
- var DEFAULT_NODES = [{
392
- id: "node-0",
393
- data: {
394
- content: "Some text",
395
- label: "Root node"
396
- },
397
- input: false,
398
- position: {
399
- x: 350,
400
- y: 100
401
- }
402
- }];
403
628
  //#endregion
404
629
  //#region src/ui/components/FlowChart.context.ts
405
630
  /**
406
631
  * Solid Context Provider component and hook for accessing flowchart board state,
407
632
  * services, and zoom.
633
+ *
634
+ * @see {@linkcode machine}, {@linkcode createContext}
408
635
  */
409
636
  var [Provider, useFlow] = createContext$1(() => {
410
- /**
411
- * Shared state machine interpreter service instance for flowchart state
412
- * management.
413
- */
637
+ /** Shared service for flowchart state management. */
414
638
  const service = interpret(machine, {
415
- context: {},
416
- pContext: { generatedId: null }
639
+ context: {
640
+ zoom: 1,
641
+ edgesPositions: {},
642
+ bounds: {
643
+ x: 0,
644
+ y: 0
645
+ }
646
+ },
647
+ pContext: {
648
+ generatedId: null,
649
+ dimensions: {},
650
+ /**
651
+ * Converts viewport screen coordinates to unscaled board coordinates.
652
+ *
653
+ * @param clientX - Client X coordinate in pixels.
654
+ * @param clientY - Client Y coordinate in pixels.
655
+ * @param el - Board layout details of type {@linkcode Board}.
656
+ *
657
+ * @returns Calculated 2D coordinate of type {@linkcode Point}.
658
+ */
659
+ getBoardPosition(clientX, clientY, el) {
660
+ if (!el) return {
661
+ x: clientX,
662
+ y: clientY
663
+ };
664
+ const currentZoom = service.state.context.zoom;
665
+ return {
666
+ x: (clientX - el.self.left) / currentZoom,
667
+ y: (clientY - el.self.top) / currentZoom
668
+ };
669
+ },
670
+ /**
671
+ * Clamps given coordinates within board boundaries.
672
+ *
673
+ * @param board - Board layout and container details of type
674
+ * {@linkcode Board}.
675
+ * @param x - Desired X position in pixels.
676
+ * @param y - Desired Y position in pixels.
677
+ * @param nodeWidth - Node width in pixels, defaults to `192`.
678
+ * @param nodeHeight - Node height in pixels, defaults to `50`.
679
+ *
680
+ * @returns Clamped coordinate of type {@linkcode Point}.
681
+ *
682
+ * @see {@linkcode BOUNDS_CONSTRAINTS}
683
+ */
684
+ clampPosition(board, x, y, nodeWidth = 192, nodeHeight = 50) {
685
+ const container = board?.parent;
686
+ if (!container) return {
687
+ x,
688
+ y
689
+ };
690
+ const currentZoom = service.state.context.zoom ?? 1;
691
+ const minX = container.scrollLeft / currentZoom + BOUNDS_CONSTRAINTS.horizontal;
692
+ const maxX = Math.max(minX, (container.scrollLeft + container.width) / currentZoom - BOUNDS_CONSTRAINTS.horizontal - nodeWidth);
693
+ const minY = container.scrollTop / currentZoom + BOUNDS_CONSTRAINTS.vertical;
694
+ const maxY = Math.max(minY, (container.scrollTop + container.height) / currentZoom - BOUNDS_CONSTRAINTS.vertical - nodeHeight);
695
+ return {
696
+ x: Math.min(Math.max(x, minX), maxX),
697
+ y: Math.min(Math.max(y, minY), maxY)
698
+ };
699
+ },
700
+ /**
701
+ * Computes node dimension, connection points, and handle offsets.
702
+ *
703
+ * @param position - Node position of type {@linkcode Point}.
704
+ * @param parentDimension - Optional sizing and offsets to inherit.
705
+ *
706
+ * @returns Calculated dimension object of type {@linkcode Dimension}.
707
+ *
708
+ * @see {@linkcode DEFAULT_INPUT_OFFSET}, {@linkcode getDefaultOutputOffset}
709
+ */
710
+ calculateDimensions: (position, parentDimension = {
711
+ width: 192,
712
+ height: 50,
713
+ inputOffset: DEFAULT_INPUT_OFFSET
714
+ }) => {
715
+ const width = parentDimension.width;
716
+ const height = parentDimension.height;
717
+ const outputOffset = parentDimension.outputOffset ?? getDefaultOutputOffset(width);
718
+ const inputOffset = parentDimension.inputOffset;
719
+ return {
720
+ width,
721
+ height,
722
+ output: {
723
+ x: position.x + outputOffset.x,
724
+ y: position.y + outputOffset.y
725
+ },
726
+ input: {
727
+ x: position.x + inputOffset.x,
728
+ y: position.y + inputOffset.y
729
+ },
730
+ outputOffset,
731
+ inputOffset
732
+ };
733
+ }
734
+ }
417
735
  });
418
- const zoom = createSignal(1);
419
- const newEdge = createSignal();
420
- const [boardRef, setBoardRef] = createSignal();
421
- const [dimensions, setDimensions] = createSignal({}, { equals: dequal });
422
- const getBoardPoint = (clientX, clientY) => {
423
- const el = boardRef();
424
- if (!el) return {
425
- x: clientX,
426
- y: clientY
427
- };
428
- const rect = el.getBoundingClientRect();
429
- const currentZoom = zoom[0]();
430
- return {
431
- x: clientX - rect.left / currentZoom,
432
- y: clientY - rect.top / currentZoom
433
- };
434
- };
435
- const [edgesPositions, setEdgesPositions] = createSignal({}, { equals: false });
436
- const clampPosition = (x, y, nodeWidth = 192, nodeHeight = 50) => {
437
- const container = boardRef()?.parentElement;
438
- if (!container) return {
439
- x,
440
- y
441
- };
442
- const currentZoom = zoom[0]();
443
- const minX = container.scrollLeft / currentZoom + BOUNDS_CONSTRAINTS.horizontal;
444
- const maxX = Math.max(minX, (container.scrollLeft + container.clientWidth) / currentZoom - BOUNDS_CONSTRAINTS.horizontal - nodeWidth);
445
- const minY = container.scrollTop / currentZoom + BOUNDS_CONSTRAINTS.vertical;
446
- const maxY = Math.max(minY, (container.scrollTop + container.clientHeight) / currentZoom - BOUNDS_CONSTRAINTS.vertical - nodeHeight);
447
- return {
448
- x: Math.min(Math.max(x, minX), maxX),
449
- y: Math.min(Math.max(y, minY), maxY)
450
- };
451
- };
452
- service.addOptions(({ voidAction, batch, assign }) => ({ actions: {
453
- placeChild: assign("context.data.nodes", { ADD_CHILD: ({ payload, context: { data }, pContext: { generatedId } }) => {
454
- const nodes = data?.nodes ?? [];
736
+ service.addOptions(({ assign }) => ({ actions: {
737
+ placeChild: assign("data.nodes", { ADD_CHILD: ({ payload, context: { data, board }, pContext }) => {
738
+ if (!board) return data?.nodes;
739
+ const nodes = data?.nodes;
455
740
  if (!payload) return nodes;
456
- const parentNode = nodes.find((node) => node.id === payload);
741
+ const parentNode = nodes?.find((node) => node.id === payload);
457
742
  if (!parentNode) return nodes;
458
- const id = `node-${generatedId}`;
459
- const width = dimensions()[payload]?.width ?? 0;
460
- const height = dimensions()[payload]?.height ?? 0;
743
+ const parentDimension = pContext.dimensions[payload];
744
+ const id = `node-${pContext.generatedId}`;
745
+ const width = parentDimension?.width ?? 192;
746
+ const height = parentDimension?.height ?? 50;
461
747
  const initialX = parentNode.position.x + width + 100;
462
748
  const initialY = parentNode.position.y;
463
- const position = clampPosition(initialX, initialY, width, height);
464
- return [...nodes, {
749
+ const position = pContext.clampPosition(board, initialX, initialY, width, height);
750
+ const dimension = pContext.calculateDimensions(position, parentDimension);
751
+ nodes?.push({
465
752
  id,
466
753
  data: { content: "<Nouveau nœud>" },
467
754
  input: true,
468
755
  position
469
- }];
756
+ });
757
+ pContext.dimensions[id] = dimension;
758
+ return nodes;
470
759
  } }),
471
- placeParent: assign("context.data.nodes", { ADD_PARENT: ({ context: { data }, pContext: { generatedId } }) => {
472
- const nodes = data?.nodes ?? [];
473
- const id = `node-${generatedId}`;
474
- const container = boardRef()?.parentElement;
760
+ placeParent: assign("data.nodes", { ADD_PARENT: ({ context: { data, zoom = 1, board }, pContext }) => {
761
+ if (!board) return data?.nodes;
762
+ const nodes = data?.nodes;
763
+ const id = `node-${pContext.generatedId}`;
764
+ const container = board.parent;
475
765
  const scrollLeft = container?.scrollLeft ?? 0;
476
766
  const scrollTop = container?.scrollTop ?? 0;
477
- const width = container?.clientWidth ?? 0;
478
- const height = container?.clientHeight ?? 0;
479
- const currentZoom = zoom[0]();
767
+ const width = container?.width ?? 0;
768
+ const height = container?.height ?? 0;
769
+ const currentZoom = zoom;
480
770
  const x = (scrollLeft + width / 2) / currentZoom;
481
771
  const y = (scrollTop + height / 2) / currentZoom;
482
- return [...nodes, {
772
+ const position = pContext.clampPosition(board, x, y);
773
+ const dimension = pContext.calculateDimensions(position);
774
+ nodes?.push({
483
775
  id,
484
776
  data: { content: "<Nouveau nœud>" },
485
777
  input: false,
486
- position: {
487
- x,
488
- y
489
- }
490
- }];
778
+ position
779
+ });
780
+ pContext.dimensions[id] = dimension;
781
+ return nodes;
491
782
  } }),
492
- placeSibling: assign("context.data.nodes", { ADD_SIBLING: ({ payload, context: { data }, pContext: { generatedId } }) => {
493
- const edges = data?.edges ?? [];
494
- const nodes = data?.nodes ?? [];
495
- const parentID = edges.find((edge) => edge.to === payload)?.from;
783
+ placeSibling: assign("data.nodes", { ADD_SIBLING: ({ payload, context: { data, board }, pContext }) => {
784
+ if (!board) return data?.nodes;
785
+ const edges = data?.edges;
786
+ const nodes = data?.nodes;
787
+ const parentID = edges?.find((edge) => edge.to === payload)?.from;
496
788
  if (!parentID) return nodes;
497
- const parentNode = nodes.find((node) => node.id === parentID);
789
+ const parentNode = nodes?.find((node) => node.id === parentID);
498
790
  if (!parentNode) return nodes;
499
- const id = `node-${generatedId}`;
500
- const width = dimensions()[parentID]?.width ?? 0;
501
- const height = dimensions()[parentID]?.height ?? 0;
791
+ const parentDimension = pContext.dimensions[parentID];
792
+ const id = `node-${pContext.generatedId}`;
793
+ const width = parentDimension?.width ?? 192;
794
+ const height = parentDimension?.height ?? 50;
502
795
  const initialX = parentNode.position.x + width + 100;
503
796
  const initialY = parentNode.position.y + 100;
504
- const position = clampPosition(initialX, initialY, width, height);
505
- return [...nodes, {
797
+ const position = pContext.clampPosition(board, initialX, initialY, width, height);
798
+ const dimension = pContext.calculateDimensions(position, parentDimension);
799
+ nodes?.push({
506
800
  id,
507
801
  data: { content: "<Nouveau nœud>" },
508
802
  input: true,
509
803
  position
510
- }];
511
- } }),
512
- buildUI: batch(voidAction(({ context: { data } }) => {
513
- const edges = data?.edges ?? [];
514
- setEdgesPositions((data) => {
515
- const array = Object.entries({ ...data }).filter(([id]) => edges.some((edge) => edge.id === id));
516
- return Object.fromEntries(array);
517
804
  });
518
- }), voidAction({
519
- else: ({ context: { data } }) => {
520
- const edges = data?.edges ?? [];
521
- setEdgesPositions(produce((next) => {
522
- edges?.forEach(({ from, id, to }) => {
523
- const output = dimensions()[from]?.output;
524
- const input = dimensions()[to]?.input;
525
- if (output && input) next[id] = {
526
- x0: output.x,
527
- y0: output.y,
528
- x1: input.x,
529
- y1: input.y
530
- };
531
- });
532
- }));
533
- },
534
- MOVE: ({ context: { data }, payload }) => {
535
- const edges = data?.edges ?? [];
536
- setEdgesPositions(produce((next) => {
537
- edges?.forEach(({ from, to, id }) => {
538
- if (from === payload.id) {
539
- const offset = dimensions()[payload.id]?.outputOffset ?? getDefaultOutputOffset(dimensions()[payload.id]?.width);
540
- const x0 = payload.x + offset.x;
541
- const y0 = payload.y + offset.y;
542
- next[id] = {
543
- ...next[id],
544
- x0,
545
- y0
546
- };
547
- setDimensions(produce((data) => {
548
- if (data[payload.id]) data[payload.id] = {
549
- ...data[payload.id],
550
- output: {
551
- x: x0,
552
- y: y0
553
- }
554
- };
555
- }));
556
- }
557
- if (to === payload.id) {
558
- const offset = dimensions()[payload.id]?.inputOffset ?? DEFAULT_INPUT_OFFSET;
559
- const x1 = payload.x + offset.x;
560
- const y1 = payload.y + offset.y;
561
- next[id] = {
562
- ...next[id],
563
- x1,
564
- y1
565
- };
566
- setDimensions(produce((data) => {
567
- if (data[payload.id]) data[payload.id] = {
568
- ...data[payload.id],
569
- input: {
570
- x: x1,
571
- y: y1
572
- }
573
- };
574
- }));
575
- }
576
- });
577
- }));
578
- }
579
- }), assign("context.updatingUI", () => true)),
580
- buildImmediateUI: voidAction({ MOVE_IMMEDIATE: ({ context: { data }, payload }) => {
581
- const edges = data?.edges ?? [];
582
- setEdgesPositions(produce((next) => {
583
- edges?.forEach(({ from, to, id }) => {
584
- if (from === payload.id) {
585
- const offset = dimensions()[payload.id]?.outputOffset ?? getDefaultOutputOffset(dimensions()[payload.id]?.width);
586
- const x0 = payload.x + offset.x;
587
- const y0 = payload.y + offset.y;
588
- next[id] = {
589
- ...next[id],
590
- x0,
591
- y0
592
- };
593
- setDimensions(produce((data) => {
594
- if (data[payload.id]) data[payload.id] = {
595
- ...data[payload.id],
596
- output: {
597
- x: x0,
598
- y: y0
599
- }
600
- };
601
- }));
602
- }
603
- if (to === payload.id) {
604
- const offset = dimensions()[payload.id]?.inputOffset ?? DEFAULT_INPUT_OFFSET;
605
- const x1 = payload.x + offset.x;
606
- const y1 = payload.y + offset.y;
607
- next[id] = {
608
- ...next[id],
609
- x1,
610
- y1
611
- };
612
- setDimensions(produce((data) => {
613
- if (data[payload.id]) data[payload.id] = {
614
- ...data[payload.id],
615
- input: {
616
- x: x1,
617
- y: y1
618
- }
619
- };
620
- }));
621
- }
622
- });
623
- }));
805
+ pContext.dimensions[id] = dimension;
806
+ return nodes;
807
+ } }),
808
+ moveNewEdge: assign("newEdge", { MOVE_NEW_EDGE: ({ context: { newEdge, board }, payload, pContext }) => {
809
+ if (!board) return void 0;
810
+ if (!newEdge) return void 0;
811
+ const { x: x1, y: y1 } = pContext.getBoardPosition(payload.x, payload.y, board);
812
+ return {
813
+ ...newEdge,
814
+ x1,
815
+ y1
816
+ };
624
817
  } })
625
818
  } }));
626
819
  service.start();
627
- return {
628
- dimensions: [dimensions, setDimensions],
629
- newEdge,
630
- board: [boardRef, setBoardRef],
631
- getBoardPoint,
632
- edgesPositions: [edgesPositions, setEdgesPositions],
633
- service,
634
- zoom,
635
- clampPosition
636
- };
820
+ return service;
637
821
  }, { name: "FlowContext" });
638
822
  //#endregion
639
823
  //#region src/ui/components/Bounds.tsx
@@ -643,24 +827,28 @@ var [Provider, useFlow] = createContext$1(() => {
643
827
  *
644
828
  * @returns `null` as this component performs side-effect transformer registrations
645
829
  * only.
830
+ *
831
+ * @see {@linkcode useFlow}, {@linkcode BOUNDS_CONSTRAINTS}
646
832
  */
647
833
  var DragBounds = () => {
648
- const { board: [ref], zoom: [zoom] } = useFlow();
834
+ const service = useFlow();
835
+ const zoom = createState(service, { selector: ({ context }) => context.zoom });
836
+ const board = createState(service, { selector: ({ context }) => context.board });
649
837
  const [state, { addTransformer, removeTransformer, onDragStart, onDragEnd }] = useDragDropContext();
650
838
  const transformer = {
651
839
  id: "clamp-to-container",
652
840
  order: 100,
653
841
  callback: (transform) => {
654
- const container = ref();
842
+ const container = board()?.self;
655
843
  const activeDraggable = state.active.draggable;
656
844
  if (!container || !activeDraggable) return transform;
657
845
  const node = activeDraggable.node;
658
846
  if (!node) return transform;
659
847
  const currentZoom = zoom();
660
848
  const minBoardX = BOUNDS_CONSTRAINTS.horizontal;
661
- const maxBoardX = Math.max(minBoardX, container.clientWidth / currentZoom - node.offsetWidth - BOUNDS_CONSTRAINTS.horizontal);
849
+ const maxBoardX = Math.max(minBoardX, container.width / currentZoom - node.offsetWidth - BOUNDS_CONSTRAINTS.horizontal);
662
850
  const minBoardY = BOUNDS_CONSTRAINTS.vertical;
663
- const maxBoardY = Math.max(minBoardY, container.clientHeight / currentZoom - node.offsetHeight - BOUNDS_CONSTRAINTS.vertical);
851
+ const maxBoardY = Math.max(minBoardY, container.height / currentZoom - node.offsetHeight - BOUNDS_CONSTRAINTS.vertical);
664
852
  const minTransformX = (minBoardX - node.offsetLeft) * currentZoom;
665
853
  const maxTransformX = (maxBoardX - node.offsetLeft) * currentZoom;
666
854
  const minTransformY = (minBoardY - node.offsetTop) * currentZoom;
@@ -709,6 +897,8 @@ var calculateOffset = (value) => value * 100 / 200;
709
897
  * @param vector - Vector coordinates of type {@linkcode Vector}.
710
898
  *
711
899
  * @returns SVG cubic bezier path string.
900
+ *
901
+ * @see {@linkcode calculateOffset}
712
902
  */
713
903
  var draw = ({ x0, y0, x1, y1 }) => {
714
904
  return `M ${x0} ${y0} C ${x0 + calculateOffset(Math.abs(x1 - x0))} ${y0}, ${x1 - calculateOffset(Math.abs(x1 - x0))} ${y1}, ${x1} ${y1}`;
@@ -720,30 +910,53 @@ var draw = ({ x0, y0, x1, y1 }) => {
720
910
  * @param props - Edge properties of type {@linkcode Props}.
721
911
  *
722
912
  * @returns The rendered SVG JSX elements.
913
+ *
914
+ * @see {@linkcode draw}, {@linkcode useFlow}
723
915
  */
724
916
  var EdgeComponent = (props) => {
725
- const [middlePoint, setMiddlePoint] = createSignal({
726
- x: props.x0 + (props.x1 - props.x0) / 2,
727
- y: props.y0 + (props.y1 - props.y0) / 2
728
- });
729
- const { service } = useFlow();
730
- createEffect(() => {
731
- const middleX = props.x0 + (props.x1 - props.x0) / 2;
732
- const middleY = props.y0 + (props.y1 - props.y0) / 2;
733
- setMiddlePoint({
734
- x: middleX,
735
- y: middleY
736
- });
917
+ const service = useFlow();
918
+ const vector = createState(service, {
919
+ selector: ({ context }) => {
920
+ if (props.isNew) {
921
+ const edge = context.newEdge;
922
+ if (!edge) return void 0;
923
+ return {
924
+ x0: edge.x0,
925
+ y0: edge.y0,
926
+ x1: edge.x1,
927
+ y1: edge.y1
928
+ };
929
+ }
930
+ return context.edgesPositions[props.id];
931
+ },
932
+ equals: dequal
737
933
  });
738
- const selected = useState(service, { selector: (s) => s.context.selected === props.id });
739
- return [ssr(_tmpl$$4, ssrHydrationKey(), `relative cursor-pointer fill-transparent ${!!props.isNew ? "stroke-[rgba(168,168,168,0.4)] stroke-3 z-200" : ""} ${selected() && !props.isNew ? "stroke-[rgba(168,168,168,1)] stroke-4 z-100" : ""} ${!selected() && !props.isNew ? "stroke-[rgba(168,168,168,0.8)] stroke-3" : ""}`, ssrStyleProperty("pointer-events:", props.isNew ? "none" : "all"), ssrAttribute("d", escape(draw(props), true), false)), createComponent(Show, {
934
+ const selected = createState(service, { selector: (s) => s.context.selected === props.id });
935
+ /** Computes the 2D midpoint coordinate of the edge curve for handle placement. */
936
+ const middlePoint = () => {
937
+ const v = vector();
938
+ if (!v) return {
939
+ x: 0,
940
+ y: 0
941
+ };
942
+ return {
943
+ x: v.x0 + (v.x1 - v.x0) / 2,
944
+ y: v.y0 + (v.y1 - v.y0) / 2
945
+ };
946
+ };
947
+ return createComponent(Show, {
740
948
  get when() {
741
- return selected();
949
+ return vector();
742
950
  },
743
- get children() {
744
- return ssr(_tmpl$2$2, ssrHydrationKey(), `translate(${escape(middlePoint().x, true)}, ${escape(middlePoint().y, true)})`, ssrStyleProperty("pointer-events:", "all") + ssrStyleProperty(";z-index:", "30"));
745
- }
746
- })];
951
+ children: (v) => [ssr(_tmpl$$4, ssrHydrationKey(), `relative cursor-pointer fill-transparent ${!!props.isNew ? "stroke-[rgba(168,168,168,0.4)] stroke-3 z-200" : ""} ${selected() && !props.isNew ? "stroke-[rgba(168,168,168,1)] stroke-4 z-100" : ""} ${!selected() && !props.isNew ? "stroke-[rgba(168,168,168,0.8)] stroke-3" : ""}`, ssrStyleProperty("pointer-events:", props.isNew ? "none" : "all"), ssrAttribute("d", escape(draw(v()), true), false)), createComponent(Show, {
952
+ get when() {
953
+ return selected();
954
+ },
955
+ get children() {
956
+ return ssr(_tmpl$2$2, ssrHydrationKey(), `translate(${escape(middlePoint().x, true)}, ${escape(middlePoint().y, true)})`, ssrStyleProperty("pointer-events:", "all") + ssrStyleProperty(";z-index:", "30"));
957
+ }
958
+ })]
959
+ });
747
960
  };
748
961
  //#endregion
749
962
  //#region src/ui/components/EdgesBoard.tsx
@@ -758,44 +971,31 @@ var _tmpl$$3 = [
758
971
  * edge creation previews.
759
972
  *
760
973
  * @returns The rendered SVG JSX element.
974
+ *
975
+ * @see {@linkcode EdgeComponent}, {@linkcode useFlow}
761
976
  */
762
977
  var EdgesBoard = () => {
763
- const [selected, setSelected] = createSignal();
764
- const { newEdge: [newEdge], edgesPositions: [edgesPositions] } = useFlow();
765
- const datas = createMemo(() => {
766
- return Object.entries(edgesPositions()).map(([id, vector]) => ({
767
- id,
768
- ...vector
769
- }));
770
- });
771
- createEffect(() => {
772
- if (selected() && newEdge()) setSelected();
978
+ const service = useFlow();
979
+ const hasNewEdge = createState(service, { selector: (s) => !!s.context.newEdge });
980
+ const edgeIds = createState(service, {
981
+ selector: ({ context }) => Object.keys(context.edgesPositions),
982
+ equals: (prev, next) => prev.length === next.length
773
983
  });
774
984
  return ssr(_tmpl$$3, ssrHydrationKey(), escape(createComponent(Show, {
775
985
  get when() {
776
- return newEdge();
986
+ return hasNewEdge();
777
987
  },
778
- children: (value) => createComponent(EdgeComponent, {
779
- id: "__#new-edge#__TEMP",
780
- isNew: true,
781
- get x0() {
782
- return value().x0;
783
- },
784
- get y0() {
785
- return value().y0;
786
- },
787
- get x1() {
788
- return value().x1;
789
- },
790
- get y1() {
791
- return value().y1;
792
- }
793
- })
988
+ get children() {
989
+ return createComponent(EdgeComponent, {
990
+ id: "__#new-edge#__TEMP",
991
+ isNew: true
992
+ });
993
+ }
794
994
  })), escape(createComponent(For, {
795
995
  get each() {
796
- return datas();
996
+ return edgeIds();
797
997
  },
798
- children: EdgeComponent
998
+ children: (id) => createComponent(EdgeComponent, { id })
799
999
  })));
800
1000
  };
801
1001
  //#endregion
@@ -815,11 +1015,12 @@ var _tmpl$3$1 = [
815
1015
  "<div",
816
1016
  " class=\"",
817
1017
  "\" style=\"",
818
- "\"><div class=\"",
1018
+ "\"",
1019
+ "><div class=\"",
819
1020
  "\"><svg class=\"cursor-pointer rounded-full fill-[#a11111] opacity-100 transition-all duration-200 ease-in-out\" fill=\"currentColor\" stroke-width=\"2\" viewBox=\"4 4 16 16\" style=\"",
820
1021
  "\"><path d=\"M12 4c-4.419 0-8 3.582-8 8s3.581 8 8 8 8-3.582 8-8-3.581-8-8-8zm3.707 10.293a.999.999 0 11-1.414 1.414L12 13.414l-2.293 2.293a.997.997 0 01-1.414 0 .999.999 0 010-1.414L10.586 12 8.293 9.707a.999.999 0 111.414-1.414L12 10.586l2.293-2.293a.999.999 0 111.414 1.414L13.414 12l2.293 2.293z\"></path></svg><!--$-->",
821
- "<!--/--><svg class=\"flex cursor-pointer items-center justify-center rounded-lg bg-blue-500 p-0.5 text-center font-bold text-white hover:bg-blue-600\" style=\"",
822
- "\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z\"></path></svg></div><!--$-->",
1022
+ "<!--/--><svg class=\"flex cursor-pointer items-center justify-center rounded-lg bg-blue-500 p-0.5 text-center font-bold text-white hover:bg-blue-600\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\" style=\"",
1023
+ "\"><path d=\"M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z\"></path></svg></div><!--$-->",
823
1024
  "<!--/--><!--$-->",
824
1025
  "<!--/--><!--$-->",
825
1026
  "<!--/--><div id=\"inputs\" class=\"pointer-events-none absolute top-0 z-10 flex flex-col\" style=\"",
@@ -843,22 +1044,34 @@ var _tmpl$5 = [
843
1044
  * @param props - Node rendering properties of type {@linkcode Props}.
844
1045
  *
845
1046
  * @returns The rendered Solid component.
1047
+ *
1048
+ * @see {@linkcode useFlow}, {@linkcode HANDLE_CONTAINER_OFFSET_X}, {@linkcode HANDLE_MARGIN_TOP}, {@linkcode HANDLE_SIZE}
846
1049
  */
847
1050
  var NodeComponent = (props) => {
848
- const [ref, setRef] = createSignal();
849
- const { dimensions: [dimensions, setDimensions], newEdge: [newEdge, setNewEdge], getBoardPoint, service, zoom: [zoom] } = useFlow();
850
- const selected = useState(service, { selector: (s) => s.context.selected === props.id });
851
- onMount(() => {
852
- ref();
853
- zoom();
1051
+ const service = useFlow();
1052
+ const node = createState(service, {
1053
+ selector: ({ context }) => {
1054
+ const item = toArray.typed(context.data?.nodes).find((n) => n.id === props.id);
1055
+ if (!item) return void 0;
1056
+ return {
1057
+ x: item.position.x,
1058
+ y: item.position.y,
1059
+ label: item.data.label,
1060
+ content: item.data.content ?? "",
1061
+ input: item.input
1062
+ };
1063
+ },
1064
+ equals: dequal
854
1065
  });
855
- const hasParent = useState(service, { selector: ({ context: { data } }) => {
1066
+ createState(service, { selector: (s) => s.context.newEdge });
1067
+ const selected = createState(service, { selector: (s) => s.context.selected === props.id });
1068
+ const hasParent = createState(service, { selector: ({ context: { data } }) => {
856
1069
  const edges = data?.edges;
857
1070
  if (!edges) return false;
858
1071
  return edges.some(({ to }) => to === props.id);
859
1072
  } });
860
1073
  createDraggable(props.id);
861
- return ssr(_tmpl$3$1, ssrHydrationKey() + ssrAttribute("id", escape(props.id, true), false), `flex flex-col absolute cursor-grab bg-white rounded-md shadow-md select-none transition-[border,box-shadow] duration-200 ease-in-out hover:shadow-lg draggable ${selected() ? "border border-[#e38c29] z-[100]" : ""} ${!selected() ? "border border-[#e6d4be] z-[1]" : ""} min-w-48`, ssrStyleProperty("top:", `${escape(props.y, true)}px`) + ssrStyleProperty(";left:", `${escape(props.x, true)}px`), `pointer-events-none absolute flex items-center justify-end -top-7.5 right-0 transition-all duration-200 ease-in-out space-x-2 ${selected() ? "w-full opacity-100" : ""} ${!selected() ? "w-0 -right-3 opacity-0 overflow-hidden" : ""}`, ssrStyleProperty("overflow:", "visible") + ssrStyleProperty(";pointer-events:", "all") + ssrStyleProperty(";width:", `${escape(12, true) * 2}px`) + ssrStyleProperty(";height:", `${escape(12, true) * 2}px`), escape(createComponent(Show, {
1074
+ return ssr(_tmpl$3$1, ssrHydrationKey(), `flex flex-col absolute cursor-grab bg-white rounded-md shadow-md select-none transition-[border,box-shadow] duration-200 ease-in-out hover:shadow-lg draggable ${selected() ? "border border-[#e38c29] z-[100]" : ""} ${!selected() ? "border border-[#e6d4be] z-[1]" : ""} min-w-48`, ssrStyleProperty("top:", `${escape(node()?.y ?? 0, true)}px`) + ssrStyleProperty(";left:", `${escape(node()?.x ?? 0, true)}px`), ssrAttribute("id", escape(props.id, true), false), `pointer-events-none absolute flex items-center justify-end -top-7.5 right-0 transition-all duration-200 ease-in-out space-x-2 ${selected() ? "w-full opacity-100" : ""} ${!selected() ? "w-0 -right-3 opacity-0 overflow-hidden" : ""}`, ssrStyleProperty("overflow:", "visible") + ssrStyleProperty(";pointer-events:", "all") + ssrStyleProperty(";width:", `${escape(12, true) * 2}px`) + ssrStyleProperty(";height:", `${escape(12, true) * 2}px`), escape(createComponent(Show, {
862
1075
  get when() {
863
1076
  return hasParent();
864
1077
  },
@@ -867,19 +1080,19 @@ var NodeComponent = (props) => {
867
1080
  }
868
1081
  })), ssrStyleProperty("pointer-events:", "all") + ssrStyleProperty(";width:", `${escape(12, true) * 2}px`) + ssrStyleProperty(";height:", `${escape(12, true) * 2}px`), escape(createComponent(Show, {
869
1082
  get when() {
870
- return props.label;
1083
+ return node()?.label;
871
1084
  },
872
1085
  keyed: true,
873
1086
  children: (label) => ssr(_tmpl$4, ssrHydrationKey(), escape(label))
874
1087
  })), escape(createComponent(Show, {
875
1088
  get when() {
876
- return props.content;
1089
+ return node()?.content;
877
1090
  },
878
1091
  keyed: true,
879
1092
  children: (content) => ssr(_tmpl$5, ssrHydrationKey(), escape(content))
880
1093
  })), escape(createComponent(Show, {
881
1094
  get when() {
882
- return props.input || hasParent();
1095
+ return node()?.input || hasParent();
883
1096
  },
884
1097
  get children() {
885
1098
  return ssr(_tmpl$2$1, ssrHydrationKey(), ssrStyleProperty("left:", `-${escape(18, true)}px`), ssrStyleProperty("width:", `${escape(12, true)}px`) + ssrStyleProperty(";height:", `${escape(12, true)}px`) + ssrStyleProperty(";margin-top:", `${escape(12, true)}px`) + ssrStyleProperty(";pointer-events:", "all"));
@@ -890,7 +1103,7 @@ var NodeComponent = (props) => {
890
1103
  //#region src/ui/components/NodesBoard.tsx
891
1104
  var _tmpl$$1 = [
892
1105
  "<div",
893
- " class=\"relative h-full w-full overflow-scroll rounded-lg border-2 border-gray-600\"><!--$-->",
1106
+ " class=\"relative h-full w-full overflow-auto rounded-lg border-2 border-gray-600\"><!--$-->",
894
1107
  "<!--/--><div class=\"",
895
1108
  "\" style=\"",
896
1109
  "\"><div class=\"relative h-full w-full\" style=\"",
@@ -914,6 +1127,8 @@ var _tmpl$3 = [
914
1127
  * panning gestures, and rendered nodes/edges.
915
1128
  *
916
1129
  * @returns The rendered Solid component.
1130
+ *
1131
+ * @see {@linkcode DragBounds}, {@linkcode EdgesBoard}, {@linkcode NodeComponent}, {@linkcode useFlow}, {@linkcode CANVAS_FACTOR}, {@linkcode SCROLL_MULTIPLIER}
917
1132
  */
918
1133
  var NodesBoard = () => {
919
1134
  let containerRef;
@@ -923,35 +1138,71 @@ var NodesBoard = () => {
923
1138
  y: 0
924
1139
  });
925
1140
  const [id, setId] = createSignal("");
926
- const [previousZoom, setPreviousZoom] = createSignal();
1141
+ const [ref, setRef] = createSignal();
927
1142
  let percentX = 0;
928
1143
  let percentY = 0;
929
- const { board: [, setRef], service, newEdge: [newEdge], zoom: [zoom, setZoom] } = useFlow();
1144
+ const service = useFlow();
1145
+ createState(service, {
1146
+ selector: (s) => s.context.newEdge,
1147
+ equals: dequal
1148
+ });
1149
+ const zoom = createState(service, { selector: (s) => s.context.zoom ?? 1 });
1150
+ /**
1151
+ * Dispatches the current board geometry and parent container dimensions to the
1152
+ * state machine service.
1153
+ */
1154
+ const sendBoard = () => {
1155
+ const el = ref();
1156
+ if (!el) return;
1157
+ const rect = el.getBoundingClientRect();
1158
+ const parent = () => ref()?.parentElement;
1159
+ const payload = {
1160
+ self: {
1161
+ left: rect.left,
1162
+ top: rect.top,
1163
+ width: el.clientWidth,
1164
+ height: el.clientHeight
1165
+ },
1166
+ parent: parent() ? {
1167
+ scrollLeft: parent().scrollLeft,
1168
+ scrollTop: parent().scrollTop,
1169
+ height: parent().clientHeight,
1170
+ width: parent().clientWidth
1171
+ } : void 0
1172
+ };
1173
+ service.send({
1174
+ type: "SET_BOARD",
1175
+ payload
1176
+ });
1177
+ };
930
1178
  createEffect(on(zoom, () => {
931
1179
  const maxScrollX = containerRef.scrollWidth - containerRef.clientWidth;
932
1180
  const maxScrollY = containerRef.scrollHeight - containerRef.clientHeight;
933
1181
  if (maxScrollX > 0) containerRef.scrollLeft = percentX * maxScrollX;
934
1182
  if (maxScrollY > 0) containerRef.scrollTop = percentY * maxScrollY;
935
1183
  }, { defer: true }));
936
- const selectedId = useState(service, { selector: (s) => s.context?.selected });
1184
+ onMount(sendBoard);
1185
+ const selectedId = createState(service, { selector: (s) => s.context?.selected });
1186
+ /**
1187
+ * Determines whether the given element ID matches the currently selected entity.
1188
+ *
1189
+ * @param id - The node or edge identifier to check.
1190
+ *
1191
+ * @returns `true` if the item is currently selected, otherwise `false`.
1192
+ */
937
1193
  const selected = (id) => selectedId() === id;
938
- const nodes = useState(service, {
939
- selector: (s) => {
940
- return (s.context.data?.nodes ?? []).map((item) => ({
941
- id: item.id,
942
- x: item.position.x,
943
- y: item.position.y,
944
- label: item.data.label,
945
- content: item.data.content ?? "",
946
- input: item.input
947
- }));
1194
+ const nodeIds = createState(service, {
1195
+ selector: ({ context }) => {
1196
+ return toArray.typed(context.data?.nodes).map((item) => item.id);
948
1197
  },
949
- equals: dequal$1
1198
+ equals: (prev, next) => prev.length === next.length
950
1199
  });
951
1200
  const CANVAS_SIZE = 500;
952
1201
  const MARGIN_X = 265;
953
1202
  const MARGIN_Y = 425;
1203
+ /** Computes the dynamic canvas width string in CSS units adjusted for zoom. */
954
1204
  const cWidth = () => `calc((${CANVAS_SIZE}vw - ${MARGIN_X}px) * ${zoom()})`;
1205
+ /** Computes the dynamic canvas height string in CSS units adjusted for zoom. */
955
1206
  const cHeight = () => `calc((${CANVAS_SIZE}vh - ${MARGIN_Y}px) * ${zoom()})`;
956
1207
  return ssr(_tmpl$3, ssrHydrationKey(), escape(createComponent(DragDropProvider, {
957
1208
  onDragMove: ({ draggable: { transform: _transform, node, id } }) => {
@@ -999,15 +1250,15 @@ var NodesBoard = () => {
999
1250
  node.style.setProperty("top", Y + "px");
1000
1251
  node.style.setProperty("left", X + "px");
1001
1252
  node.style.removeProperty("transform");
1253
+ service.send({
1254
+ type: "MOVE",
1255
+ payload: {
1256
+ id: `${id}`,
1257
+ x: X,
1258
+ y: Y
1259
+ }
1260
+ });
1002
1261
  setTimeout(() => {
1003
- service.send({
1004
- type: "MOVE",
1005
- payload: {
1006
- id: `${id}`,
1007
- x: X,
1008
- y: Y
1009
- }
1010
- });
1011
1262
  setTransform({
1012
1263
  x: 0,
1013
1264
  y: 0
@@ -1018,9 +1269,9 @@ var NodesBoard = () => {
1018
1269
  return [
1019
1270
  ssr(_tmpl$$1, ssrHydrationKey(), escape(createComponent(DragDropSensors, {})), `relative cursor-crosshair overflow-hidden ${isPanning() ? "cursor-grabbing" : ""}`, ssrStyleProperty("height:", escape(cHeight(), true)) + ssrStyleProperty(";width:", escape(cWidth(), true)), ssrStyleProperty("scale:", escape(zoom(), true)) + ssrStyleProperty(";transform-origin:", "top left"), escape(createComponent(DragBounds, {})), escape(createComponent(EdgesBoard, {})), escape(createComponent(For, {
1020
1271
  get each() {
1021
- return nodes();
1272
+ return nodeIds();
1022
1273
  },
1023
- children: NodeComponent
1274
+ children: (id) => createComponent(NodeComponent, { id })
1024
1275
  }))),
1025
1276
  ssr(_tmpl$2, ssrHydrationKey(), escape(Math.round(zoom() * 100))),
1026
1277
  createComponent(Show, {
@@ -1051,13 +1302,16 @@ var _tmpl$ = [
1051
1302
  * {@linkcode FlowProps}.
1052
1303
  *
1053
1304
  * @returns The rendered Solid component.
1305
+ *
1306
+ * @see {@linkcode NodesBoard}, {@linkcode useFlow}, {@linkcode DEFAULT_NODES}
1054
1307
  */
1055
1308
  var FlowChart = (props) => {
1056
1309
  const primaryNodes = props.config?.nodes ?? DEFAULT_NODES;
1057
1310
  const primaryEdges = props.config?.edges;
1058
- const { service, newEdge: [newEdge, setNewEdge], getBoardPoint } = useFlow();
1311
+ const service = useFlow();
1312
+ const hasNewEdge = createState(service, { selector: (s) => !!s.context.newEdge });
1059
1313
  onMount(() => {
1060
- service.start();
1314
+ service.resume();
1061
1315
  service.send({
1062
1316
  type: "CONFIGURE",
1063
1317
  payload: {
@@ -1066,8 +1320,8 @@ var FlowChart = (props) => {
1066
1320
  }
1067
1321
  });
1068
1322
  });
1069
- onCleanup(service.stop);
1070
- return ssr(_tmpl$, ssrHydrationKey(), ssrStyleProperty("cursor:", newEdge() ? "inherit" : "crosshair") + ssrStyleProperty(";background-image:", "radial-gradient(circle, #b8b8b8bf 1px, rgba(0, 0, 0, 0) 1px)"), escape(createComponent(NodesBoard, {})));
1323
+ onCleanup(service.pause);
1324
+ return ssr(_tmpl$, ssrHydrationKey(), ssrStyleProperty("cursor:", hasNewEdge() ? "inherit" : "crosshair") + ssrStyleProperty(";background-image:", "radial-gradient(circle, #b8b8b8bf 1px, rgba(0, 0, 0, 0) 1px)"), escape(createComponent(NodesBoard, {})));
1071
1325
  };
1072
1326
  //#endregion
1073
1327
  //#region src/ui/Flow.tsx
@@ -1079,6 +1333,8 @@ var FlowChart = (props) => {
1079
1333
  * {@linkcode FlowProps}.
1080
1334
  *
1081
1335
  * @returns The rendered Solid component.
1336
+ *
1337
+ * @see {@linkcode Provider}
1082
1338
  */
1083
1339
  var Flow = (props) => createComponent(Provider, { get children() {
1084
1340
  return createComponent(FlowChart, props);