@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
@@ -1,39 +1,42 @@
1
1
  import { createMachine } from '@bemedev/app';
2
- import { type } from '@bemedev/app/bemedev';
2
+ import { toArray, type } from '@bemedev/app/bemedev';
3
+ import type { useDragDropContext } from '@thisbeyond/solid-dnd';
3
4
  import { nanoid } from 'nanoid';
4
5
 
5
- import { edgeJSON, extremities, nodeJSON } from './main.typings';
6
+ /** Type alias for drag-drop state extracted from {@linkcode useDragDropContext}. */
7
+ export type DragDropState = Exclude<ReturnType<typeof useDragDropContext>, null>[0];
6
8
 
7
- /**
8
- * Constructs a unique edge identifier string from source and destination node IDs.
9
- *
10
- * @param out - The source node ID string.
11
- * @param _in - The destination node ID string.
12
- *
13
- * @returns Formatted edge identifier string.
14
- */
15
- export const buildEdgeId = (out: string, _in: string) => {
16
- return `edge = ${out} => ${_in}`;
17
- };
18
-
19
- /**
20
- * Constructs a formatted node identifier string from a generated ID.
21
- *
22
- * @param generated - The generated unique ID string or `null`/`undefined`.
23
- *
24
- * @returns Formatted node identifier string.
25
- */
26
- export const buildNodeID = (generated?: string | null) => {
27
- return `node-${generated}`;
28
- };
9
+ import { DEFAULT_INPUT_OFFSET, getDefaultOutputOffset } from './main.machine.data';
10
+ import {
11
+ buildEdgeId,
12
+ buildNodeID,
13
+ calculateDimensions,
14
+ } from './main.machine.helpers';
15
+ import {
16
+ dimension,
17
+ edgeJSON,
18
+ extremities,
19
+ newEdge,
20
+ nodeJSON,
21
+ point,
22
+ vector,
23
+ board,
24
+ type Dimension,
25
+ type Point,
26
+ type Board,
27
+ } from './main.machine.typings';
29
28
 
30
29
  /**
31
30
  * State machine managing flowchart state transitions, nodes, edges, selection, and
32
31
  * layout actions.
32
+ *
33
+ * @see {@linkcode calculateDimensions}, {@linkcode buildEdgeId}, {@linkcode buildNodeID}, {@linkcode getDefaultOutputOffset}, {@linkcode DEFAULT_INPUT_OFFSET}
33
34
  */
34
35
  export const machine = createMachine(
35
36
  {
36
37
  initial: 'idle',
38
+
39
+ on: { SET_BOARD: { actions: ['setBoard'] } },
37
40
  states: {
38
41
  idle: {
39
42
  on: {
@@ -47,9 +50,7 @@ export const machine = createMachine(
47
50
  working: {
48
51
  on: {
49
52
  MOVE_IMMEDIATE: {
50
- actions: [
51
- { name: 'buildImmediateUI', description: 'Must be in the ui' },
52
- ],
53
+ actions: [{ name: 'buildUI', description: 'Must be in the ui' }],
53
54
  },
54
55
 
55
56
  ADD_CHILD: {
@@ -70,6 +71,8 @@ export const machine = createMachine(
70
71
  target: '/construction',
71
72
  },
72
73
 
74
+ ADD_DIMENSION: { actions: ['addDimension'] },
75
+
73
76
  ADD_SIBLING: {
74
77
  actions: [
75
78
  'generateID',
@@ -81,9 +84,15 @@ export const machine = createMachine(
81
84
 
82
85
  MOVE: { actions: ['moveNode', 'buildUI'], target: '/construction' },
83
86
  ADD_EDGE: { actions: ['addEdge'], target: '/construction' },
87
+ START_NEW_EDGE: { actions: ['startNewEdge'] },
88
+ MOVE_NEW_EDGE: { actions: ['moveNewEdge'] },
89
+ CLEAR_NEW_EDGE: { actions: ['clearNewEdge'] },
84
90
  DELETE: { actions: ['delete'], target: '/construction' },
85
91
  SELECT: { actions: ['select'] },
86
92
  DESELECT: { actions: ['deselect'] },
93
+ ZOOM: { actions: ['zoom'] },
94
+ TOGGLE_ZOOM: { actions: ['toggleZoom'] },
95
+ SET_BOARD: { actions: ['setBoard'] },
87
96
  },
88
97
  },
89
98
  },
@@ -95,6 +104,7 @@ export const machine = createMachine(
95
104
  edges: array(intersection(use(edgeJSON), { id: 'string' })),
96
105
  },
97
106
 
107
+ SET_BOARD: use(board),
98
108
  CONFIGURE_EMPTY: 'never',
99
109
  MOVE: { id: 'string', x: 'number', y: 'number' },
100
110
  MOVE_IMMEDIATE: { id: 'string', x: 'number', y: 'number' },
@@ -105,79 +115,261 @@ export const machine = createMachine(
105
115
  SELECT: 'string',
106
116
  DESELECT: 'never',
107
117
  ADD_EDGE: use(extremities),
118
+ START_NEW_EDGE: 'string',
119
+ MOVE_NEW_EDGE: use(point),
120
+ CLEAR_NEW_EDGE: 'never',
121
+ ZOOM: 'number',
122
+ TOGGLE_ZOOM: 'never',
123
+ ADD_DIMENSION: { id: 'string', dimension: use(dimension) },
108
124
  })),
109
125
 
110
126
  sync: true,
111
- pContext: type(({ union }) => ({ generatedId: union('string', 'null') })),
127
+ pContext: type(({ union, optional, record, use, custom }) => ({
128
+ generatedId: union('string', 'null'),
129
+ previousZoom: optional('number'),
130
+ dimensions: record(use(dimension)),
131
+
132
+ getBoardPosition:
133
+ custom<(clientX: number, clientY: number, board: Board) => Point>(),
112
134
 
113
- context: type(({ optional, use, array }) => ({
135
+ clampPosition:
136
+ custom<
137
+ (
138
+ board: Board,
139
+ x: number,
140
+ y: number,
141
+ nodeWidth?: number,
142
+ nodeHeight?: number,
143
+ ) => Point
144
+ >(),
145
+
146
+ calculateDimensions:
147
+ custom<
148
+ (
149
+ position: { x: number; y: number },
150
+ parentDimension?: Pick<
151
+ {
152
+ width: number;
153
+ height: number;
154
+ output: { x: number; y: number };
155
+ input?: { x: number; y: number } | undefined;
156
+ inputOffset?: { x: number; y: number } | undefined;
157
+ outputOffset?: { x: number; y: number } | undefined;
158
+ },
159
+ 'width' | 'height' | 'inputOffset' | 'outputOffset'
160
+ >,
161
+ ) => Dimension
162
+ >(),
163
+ })),
164
+
165
+ context: type(({ optional, use, array, record }) => ({
114
166
  data: optional({
115
167
  nodes: array({ ...use(nodeJSON), id: 'string' }),
116
168
  edges: array({ ...use(edgeJSON), id: 'string' }),
117
169
  }),
170
+ board: optional(use(board)),
118
171
 
172
+ edgesPositions: record(use(vector)),
173
+ newEdge: optional(use(newEdge)),
119
174
  selected: optional('string'),
120
175
  updatingUI: optional('boolean'),
176
+ zoom: 'number',
177
+ bounds: use(point),
121
178
  })),
122
179
  },
123
- ).provideOptions(({ assign, batch, erase }) => ({
180
+ ).provideOptions(({ assign, batch, erase, filter, action }) => ({
124
181
  actions: {
125
182
  configure: batch(
126
- assign('context.data', () => ({ nodes: [], edges: [] })),
127
- assign('context.data.nodes', { CONFIGURE: ({ payload: { nodes } }) => nodes }),
128
- assign('context.data.edges', { CONFIGURE: ({ payload: { edges } }) => edges }),
129
- assign('context.updatingUI', () => false),
130
- assign('pContext.generatedId', () => null),
183
+ assign('data', { CONFIGURE: ({ payload }) => payload }),
184
+ assign('newEdge', () => undefined),
185
+ assign('updatingUI', () => false),
186
+ action(({ pContext }) => {
187
+ pContext.generatedId = null;
188
+ }),
189
+ action({
190
+ CONFIGURE: ({ payload: { nodes }, pContext: { dimensions } }) => {
191
+ nodes.forEach(({ id, position }) => {
192
+ dimensions[id] = calculateDimensions(position);
193
+ });
194
+ },
195
+ }),
196
+ ),
197
+
198
+ startNewEdge: assign('newEdge', {
199
+ START_NEW_EDGE: ({ payload: from, pContext: { dimensions } }) => {
200
+ const { x, y } = dimensions[from].output;
201
+ return { from, x0: x, y0: y, x1: x, y1: y };
202
+ },
203
+ }),
204
+
205
+ setBoard: assign('board', { SET_BOARD: ({ payload }) => payload }),
206
+
207
+ buildUI: batch(
208
+ assign('edgesPositions', {
209
+ MOVE: ({
210
+ context: { data, edgesPositions },
211
+ payload,
212
+ pContext: { dimensions },
213
+ }) => {
214
+ const edges = data?.edges;
215
+ const dimension = dimensions[payload.id];
216
+
217
+ const outputOffset =
218
+ dimension?.outputOffset ?? getDefaultOutputOffset(dimension?.width);
219
+
220
+ const inputOffset = dimension?.inputOffset ?? DEFAULT_INPUT_OFFSET;
221
+
222
+ const output = {
223
+ x: payload.x + outputOffset.x,
224
+ y: payload.y + outputOffset.y,
225
+ };
226
+
227
+ const input = {
228
+ x: payload.x + inputOffset.x,
229
+ y: payload.y + inputOffset.y,
230
+ };
231
+
232
+ if (dimension) {
233
+ dimension.output = output;
234
+ dimension.input = input;
235
+ }
236
+
237
+ edges?.forEach(({ from, to, id }) => {
238
+ if (from === payload.id) {
239
+ edgesPositions[id].x0 = output.x;
240
+ edgesPositions[id].y0 = output.y;
241
+ }
242
+ if (to === payload.id) {
243
+ edgesPositions[id].x1 = input.x;
244
+ edgesPositions[id].y1 = input.y;
245
+ }
246
+ });
247
+
248
+ return edgesPositions;
249
+ },
250
+ MOVE_IMMEDIATE: ({
251
+ context: { data, edgesPositions },
252
+ payload,
253
+ pContext: { dimensions },
254
+ }) => {
255
+ const edges = data?.edges;
256
+
257
+ edges?.forEach(({ from, to, id }) => {
258
+ if (from === payload.id) {
259
+ const dimension = dimensions[payload.id];
260
+ const offset =
261
+ dimension?.outputOffset ?? getDefaultOutputOffset(dimension?.width);
262
+
263
+ const x0 = payload.x + offset.x;
264
+ 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
+ }
272
+ }
273
+
274
+ if (to === payload.id) {
275
+ const dimension = dimensions[payload.id];
276
+ const offset = dimension?.inputOffset ?? DEFAULT_INPUT_OFFSET;
277
+
278
+ const x1 = payload.x + offset.x;
279
+ 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
+ }
286
+ }
287
+ });
288
+
289
+ return edgesPositions;
290
+ },
291
+ else: ({ context: { data, edgesPositions }, pContext: { dimensions } }) => {
292
+ const edges = data?.edges ?? [];
293
+ edgesPositions = {};
294
+
295
+ edges.forEach(({ from, id, to }) => {
296
+ const output = dimensions[from]?.output;
297
+ const input = dimensions[to]?.input;
298
+
299
+ if (output && input) {
300
+ edgesPositions[id] = {
301
+ x0: output.x,
302
+ y0: output.y,
303
+ x1: input.x,
304
+ y1: input.y,
305
+ };
306
+ } else {
307
+ delete edgesPositions[id];
308
+ }
309
+ });
310
+
311
+ return edgesPositions;
312
+ },
313
+ }),
314
+
315
+ assign('updatingUI', () => true),
131
316
  ),
132
317
 
133
- generateID: assign('pContext.generatedId', () => nanoid()),
318
+ addDimension: action({
319
+ ADD_DIMENSION: ({ payload: { id, dimension }, pContext: { dimensions } }) => {
320
+ dimensions[id] = dimension;
321
+ },
322
+ }),
323
+
324
+ generateID: action(({ pContext }) => {
325
+ pContext.generatedId = nanoid();
326
+ }),
134
327
 
135
328
  linkChild: batch(
136
- assign('context.data.edges', {
137
- ADD_CHILD: ({ context, pContext, payload }) => {
138
- const data = context.data;
329
+ assign('data.edges', {
330
+ ADD_CHILD: ({ context: { data }, pContext, payload }) => {
331
+ const edges = toArray.typed(data?.edges);
139
332
  const from = payload;
140
333
  const generatedId = pContext?.generatedId;
141
334
  const to = buildNodeID(generatedId);
142
335
  const id = buildEdgeId(from, to);
143
- return [...(data?.edges ?? []), { id, from, to }];
336
+ edges.push({ id, from, to });
337
+ return edges;
144
338
  },
145
339
  }),
146
340
 
147
- assign('context.selected', ({ pContext: { generatedId } }) =>
341
+ assign('selected', ({ pContext: { generatedId } }) =>
148
342
  buildNodeID(generatedId),
149
343
  ),
150
344
  ),
151
345
 
152
346
  linkSibling: batch(
153
- assign('context.data.edges', {
154
- ADD_SIBLING: ({ pContext, payload, context }) => {
155
- const edges = context.data?.edges;
347
+ assign('data.edges', {
348
+ ADD_SIBLING: ({ pContext, payload, context: { data } }) => {
349
+ const edges = toArray.typed(data?.edges);
156
350
  const generatedId = pContext?.generatedId;
157
- const out = [...(edges ?? [])];
158
- const from = edges?.find(({ to }) => to === payload)?.from;
159
- if (!from) return out;
351
+ const from = edges.find(({ to }) => to === payload)?.from;
352
+ if (!from) return edges;
160
353
 
161
354
  const to = buildNodeID(generatedId);
162
355
  const id = buildEdgeId(from, to);
163
- out.push({ from, to, id });
164
- return out;
356
+ edges.push({ from, to, id });
357
+ return edges;
165
358
  },
166
359
  }),
167
360
 
168
- assign('context.selected', ({ pContext: { generatedId } }) =>
361
+ assign('selected', ({ pContext: { generatedId } }) =>
169
362
  buildNodeID(generatedId),
170
363
  ),
171
364
  ),
172
365
 
173
- selectParent: assign('context.selected', ({ pContext: { generatedId } }) =>
366
+ selectParent: assign('selected', ({ pContext: { generatedId } }) =>
174
367
  buildNodeID(generatedId),
175
368
  ),
176
369
 
177
- moveNode: assign('context.data.nodes', {
370
+ moveNode: assign('data.nodes', {
178
371
  MOVE: ({ context: { data }, payload }) => {
179
372
  const { id, x, y } = payload;
180
- if (!id) return data?.nodes ?? [];
181
373
 
182
374
  return (
183
375
  data?.nodes?.map(d => {
@@ -190,30 +382,56 @@ export const machine = createMachine(
190
382
  },
191
383
  }),
192
384
 
193
- select: assign('context.selected', { SELECT: ({ payload }) => payload }),
385
+ select: assign('selected', { SELECT: ({ payload }) => payload }),
194
386
 
195
- delete: assign(['context.data.nodes', 'context.data.edges'], {
196
- DELETE: ({ context: { data }, payload }) => {
197
- const nodes = data?.nodes?.filter(({ id }) => id !== payload);
198
- const edges = data?.edges?.filter(
199
- ({ id, from, to }) => id !== payload && from !== payload && to !== payload,
200
- );
387
+ delete: batch(
388
+ filter('data.edges', {
389
+ DELETE: ({ id, from, to }, _, { payload }) => {
390
+ return id !== payload && from !== payload && to !== payload;
391
+ },
392
+ }),
393
+
394
+ filter('data.nodes', { DELETE: ({ id }, _, { payload }) => id !== payload }),
395
+ ),
396
+
397
+ addEdge: batch(
398
+ assign('data.edges', {
399
+ ADD_EDGE: ({ context, payload: { from, to } }) => {
400
+ const edges = context.data?.edges ?? [];
401
+ const id = buildEdgeId(from, to);
402
+ if (edges.some(e => e.id === id)) return edges;
201
403
 
202
- return [nodes, edges];
404
+ const out = [...edges, { id, from, to }];
405
+ return out;
406
+ },
407
+ }),
408
+ erase('newEdge'),
409
+ ),
410
+
411
+ clearNewEdge: erase('newEdge'),
412
+ deselect: erase('selected'),
413
+
414
+ zoom: assign('zoom', {
415
+ ZOOM: ({ context: { zoom }, payload, pContext }) => {
416
+ const next = zoom + payload;
417
+ const clamped = Math.min(Math.max(Number(next.toFixed(2)), 0.2), 3);
418
+ pContext.previousZoom = undefined;
419
+ return clamped;
203
420
  },
204
421
  }),
205
422
 
206
- addEdge: assign('context.data.edges', {
207
- ADD_EDGE: ({ context, payload: { from, to } }) => {
208
- const edges = context.data?.edges ?? [];
209
- const id = buildEdgeId(from, to);
210
- if (edges.some(e => e.id === id)) return edges;
423
+ toggleZoom: assign('zoom', {
424
+ TOGGLE_ZOOM: ({ context: { zoom }, pContext }) => {
425
+ const previous = pContext.previousZoom;
211
426
 
212
- const out = [...edges, { id, from, to }];
213
- return out;
427
+ if (previous !== undefined) {
428
+ pContext.previousZoom = undefined;
429
+ return previous;
430
+ }
431
+
432
+ pContext.previousZoom = zoom;
433
+ return 1;
214
434
  },
215
435
  }),
216
-
217
- deselect: erase('context.selected'),
218
436
  },
219
437
  }));
@@ -0,0 +1,98 @@
1
+ import { type } from '@bemedev/app/bemedev';
2
+ import type { inferT } from '@bemedev/app/typings';
3
+
4
+ /** Schema definition for 2D coordinates `(x, y)`. */
5
+ export const point = type({ x: 'number', y: 'number' });
6
+
7
+ /** 2D coordinate point type inferred from schema {@linkcode point}. */
8
+ export type Point = inferT<typeof point>;
9
+
10
+ /**
11
+ * Schema definition for node handle offsets (input and output).
12
+ *
13
+ * @see {@linkcode point}
14
+ */
15
+ export const nodeOffset = type(({ optional, use }) => ({
16
+ input: optional(use(point)),
17
+ output: use(point),
18
+ }));
19
+
20
+ /** Schema definition for edge extremities. */
21
+ export const extremities = type({ from: 'string', to: 'string' });
22
+
23
+ /**
24
+ * Schema definition for a serialized flowchart node entity.
25
+ *
26
+ * @see {@linkcode point}
27
+ */
28
+ export const nodeJSON = type(({ optional, use }) => ({
29
+ position: use(point),
30
+ data: { label: optional('string'), content: 'string' },
31
+ input: 'boolean',
32
+ }));
33
+
34
+ /**
35
+ * Schema definition for a serialized flowchart edge entity.
36
+ *
37
+ * @see {@linkcode extremities}
38
+ */
39
+ export const edgeJSON = extremities;
40
+
41
+ /**
42
+ * Schema definition for layout dimensions and connection points of a node.
43
+ *
44
+ * @see {@linkcode point}
45
+ */
46
+ export const dimension = type(({ optional, use }) => ({
47
+ width: 'number',
48
+ height: 'number',
49
+ output: use(point),
50
+ input: optional(use(point)),
51
+ inputOffset: optional(use(point)),
52
+ outputOffset: optional(use(point)),
53
+ }));
54
+
55
+ /** Node layout dimension type inferred from schema {@linkcode dimension}. */
56
+ export type Dimension = inferT<typeof dimension>;
57
+
58
+ /**
59
+ * Schema definition for a 2D line vector representing edge coordinates `(x0, y0)` to
60
+ * `(x1, y1)`.
61
+ */
62
+ export const vector = type({
63
+ x0: 'number',
64
+ y0: 'number',
65
+ x1: 'number',
66
+ y1: 'number',
67
+ });
68
+
69
+ /** 2D vector coordinate type inferred from schema {@linkcode vector}. */
70
+ export type Vector = inferT<typeof vector>;
71
+
72
+ /**
73
+ * Schema definition for an ongoing new connection edge creation preview between
74
+ * source node and cursor position.
75
+ *
76
+ * @see {@linkcode vector}
77
+ */
78
+ export const newEdge = type(({ intersection, use }) =>
79
+ intersection({ from: 'string' }, use(vector)),
80
+ );
81
+
82
+ /** Ongoing new connection edge preview type inferred from schema {@linkcode newEdge}. */
83
+ export type Edge = inferT<typeof newEdge>;
84
+
85
+ /** Schema definition for flowchart board geometry and container scroll dimensions. */
86
+ export const board = type(({ optional }) => ({
87
+ self: { left: 'number', top: 'number', width: 'number', height: 'number' },
88
+
89
+ parent: optional({
90
+ scrollLeft: 'number',
91
+ scrollTop: 'number',
92
+ height: 'number',
93
+ width: 'number',
94
+ }),
95
+ }));
96
+
97
+ /** Flowchart board layout type inferred from schema {@linkcode board}. */
98
+ export type Board = inferT<typeof board>;
package/src/ui/Flow.tsx CHANGED
@@ -11,6 +11,8 @@ import { Provider } from './components/FlowChart.context';
11
11
  * {@linkcode FlowProps}.
12
12
  *
13
13
  * @returns The rendered Solid component.
14
+ *
15
+ * @see {@linkcode Provider}
14
16
  */
15
17
  export const Flow: Component<FlowProps> = props => (
16
18
  <Provider>
@@ -1,8 +1,10 @@
1
+ import { createState } from '@bemedev/app-solidjs';
1
2
  import { useDragDropContext, type Transformer } from '@thisbeyond/solid-dnd';
2
3
  import { type Component } from 'solid-js';
3
4
 
5
+ import { BOUNDS_CONSTRAINTS } from '#services/main.machine.data';
6
+
4
7
  import { useFlow } from './FlowChart.context';
5
- import { BOUNDS_CONSTRAINTS } from './FlowChart.data';
6
8
 
7
9
  /**
8
10
  * Drag boundary transformer component that clamps draggable nodes within container
@@ -10,12 +12,15 @@ import { BOUNDS_CONSTRAINTS } from './FlowChart.data';
10
12
  *
11
13
  * @returns `null` as this component performs side-effect transformer registrations
12
14
  * only.
15
+ *
16
+ * @see {@linkcode useFlow}, {@linkcode BOUNDS_CONSTRAINTS}
13
17
  */
14
18
  export const DragBounds: Component = () => {
15
- const {
16
- board: [ref],
17
- zoom: [zoom],
18
- } = useFlow();
19
+ const service = useFlow();
20
+
21
+ const zoom = createState(service, { selector: ({ context }) => context.zoom });
22
+
23
+ const board = createState(service, { selector: ({ context }) => context.board });
19
24
 
20
25
  const [state, { addTransformer, removeTransformer, onDragStart, onDragEnd }] =
21
26
  useDragDropContext()!;
@@ -24,7 +29,7 @@ export const DragBounds: Component = () => {
24
29
  id: 'clamp-to-container',
25
30
  order: 100,
26
31
  callback: transform => {
27
- const container = ref();
32
+ const container = board()?.self;
28
33
  const activeDraggable = state.active.draggable;
29
34
  if (!container || !activeDraggable) return transform;
30
35
 
@@ -37,7 +42,7 @@ export const DragBounds: Component = () => {
37
42
  const minBoardX = BOUNDS_CONSTRAINTS.horizontal;
38
43
  const maxBoardX = Math.max(
39
44
  minBoardX,
40
- container.clientWidth / currentZoom -
45
+ container.width / currentZoom -
41
46
  node.offsetWidth -
42
47
  BOUNDS_CONSTRAINTS.horizontal,
43
48
  );
@@ -45,7 +50,7 @@ export const DragBounds: Component = () => {
45
50
  const minBoardY = BOUNDS_CONSTRAINTS.vertical;
46
51
  const maxBoardY = Math.max(
47
52
  minBoardY,
48
- container.clientHeight / currentZoom -
53
+ container.height / currentZoom -
49
54
  node.offsetHeight -
50
55
  BOUNDS_CONSTRAINTS.vertical,
51
56
  );