@bemedev/mind-flow 0.3.1 → 1.0.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 (115) hide show
  1. package/README.md +55 -12
  2. package/lib/helpers/clamp.d.ts +2 -0
  3. package/lib/helpers/clamp.d.ts.map +1 -0
  4. package/lib/helpers/createContext.d.ts +1 -1
  5. package/lib/helpers/createContext.d.ts.map +1 -1
  6. package/lib/helpers/index.d.ts +3 -0
  7. package/lib/helpers/index.d.ts.map +1 -0
  8. package/lib/index.cjs.js +1 -1
  9. package/lib/index.d.ts +9 -1
  10. package/lib/index.d.ts.map +1 -1
  11. package/lib/index.es.js +2543 -448
  12. package/lib/output.css +1 -1
  13. package/lib/server.cjs.js +1129 -397
  14. package/lib/server.es.js +1095 -407
  15. package/lib/services/main.machine.d.ts +2453 -576
  16. package/lib/services/main.machine.d.ts.map +1 -1
  17. package/lib/services/main.machine.data.d.ts +14 -1
  18. package/lib/services/main.machine.data.d.ts.map +1 -1
  19. package/lib/services/main.machine.helpers.d.ts +1 -1
  20. package/lib/services/main.machine.helpers.d.ts.map +1 -1
  21. package/lib/services/main.machine.test.d.ts +2 -0
  22. package/lib/services/main.machine.test.d.ts.map +1 -0
  23. package/lib/services/main.machine.typings.d.ts +26 -8
  24. package/lib/services/main.machine.typings.d.ts.map +1 -1
  25. package/lib/ui/Flow.d.ts +8 -4
  26. package/lib/ui/Flow.d.ts.map +1 -1
  27. package/lib/ui/components/Bounds.d.ts.map +1 -1
  28. package/lib/ui/components/EdgeComponent.d.ts.map +1 -1
  29. package/lib/ui/components/EdgesBoard.d.ts.map +1 -1
  30. package/lib/ui/components/EditPanel.d.ts +19 -0
  31. package/lib/ui/components/EditPanel.d.ts.map +1 -0
  32. package/lib/ui/components/EditPanel.hooks.d.ts +29 -0
  33. package/lib/ui/components/EditPanel.hooks.d.ts.map +1 -0
  34. package/lib/ui/components/EditPanel.types.d.ts +47 -0
  35. package/lib/ui/components/EditPanel.types.d.ts.map +1 -0
  36. package/lib/ui/components/FlowChart.context.d.ts +2452 -575
  37. package/lib/ui/components/FlowChart.context.d.ts.map +1 -1
  38. package/lib/ui/components/FlowChart.d.ts +8 -47
  39. package/lib/ui/components/FlowChart.d.ts.map +1 -1
  40. package/lib/ui/components/FlowChart.types.d.ts +87 -0
  41. package/lib/ui/components/FlowChart.types.d.ts.map +1 -0
  42. package/lib/ui/components/NodeComponent.d.ts +10 -12
  43. package/lib/ui/components/NodeComponent.d.ts.map +1 -1
  44. package/lib/ui/components/NodesBoard.d.ts +17 -2
  45. package/lib/ui/components/NodesBoard.d.ts.map +1 -1
  46. package/lib/ui/components/Panels.d.ts +12 -0
  47. package/lib/ui/components/Panels.d.ts.map +1 -0
  48. package/lib/ui/globals/components/atoms/TypingText.d.ts +29 -0
  49. package/lib/ui/globals/components/atoms/TypingText.d.ts.map +1 -0
  50. package/lib/ui/globals/components/molecules/FadingDots.d.ts +21 -0
  51. package/lib/ui/globals/components/molecules/FadingDots.d.ts.map +1 -0
  52. package/lib/ui/globals/components/organisms/LoadingFallback.d.ts +46 -0
  53. package/lib/ui/globals/components/organisms/LoadingFallback.d.ts.map +1 -0
  54. package/lib/ui/globals/constants/index.d.ts +3 -0
  55. package/lib/ui/globals/constants/index.d.ts.map +1 -0
  56. package/lib/ui/globals/constants/links.d.ts +4 -0
  57. package/lib/ui/globals/constants/links.d.ts.map +1 -0
  58. package/lib/ui/globals/constants/strings.d.ts +5 -0
  59. package/lib/ui/globals/constants/strings.d.ts.map +1 -0
  60. package/lib/ui/globals/directives/clickOutside.d.ts +9 -0
  61. package/lib/ui/globals/directives/clickOutside.d.ts.map +1 -0
  62. package/lib/ui/globals/directives/index.d.ts +16 -0
  63. package/lib/ui/globals/directives/index.d.ts.map +1 -0
  64. package/lib/ui/globals/directives/mouseOut.d.ts +33 -0
  65. package/lib/ui/globals/directives/mouseOut.d.ts.map +1 -0
  66. package/lib/ui/globals/directives/mouseOut.test.d.ts +2 -0
  67. package/lib/ui/globals/directives/mouseOut.test.d.ts.map +1 -0
  68. package/lib/ui/globals/directives/resize.d.ts +11 -0
  69. package/lib/ui/globals/directives/resize.d.ts.map +1 -0
  70. package/lib/ui/globals/helpers/espace.d.ts +17 -0
  71. package/lib/ui/globals/helpers/espace.d.ts.map +1 -0
  72. package/lib/ui/globals/signals/createTyping.d.ts +27 -0
  73. package/lib/ui/globals/signals/createTyping.d.ts.map +1 -0
  74. package/lib/ui/globals/types.d.ts +54 -0
  75. package/lib/ui/globals/types.d.ts.map +1 -0
  76. package/lib/ui/utils.d.ts +11 -0
  77. package/lib/ui/utils.d.ts.map +1 -0
  78. package/package.json +2 -3
  79. package/src/helpers/clamp.ts +3 -0
  80. package/src/helpers/createContext.ts +20 -4
  81. package/src/helpers/index.ts +2 -0
  82. package/src/index.ts +9 -1
  83. package/src/services/main.machine.data.ts +13 -2
  84. package/src/services/main.machine.helpers.ts +12 -4
  85. package/src/services/main.machine.test.ts +205 -0
  86. package/src/services/main.machine.ts +253 -75
  87. package/src/services/main.machine.typings.ts +31 -6
  88. package/src/ui/Flow.tsx +47 -8
  89. package/src/ui/components/Bounds.tsx +0 -2
  90. package/src/ui/components/EdgeComponent.tsx +60 -46
  91. package/src/ui/components/EdgesBoard.tsx +1 -2
  92. package/src/ui/components/EditPanel.hooks.ts +69 -0
  93. package/src/ui/components/EditPanel.tsx +89 -0
  94. package/src/ui/components/EditPanel.types.ts +49 -0
  95. package/src/ui/components/FlowChart.context.ts +22 -141
  96. package/src/ui/components/FlowChart.tsx +27 -64
  97. package/src/ui/components/FlowChart.types.ts +92 -0
  98. package/src/ui/components/NodeComponent.tsx +88 -65
  99. package/src/ui/components/NodesBoard.tsx +61 -56
  100. package/src/ui/components/Panels.tsx +40 -0
  101. package/src/ui/globals/components/atoms/TypingText.tsx +63 -0
  102. package/src/ui/globals/components/molecules/FadingDots.tsx +45 -0
  103. package/src/ui/globals/components/organisms/LoadingFallback.tsx +116 -0
  104. package/src/ui/globals/constants/index.ts +2 -0
  105. package/src/ui/globals/constants/links.ts +15 -0
  106. package/src/ui/globals/constants/strings.ts +5 -0
  107. package/src/ui/globals/directives/clickOutside.ts +18 -0
  108. package/src/ui/globals/directives/index.ts +16 -0
  109. package/src/ui/globals/directives/mouseOut.test.ts +115 -0
  110. package/src/ui/globals/directives/mouseOut.ts +98 -0
  111. package/src/ui/globals/directives/resize.ts +33 -0
  112. package/src/ui/globals/helpers/espace.ts +26 -0
  113. package/src/ui/globals/signals/createTyping.ts +82 -0
  114. package/src/ui/globals/types.ts +83 -0
  115. package/src/ui/utils.ts +14 -0
@@ -6,7 +6,14 @@ import { nanoid } from 'nanoid';
6
6
  /** Type alias for drag-drop state extracted from {@linkcode useDragDropContext}. */
7
7
  export type DragDropState = Exclude<ReturnType<typeof useDragDropContext>, null>[0];
8
8
 
9
- import { DEFAULT_INPUT_OFFSET, getDefaultOutputOffset } from './main.machine.data';
9
+ import { clamp } from '..';
10
+ import {
11
+ DEFAULT_DATA,
12
+ DEFAULT_INPUT_OFFSET,
13
+ DEFAULT_SIZE,
14
+ getDefaultOutputOffset,
15
+ PARENT_CHILD_GAP_WIDTH,
16
+ } from './main.machine.data';
10
17
  import {
11
18
  buildEdgeId,
12
19
  buildNodeID,
@@ -17,6 +24,7 @@ import {
17
24
  edgeJSON,
18
25
  extremities,
19
26
  newEdge,
27
+ data,
20
28
  nodeJSON,
21
29
  point,
22
30
  vector,
@@ -24,13 +32,14 @@ import {
24
32
  type Dimension,
25
33
  type Point,
26
34
  type Board,
35
+ type Vector,
27
36
  } from './main.machine.typings';
28
37
 
29
38
  /**
30
39
  * State machine managing flowchart state transitions, nodes, edges, selection, and
31
40
  * layout actions.
32
41
  *
33
- * @see {@linkcode calculateDimensions}, {@linkcode buildEdgeId}, {@linkcode buildNodeID}, {@linkcode getDefaultOutputOffset}, {@linkcode DEFAULT_INPUT_OFFSET}
42
+ * @see {@linkcode calculateDimensions}, {@linkcode buildEdgeId}, {@linkcode buildNodeID}, {@linkcode getDefaultOutputOffset}, {@linkcode DEFAULT_INPUT_OFFSET}, {@linkcode DEFAULT_SIZE}, {@linkcode DEFAULT_DATA}
34
43
  */
35
44
  export const machine = createMachine(
36
45
  {
@@ -40,7 +49,7 @@ export const machine = createMachine(
40
49
  states: {
41
50
  idle: {
42
51
  on: {
43
- CONFIGURE: { actions: ['configure'], target: '/working' },
52
+ CONFIGURE: { actions: ['configure'], target: '/construction' },
44
53
  CONFIGURE_EMPTY: '/working',
45
54
  },
46
55
  },
@@ -49,6 +58,7 @@ export const machine = createMachine(
49
58
 
50
59
  working: {
51
60
  on: {
61
+ CONFIGURE: { actions: ['configure'], target: '/construction' },
52
62
  MOVE_IMMEDIATE: {
53
63
  actions: [{ name: 'buildUI', description: 'Must be in the ui' }],
54
64
  },
@@ -71,8 +81,6 @@ export const machine = createMachine(
71
81
  target: '/construction',
72
82
  },
73
83
 
74
- ADD_DIMENSION: { actions: ['addDimension'] },
75
-
76
84
  ADD_SIBLING: {
77
85
  actions: [
78
86
  'generateID',
@@ -82,6 +90,7 @@ export const machine = createMachine(
82
90
  target: '/construction',
83
91
  },
84
92
 
93
+ RESIZE: { actions: ['resize', 'buildUI'] },
85
94
  MOVE: { actions: ['moveNode', 'buildUI'], target: '/construction' },
86
95
  ADD_EDGE: { actions: ['addEdge'], target: '/construction' },
87
96
  START_NEW_EDGE: { actions: ['startNewEdge'] },
@@ -93,15 +102,19 @@ export const machine = createMachine(
93
102
  ZOOM: { actions: ['zoom'] },
94
103
  TOGGLE_ZOOM: { actions: ['toggleZoom'] },
95
104
  SET_BOARD: { actions: ['setBoard'] },
105
+ SET_NODE_DATA: { actions: ['setNodeData'] },
106
+ EDIT: { actions: ['edit'] },
107
+ STOP_EDIT: { actions: ['stopEdit'] },
96
108
  },
97
109
  },
98
110
  },
99
111
  },
100
112
  {
101
- eventsMap: type(({ intersection, use, array }) => ({
113
+ eventsMap: type(({ intersection, use, array, optional }) => ({
102
114
  CONFIGURE: {
103
115
  nodes: array(intersection(use(nodeJSON), { id: 'string' })),
104
116
  edges: array(intersection(use(edgeJSON), { id: 'string' })),
117
+ defaultData: optional(use(data)),
105
118
  },
106
119
 
107
120
  SET_BOARD: use(board),
@@ -114,13 +127,16 @@ export const machine = createMachine(
114
127
  DELETE: 'string',
115
128
  SELECT: 'string',
116
129
  DESELECT: 'never',
130
+ EDIT: 'string',
131
+ STOP_EDIT: 'never',
117
132
  ADD_EDGE: use(extremities),
118
133
  START_NEW_EDGE: 'string',
119
134
  MOVE_NEW_EDGE: use(point),
120
135
  CLEAR_NEW_EDGE: 'never',
121
136
  ZOOM: 'number',
122
137
  TOGGLE_ZOOM: 'never',
123
- ADD_DIMENSION: { id: 'string', dimension: use(dimension) },
138
+ RESIZE: { id: 'string', size: { width: 'number', height: 'number' } },
139
+ SET_NODE_DATA: { id: 'string', data: use(data) },
124
140
  })),
125
141
 
126
142
  sync: true,
@@ -128,6 +144,7 @@ export const machine = createMachine(
128
144
  generatedId: union('string', 'null'),
129
145
  previousZoom: optional('number'),
130
146
  dimensions: record(use(dimension)),
147
+ defaultData: optional(use(data)),
131
148
 
132
149
  getBoardPosition:
133
150
  custom<(clientX: number, clientY: number, board: Board) => Point>(),
@@ -172,6 +189,7 @@ export const machine = createMachine(
172
189
  edgesPositions: record(use(vector)),
173
190
  newEdge: optional(use(newEdge)),
174
191
  selected: optional('string'),
192
+ editing: optional('string'),
175
193
  updatingUI: optional('boolean'),
176
194
  zoom: 'number',
177
195
  bounds: use(point),
@@ -180,21 +198,46 @@ export const machine = createMachine(
180
198
  ).provideOptions(({ assign, batch, erase, filter, action }) => ({
181
199
  actions: {
182
200
  configure: batch(
183
- assign('data', { CONFIGURE: ({ payload }) => payload }),
201
+ assign('data', {
202
+ CONFIGURE: ({ payload: { nodes, edges } }) => ({ nodes, edges }),
203
+ }),
184
204
  assign('newEdge', () => undefined),
185
205
  assign('updatingUI', () => false),
186
206
  action(({ pContext }) => {
187
207
  pContext.generatedId = null;
188
208
  }),
189
209
  action({
190
- CONFIGURE: ({ payload: { nodes }, pContext: { dimensions } }) => {
210
+ CONFIGURE: ({ payload: { nodes, defaultData }, pContext }) => {
211
+ pContext.defaultData = defaultData;
191
212
  nodes.forEach(({ id, position }) => {
192
- dimensions[id] = calculateDimensions(position);
213
+ pContext.dimensions[id] = calculateDimensions(position);
193
214
  });
194
215
  },
195
216
  }),
196
217
  ),
197
218
 
219
+ setNodeData: assign('data.nodes', {
220
+ SET_NODE_DATA: ({ context: { data }, payload: { id, data: newData } }) => {
221
+ return data?.nodes?.map(node => {
222
+ if (node.id === id) {
223
+ return { ...node, data: { ...node.data, ...newData } };
224
+ }
225
+ return node;
226
+ });
227
+ },
228
+ }),
229
+
230
+ setBoard: assign('board', { SET_BOARD: ({ payload }) => payload }),
231
+ generateID: action(({ pContext }) => (pContext.generatedId = nanoid())),
232
+ select: assign('selected', { SELECT: ({ payload }) => payload }),
233
+ clearNewEdge: erase('newEdge'),
234
+ deselect: batch(erase('selected'), erase('editing')),
235
+ edit: batch(
236
+ assign('editing', { EDIT: ({ payload }) => payload }),
237
+ assign('selected', { EDIT: ({ payload }) => payload }),
238
+ ),
239
+ stopEdit: erase('editing'),
240
+
198
241
  startNewEdge: assign('newEdge', {
199
242
  START_NEW_EDGE: ({ payload: from, pContext: { dimensions } }) => {
200
243
  const { x, y } = dimensions[from].output;
@@ -202,8 +245,6 @@ export const machine = createMachine(
202
245
  },
203
246
  }),
204
247
 
205
- setBoard: assign('board', { SET_BOARD: ({ payload }) => payload }),
206
-
207
248
  buildUI: batch(
208
249
  assign('edgesPositions', {
209
250
  MOVE: ({
@@ -213,11 +254,12 @@ export const machine = createMachine(
213
254
  }) => {
214
255
  const edges = data?.edges;
215
256
  const dimension = dimensions[payload.id];
257
+ if (!dimension) return;
216
258
 
217
259
  const outputOffset =
218
- dimension?.outputOffset ?? getDefaultOutputOffset(dimension?.width);
260
+ dimension.outputOffset ?? getDefaultOutputOffset(dimension.width);
219
261
 
220
- const inputOffset = dimension?.inputOffset ?? DEFAULT_INPUT_OFFSET;
262
+ const inputOffset = dimension.inputOffset ?? DEFAULT_INPUT_OFFSET;
221
263
 
222
264
  const output = {
223
265
  x: payload.x + outputOffset.x,
@@ -229,107 +271,125 @@ export const machine = createMachine(
229
271
  y: payload.y + inputOffset.y,
230
272
  };
231
273
 
232
- if (dimension) {
233
- dimension.output = output;
234
- dimension.input = input;
235
- }
274
+ dimension.output = output;
275
+ dimension.input = input;
236
276
 
237
277
  edges?.forEach(({ from, to, id }) => {
278
+ const edgePosition = edgesPositions[id];
279
+ if (!edgePosition) return;
280
+
238
281
  if (from === payload.id) {
239
- edgesPositions[id].x0 = output.x;
240
- edgesPositions[id].y0 = output.y;
282
+ edgePosition.x0 = output.x;
283
+ edgePosition.y0 = output.y;
241
284
  }
242
285
  if (to === payload.id) {
243
- edgesPositions[id].x1 = input.x;
244
- edgesPositions[id].y1 = input.y;
286
+ edgePosition.x1 = input.x;
287
+ edgePosition.y1 = input.y;
245
288
  }
246
289
  });
247
290
 
248
291
  return edgesPositions;
249
292
  },
293
+
250
294
  MOVE_IMMEDIATE: ({
251
295
  context: { data, edgesPositions },
252
296
  payload,
253
297
  pContext: { dimensions },
254
298
  }) => {
255
- const edges = data?.edges;
299
+ data?.edges?.forEach(({ from, to, id }) => {
300
+ const dimension = dimensions[payload.id];
301
+ const edgePosition = edgesPositions[id];
302
+ if (!edgePosition || !dimension) return;
256
303
 
257
- edges?.forEach(({ from, to, id }) => {
258
304
  if (from === payload.id) {
259
- const dimension = dimensions[payload.id];
260
305
  const offset =
261
- dimension?.outputOffset ?? getDefaultOutputOffset(dimension?.width);
306
+ dimension.outputOffset ?? getDefaultOutputOffset(dimension.width);
262
307
 
263
308
  const x0 = payload.x + offset.x;
264
309
  const y0 = payload.y + offset.y;
265
- edgesPositions[id].x0 = x0;
266
- edgesPositions[id].y0 = y0;
267
-
268
- if (dimension) {
269
- dimension.output.x = x0;
270
- dimension.output.y = y0;
271
- }
310
+ edgePosition.x0 = x0;
311
+ edgePosition.y0 = y0;
312
+ dimension.output = { x: x0, y: y0 };
272
313
  }
273
314
 
274
315
  if (to === payload.id) {
275
- const dimension = dimensions[payload.id];
276
316
  const offset = dimension?.inputOffset ?? DEFAULT_INPUT_OFFSET;
277
-
278
317
  const x1 = payload.x + offset.x;
279
318
  const y1 = payload.y + offset.y;
280
- edgesPositions[id].x1 = x1;
281
- edgesPositions[id].y1 = y1;
282
-
283
- if (dimension) {
284
- dimension.input = { x: x1, y: y1 };
285
- }
319
+ edgePosition.x1 = x1;
320
+ edgePosition.y1 = y1;
321
+ dimension.input = { x: x1, y: y1 };
286
322
  }
287
323
  });
288
324
 
289
325
  return edgesPositions;
290
326
  },
291
- else: ({ context: { data, edgesPositions }, pContext: { dimensions } }) => {
292
- const edges = data?.edges ?? [];
293
- edgesPositions = {};
294
327
 
295
- edges.forEach(({ from, id, to }) => {
328
+ else: ({ context: { data }, pContext: { dimensions } }) => {
329
+ const nextEdgesPositions: Record<string, Vector> = {};
330
+
331
+ data?.edges?.forEach(({ from, id, to }) => {
296
332
  const output = dimensions[from]?.output;
297
333
  const input = dimensions[to]?.input;
298
334
 
299
335
  if (output && input) {
300
- edgesPositions[id] = {
336
+ nextEdgesPositions[id] = {
301
337
  x0: output.x,
302
338
  y0: output.y,
303
339
  x1: input.x,
304
340
  y1: input.y,
305
341
  };
306
- } else {
307
- delete edgesPositions[id];
308
342
  }
309
343
  });
310
344
 
311
- return edgesPositions;
345
+ return nextEdgesPositions;
312
346
  },
313
347
  }),
314
348
 
315
349
  assign('updatingUI', () => true),
316
350
  ),
317
351
 
318
- addDimension: action({
319
- ADD_DIMENSION: ({ payload: { id, dimension }, pContext: { dimensions } }) => {
320
- dimensions[id] = dimension;
352
+ resize: action({
353
+ RESIZE: ({
354
+ payload: {
355
+ id,
356
+ size: { width, height },
357
+ },
358
+ context: { data },
359
+ pContext: { dimensions },
360
+ }) => {
361
+ const dimension = dimensions[id];
362
+ if (!dimension) return;
363
+
364
+ dimension.width = width;
365
+ dimension.height = height;
366
+
367
+ const node = data?.nodes?.find(n => n.id === id);
368
+ const outputOffset = getDefaultOutputOffset(width);
369
+ const inputOffset = dimension.inputOffset ?? DEFAULT_INPUT_OFFSET;
370
+
371
+ dimension.outputOffset = outputOffset;
372
+ dimension.inputOffset = inputOffset;
373
+
374
+ if (node) {
375
+ dimension.output = {
376
+ x: node.position.x + outputOffset.x,
377
+ y: node.position.y + outputOffset.y,
378
+ };
379
+ dimension.input = {
380
+ x: node.position.x + inputOffset.x,
381
+ y: node.position.y + inputOffset.y,
382
+ };
383
+ } else {
384
+ dimension.output = { x: dimension.output.x, y: dimension.output.y };
385
+ }
321
386
  },
322
387
  }),
323
388
 
324
- generateID: action(({ pContext }) => {
325
- pContext.generatedId = nanoid();
326
- }),
327
-
328
389
  linkChild: batch(
329
390
  assign('data.edges', {
330
- ADD_CHILD: ({ context: { data }, pContext, payload }) => {
391
+ ADD_CHILD: ({ context: { data }, pContext, payload: from }) => {
331
392
  const edges = toArray.typed(data?.edges);
332
- const from = payload;
333
393
  const generatedId = pContext?.generatedId;
334
394
  const to = buildNodeID(generatedId);
335
395
  const id = buildEdgeId(from, to);
@@ -368,22 +428,14 @@ export const machine = createMachine(
368
428
  ),
369
429
 
370
430
  moveNode: assign('data.nodes', {
371
- MOVE: ({ context: { data }, payload }) => {
372
- const { id, x, y } = payload;
373
-
374
- return (
375
- data?.nodes?.map(d => {
376
- if (d.id === id) {
377
- return { ...d, position: { x, y } };
378
- }
379
- return d;
380
- }) ?? []
381
- );
431
+ MOVE: ({ context: { data }, payload: { id, x, y } }) => {
432
+ return data?.nodes?.map(node => {
433
+ if (node.id === id) return { ...node, position: { x, y } };
434
+ return node;
435
+ });
382
436
  },
383
437
  }),
384
438
 
385
- select: assign('selected', { SELECT: ({ payload }) => payload }),
386
-
387
439
  delete: batch(
388
440
  filter('data.edges', {
389
441
  DELETE: ({ id, from, to }, _, { payload }) => {
@@ -392,6 +444,7 @@ export const machine = createMachine(
392
444
  }),
393
445
 
394
446
  filter('data.nodes', { DELETE: ({ id }, _, { payload }) => id !== payload }),
447
+ erase('editing'),
395
448
  ),
396
449
 
397
450
  addEdge: batch(
@@ -405,16 +458,14 @@ export const machine = createMachine(
405
458
  return out;
406
459
  },
407
460
  }),
461
+
408
462
  erase('newEdge'),
409
463
  ),
410
464
 
411
- clearNewEdge: erase('newEdge'),
412
- deselect: erase('selected'),
413
-
414
465
  zoom: assign('zoom', {
415
466
  ZOOM: ({ context: { zoom }, payload, pContext }) => {
416
467
  const next = zoom + payload;
417
- const clamped = Math.min(Math.max(Number(next.toFixed(2)), 0.2), 3);
468
+ const clamped = clamp(next, 0.1, 3);
418
469
  pContext.previousZoom = undefined;
419
470
  return clamped;
420
471
  },
@@ -433,5 +484,132 @@ export const machine = createMachine(
433
484
  return 1;
434
485
  },
435
486
  }),
487
+
488
+ // #region UI
489
+ placeChild: assign('data.nodes', {
490
+ ADD_CHILD: ({ payload, context: { data, board }, pContext }) => {
491
+ if (!board) return data?.nodes;
492
+
493
+ const nodes = data?.nodes;
494
+ if (!payload) return nodes;
495
+
496
+ const parentNode = nodes?.find(node => node.id === payload);
497
+ if (!parentNode) return nodes;
498
+
499
+ const parentDimension = pContext.dimensions[payload];
500
+ const id = `node-${pContext.generatedId}`;
501
+ const width = parentDimension?.width ?? DEFAULT_SIZE.width;
502
+ const height = parentDimension?.height ?? DEFAULT_SIZE.height;
503
+ const initialX = parentNode.position.x + width + PARENT_CHILD_GAP_WIDTH;
504
+ const initialY = parentNode.position.y;
505
+
506
+ const position = pContext.clampPosition(
507
+ board,
508
+ initialX,
509
+ initialY,
510
+ width,
511
+ height,
512
+ );
513
+
514
+ const defaultData = pContext.defaultData ?? DEFAULT_DATA;
515
+
516
+ nodes?.push({ id, data: { ...defaultData }, position });
517
+
518
+ pContext.dimensions[id] = pContext.calculateDimensions(
519
+ position,
520
+ parentDimension ?? DEFAULT_SIZE,
521
+ );
522
+
523
+ return nodes;
524
+ },
525
+ }),
526
+
527
+ placeParent: assign('data.nodes', {
528
+ ADD_PARENT: ({ context: { data, zoom = 1, board }, pContext }) => {
529
+ if (!board) return data?.nodes;
530
+
531
+ const nodes = data?.nodes;
532
+ const id = `node-${pContext.generatedId}`;
533
+ const container = board.parent;
534
+ const scrollLeft = container?.scrollLeft ?? 0;
535
+ const scrollTop = container?.scrollTop ?? 0;
536
+ const width = container?.width ?? 0;
537
+ const height = container?.height ?? 0;
538
+ const currentZoom = zoom;
539
+ const x = (scrollLeft + width / 2) / currentZoom;
540
+ const y = (scrollTop + height / 2) / currentZoom;
541
+ const position = pContext.clampPosition(
542
+ board,
543
+ x,
544
+ y,
545
+ DEFAULT_SIZE.width,
546
+ DEFAULT_SIZE.height,
547
+ );
548
+
549
+ const defaultData = pContext.defaultData ?? DEFAULT_DATA;
550
+
551
+ nodes?.push({ id, data: { ...defaultData }, position });
552
+
553
+ pContext.dimensions[id] = pContext.calculateDimensions(
554
+ position,
555
+ DEFAULT_SIZE,
556
+ );
557
+ return nodes;
558
+ },
559
+ }),
560
+
561
+ placeSibling: assign('data.nodes', {
562
+ ADD_SIBLING: ({ payload, context: { data, board }, pContext }) => {
563
+ if (!board) return data?.nodes;
564
+
565
+ const edges = data?.edges;
566
+ const nodes = data?.nodes;
567
+ const parentID = edges?.find(edge => edge.to === payload)?.from;
568
+ if (!parentID) return nodes;
569
+
570
+ const parentNode = nodes?.find(node => node.id === parentID);
571
+ if (!parentNode) return nodes;
572
+
573
+ const parentDimension = pContext.dimensions[parentID];
574
+ const id = `node-${pContext.generatedId}`;
575
+ const width = parentDimension?.width ?? DEFAULT_SIZE.width;
576
+ const height = parentDimension?.height ?? DEFAULT_SIZE.height;
577
+ const initialX = parentNode.position.x + width + PARENT_CHILD_GAP_WIDTH;
578
+ const initialY = parentNode.position.y + PARENT_CHILD_GAP_WIDTH;
579
+
580
+ const position = pContext.clampPosition(
581
+ board,
582
+ initialX,
583
+ initialY,
584
+ width,
585
+ height,
586
+ );
587
+
588
+ const defaultData = pContext.defaultData ?? DEFAULT_DATA;
589
+
590
+ nodes?.push({ id, data: { ...defaultData }, position });
591
+
592
+ pContext.dimensions[id] = pContext.calculateDimensions(
593
+ position,
594
+ parentDimension ?? DEFAULT_SIZE,
595
+ );
596
+
597
+ return nodes;
598
+ },
599
+ }),
600
+
601
+ moveNewEdge: assign('newEdge', {
602
+ MOVE_NEW_EDGE: ({ context: { newEdge, board }, payload, pContext }) => {
603
+ if (!board) return undefined;
604
+ if (!newEdge) return undefined;
605
+ const { x: x1, y: y1 } = pContext.getBoardPosition(
606
+ payload.x,
607
+ payload.y,
608
+ board,
609
+ );
610
+ return { ...newEdge, x1, y1 };
611
+ },
612
+ }),
613
+ // #endregion
436
614
  },
437
615
  }));
@@ -12,25 +12,47 @@ export type Point = inferT<typeof point>;
12
12
  *
13
13
  * @see {@linkcode point}
14
14
  */
15
- export const nodeOffset = type(({ optional, use }) => ({
16
- input: optional(use(point)),
15
+ export const nodeOffset = type(({ use }) => ({
16
+ input: use(point),
17
17
  output: use(point),
18
18
  }));
19
19
 
20
20
  /** Schema definition for edge extremities. */
21
21
  export const extremities = type({ from: 'string', to: 'string' });
22
22
 
23
+ /** Schema definition for primitive values allowed in node data. */
24
+ export const nodeDataValue = type(({ union }) =>
25
+ union('string', 'boolean', 'number'),
26
+ );
27
+
28
+ /**
29
+ * Schema definition for serialized node data dictionary.
30
+ *
31
+ * @see {@linkcode nodeDataValue}
32
+ */
33
+ export const data = type(({ record, use }) => record(use(nodeDataValue)));
34
+
35
+ /** Serialized node data dictionary type inferred from schema {@linkcode data}. */
36
+ export type NodeData = Record<string, string | boolean | number>;
37
+
23
38
  /**
24
39
  * Schema definition for a serialized flowchart node entity.
25
40
  *
26
- * @see {@linkcode point}
41
+ * @see {@linkcode point}, {@linkcode data}
27
42
  */
28
- export const nodeJSON = type(({ optional, use }) => ({
43
+ export const nodeJSON = type(({ use }) => ({
29
44
  position: use(point),
30
- data: { label: optional('string'), content: 'string' },
31
- input: 'boolean',
45
+ data: use(data),
32
46
  }));
33
47
 
48
+ /**
49
+ * Serialized node properties type inferred from schema {@linkcode nodeJSON}.
50
+ *
51
+ * @template | {@linkcode NodeData} `D` - Custom data properties type extending
52
+ * {@linkcode NodeData}.
53
+ */
54
+ export type NodeProps<D extends NodeData = NodeData> = { position: Point; data: D };
55
+
34
56
  /**
35
57
  * Schema definition for a serialized flowchart edge entity.
36
58
  *
@@ -38,6 +60,9 @@ export const nodeJSON = type(({ optional, use }) => ({
38
60
  */
39
61
  export const edgeJSON = extremities;
40
62
 
63
+ /** Serialized edge properties type inferred from schema {@linkcode edgeJSON}. */
64
+ export type EdgeProps = inferT<typeof edgeJSON>;
65
+
41
66
  /**
42
67
  * Schema definition for layout dimensions and connection points of a node.
43
68
  *
package/src/ui/Flow.tsx CHANGED
@@ -1,21 +1,60 @@
1
- import type { Component } from 'solid-js';
1
+ import { createSignal, onMount, Show, type JSX } from 'solid-js';
2
+ import {} from 'solid-js/web';
2
3
 
3
- import { FlowChart, type FlowProps } from './components/FlowChart';
4
+ import type { NodeData } from '../services/main.machine.typings';
5
+ import { FlowChart } from './components/FlowChart';
4
6
  import { Provider } from './components/FlowChart.context';
7
+ import type { FlowProps } from './components/FlowChart.types';
8
+ import { LoadingFallback } from './globals/components/organisms/LoadingFallback';
5
9
 
6
10
  /**
7
11
  * Root Flow component wrapping the {@linkcode FlowChart} inside the flow context
8
12
  * provider.
9
13
  *
14
+ * @template | {@linkcode NodeData} `D` - Custom node data dictionary type extending
15
+ * {@linkcode NodeData}.
16
+ *
10
17
  * @param props - Flow chart configuration and callbacks of type
11
18
  * {@linkcode FlowProps}.
12
19
  *
13
20
  * @returns The rendered Solid component.
14
21
  *
15
- * @see {@linkcode Provider}
22
+ * @see {@linkcode Provider}, {@linkcode FlowChart}
16
23
  */
17
- export const Flow: Component<FlowProps> = props => (
18
- <Provider>
19
- <FlowChart {...props} />
20
- </Provider>
21
- );
24
+ export const Flow = <D extends NodeData = NodeData>(
25
+ props: FlowProps<D>,
26
+ ): JSX.Element => {
27
+ const [isMounted, setIsMounted] = createSignal(false);
28
+
29
+ // onMount never runs on the server, only in the browser
30
+ onMount(() => setTimeout(() => setIsMounted(true), props.delay));
31
+ return (
32
+ <div
33
+ style={{
34
+ 'background-image':
35
+ 'radial-gradient(circle, #b8b8b8bf 1px, rgba(0, 0, 0, 0) 1px)',
36
+ }}
37
+
38
+ class='relative flex h-full w-full rounded-lg border-2 border-gray-600 bg-white bg-size-[30px_30px]'
39
+ >
40
+ <Show
41
+ when={isMounted()}
42
+ keyed
43
+ fallback={
44
+ <div class='relative flex flex-1 items-center justify-center'>
45
+ <LoadingFallback
46
+ message='Chart is loading...'
47
+ interval={35}
48
+ rewind
49
+ rewindDelay={1_000}
50
+ />
51
+ </div>
52
+ }
53
+ >
54
+ <Provider>
55
+ <FlowChart {...props} />
56
+ </Provider>
57
+ </Show>
58
+ </div>
59
+ );
60
+ };
@@ -17,9 +17,7 @@ import { useFlow } from './FlowChart.context';
17
17
  */
18
18
  export const DragBounds: Component = () => {
19
19
  const service = useFlow();
20
-
21
20
  const zoom = createState(service, { selector: ({ context }) => context.zoom });
22
-
23
21
  const board = createState(service, { selector: ({ context }) => context.board });
24
22
 
25
23
  const [state, { addTransformer, removeTransformer, onDragStart, onDragEnd }] =