@bemedev/mind-flow 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +95 -0
  2. package/lib/helpers/createContext.d.ts +8 -11
  3. package/lib/helpers/createContext.d.ts.map +1 -1
  4. package/lib/index.cjs.js +1 -1
  5. package/lib/index.es.js +240 -235
  6. package/lib/server.cjs.js +181 -121
  7. package/lib/server.es.js +181 -121
  8. package/lib/services/main.machine.d.ts +7 -9
  9. package/lib/services/main.machine.d.ts.map +1 -1
  10. package/lib/services/main.typings.d.ts +3 -3
  11. package/lib/services/main.typings.d.ts.map +1 -1
  12. package/lib/ui/Flow.d.ts +2 -2
  13. package/lib/ui/Flow.d.ts.map +1 -1
  14. package/lib/ui/components/Bounds.d.ts +4 -4
  15. package/lib/ui/components/Bounds.d.ts.map +1 -1
  16. package/lib/ui/components/EdgeComponent.d.ts +2 -2
  17. package/lib/ui/components/EdgeComponent.d.ts.map +1 -1
  18. package/lib/ui/components/EdgesBoard.d.ts +2 -2
  19. package/lib/ui/components/EdgesBoard.d.ts.map +1 -1
  20. package/lib/ui/components/FlowChart.context.d.ts +10 -18
  21. package/lib/ui/components/FlowChart.context.d.ts.map +1 -1
  22. package/lib/ui/components/FlowChart.d.ts +4 -10
  23. package/lib/ui/components/FlowChart.d.ts.map +1 -1
  24. package/lib/ui/components/FlowChart.data.d.ts +10 -11
  25. package/lib/ui/components/FlowChart.data.d.ts.map +1 -1
  26. package/lib/ui/components/NodeComponent.d.ts +2 -2
  27. package/lib/ui/components/NodeComponent.d.ts.map +1 -1
  28. package/lib/ui/components/NodesBoard.d.ts +2 -2
  29. package/lib/ui/components/NodesBoard.d.ts.map +1 -1
  30. package/lib/ui/components/classes.d.ts +2 -2
  31. package/package.json +3 -9
  32. package/src/README.md +2 -2
  33. package/src/helpers/createContext.ts +8 -11
  34. package/src/services/main.machine.ts +18 -46
  35. package/src/services/main.typings.ts +3 -3
  36. package/src/ui/Flow.tsx +3 -2
  37. package/src/ui/components/Bounds.tsx +35 -44
  38. package/src/ui/components/EdgeComponent.tsx +7 -8
  39. package/src/ui/components/EdgesBoard.tsx +3 -2
  40. package/src/ui/components/FlowChart.context.ts +75 -82
  41. package/src/ui/components/FlowChart.data.ts +12 -15
  42. package/src/ui/components/FlowChart.tsx +6 -11
  43. package/src/ui/components/NodeComponent.tsx +13 -25
  44. package/src/ui/components/NodesBoard.tsx +120 -111
  45. package/src/ui/components/classes.ts +2 -2
@@ -5,35 +5,26 @@ import {
5
5
  DragOverlay,
6
6
  } from '@thisbeyond/solid-dnd';
7
7
  import { dequal } from 'dequal';
8
- import {
9
- Component,
10
- createEffect,
11
- createSignal,
12
- For,
13
- on,
14
- onCleanup,
15
- Show,
16
- } from 'solid-js';
8
+ import { Component, createEffect, createSignal, For, on, Show } from 'solid-js';
9
+
17
10
  import { DragBounds } from './Bounds';
18
11
  import { EdgesBoard } from './EdgesBoard';
19
12
  import { useFlow } from './FlowChart.context';
13
+ import { CANVAS_FACTOR, SCROLL_MULTIPLIER } from './FlowChart.data';
20
14
  import { NodeComponent } from './NodeComponent';
21
- import { CANVAS_FACTOR } from './FlowChart.data';
22
15
 
23
16
  /**
24
- * Interactive board component containing the drag-drop viewport, zoom
25
- * controls, panning gestures, and rendered nodes/edges.
17
+ * Interactive board component containing the drag-drop viewport, zoom controls,
18
+ * panning gestures, and rendered nodes/edges.
26
19
  *
27
20
  * @returns The rendered Solid component.
28
21
  */
29
22
  export const NodesBoard: Component = () => {
30
- let containerRef: HTMLDivElement | undefined;
23
+ let containerRef: HTMLDivElement;
31
24
  const [isPanning, setIsPanning] = createSignal(false);
32
25
  const [transform, setTransform] = createSignal({ x: 0, y: 0 });
33
26
  const [id, setId] = createSignal<string | number>('');
34
27
  const [previousZoom, setPreviousZoom] = createSignal<number>();
35
- let cleanupPanning = () => {};
36
- onCleanup(cleanupPanning);
37
28
  let percentX = 0;
38
29
  let percentY = 0;
39
30
 
@@ -44,11 +35,13 @@ export const NodesBoard: Component = () => {
44
35
  zoom: [zoom, setZoom],
45
36
  } = useFlow();
46
37
 
38
+ /**
39
+ * Calculates and saves the normalized scroll percentages across X and Y axes for
40
+ * preserving viewport alignment on zoom changes.
41
+ */
47
42
  const updateScrollPercentages = () => {
48
- if (!containerRef) return;
49
43
  const maxScrollX = containerRef.scrollWidth - containerRef.clientWidth;
50
- const maxScrollY =
51
- containerRef.scrollHeight - containerRef.clientHeight;
44
+ const maxScrollY = containerRef.scrollHeight - containerRef.clientHeight;
52
45
  percentX = maxScrollX > 0 ? containerRef.scrollLeft / maxScrollX : 0;
53
46
  percentY = maxScrollY > 0 ? containerRef.scrollTop / maxScrollY : 0;
54
47
  };
@@ -57,23 +50,24 @@ export const NodesBoard: Component = () => {
57
50
  on(
58
51
  zoom,
59
52
  () => {
60
- if (!containerRef) return;
61
- const maxScrollX =
62
- containerRef.scrollWidth - containerRef.clientWidth;
63
- const maxScrollY =
64
- containerRef.scrollHeight - containerRef.clientHeight;
65
- if (maxScrollX > 0)
66
- containerRef.scrollLeft = percentX * maxScrollX;
53
+ const maxScrollX = containerRef.scrollWidth - containerRef.clientWidth;
54
+ const maxScrollY = containerRef.scrollHeight - containerRef.clientHeight;
55
+ if (maxScrollX > 0) containerRef.scrollLeft = percentX * maxScrollX;
67
56
  if (maxScrollY > 0) containerRef.scrollTop = percentY * maxScrollY;
68
57
  },
69
58
  { defer: true },
70
59
  ),
71
60
  );
72
61
 
73
- const selectedId = useState(service, {
74
- selector: s => s.context?.selected,
75
- });
62
+ const selectedId = useState(service, { selector: s => s.context?.selected });
76
63
 
64
+ /**
65
+ * Determines whether the given element ID matches the currently selected entity.
66
+ *
67
+ * @param id - The node or edge identifier to check.
68
+ *
69
+ * @returns `true` if the item is currently selected, otherwise `false`.
70
+ */
77
71
  const selected = (id: string | number) => selectedId() === id;
78
72
 
79
73
  const nodes = useState(service, {
@@ -91,76 +85,93 @@ export const NodesBoard: Component = () => {
91
85
  equals: dequal,
92
86
  });
93
87
 
94
- const cDim = () => {
95
- const _zoom = zoom();
96
- if (_zoom < 1) {
97
- return CANVAS_FACTOR * 100 * _zoom;
98
- }
99
- return CANVAS_FACTOR * 100;
100
- };
88
+ const CANVAS_SIZE = CANVAS_FACTOR * 100;
89
+ const MARGIN_X = 53 * CANVAS_FACTOR;
90
+ const MARGIN_Y = 85 * CANVAS_FACTOR;
91
+ const cWidth = () => `calc((${CANVAS_SIZE}vw - ${MARGIN_X}px) * ${zoom()})`;
92
+ const cHeight = () => `calc((${CANVAS_SIZE}vh - ${MARGIN_Y}px) * ${zoom()})`;
101
93
 
102
94
  return (
103
- <DragDropProvider
104
- onDragMove={({
105
- draggable: { transform: _transform, node, id },
106
- overlay,
107
- }) => {
108
- setId(id);
109
- if (selected(id)) {
110
- const x = node.offsetLeft + _transform.x / zoom();
111
- const y = node.offsetTop + _transform.y / zoom();
112
- overlay?.node.style.setProperty('top', y * 2 + 'px');
113
- overlay?.node.style.setProperty('left', x * 2 + 'px');
114
- const deltaX = _transform.x / zoom();
115
- const deltaY = _transform.y / zoom();
116
- // Directly update the draggable node's CSS transform adjusted for zoom:
117
- node.style.setProperty(
118
- 'transform',
119
- `translate3d(${deltaX}px, ${deltaY}px, 0)`,
95
+ <div
96
+ onWheel={e => {
97
+ if (e.ctrlKey || e.metaKey) {
98
+ e.preventDefault();
99
+ updateScrollPercentages();
100
+ const delta = e.deltaY < 0 ? 0.1 : -0.1;
101
+ setZoom(prev =>
102
+ Math.min(Math.max(Number((prev + delta).toFixed(2)), 0.2), 3),
120
103
  );
121
-
122
- service.send({
123
- type: 'MOVE_IMMEDIATE',
124
- payload: { id: `${id}`, x, y },
125
- });
126
- setTransform({ ..._transform });
127
104
  }
128
105
  }}
129
106
 
130
- onDragEnd={({ draggable: { node, id } }) => {
131
- if (!selected(id)) return;
107
+ class='relative mx-auto h-[calc(100vh-64px)] w-[calc(100vw-32px)] overflow-hidden'
108
+ >
109
+ <DragDropProvider
110
+ onDragMove={({ draggable: { transform: _transform, node, id } }) => {
111
+ setId(id);
112
+ if (selected(id)) {
113
+ const grandParent = node.parentElement?.parentElement;
114
+ const currentZoom = zoom();
115
+ const minX = 0;
116
+ const maxX = grandParent
117
+ ? Math.max(0, grandParent.clientWidth / currentZoom - node.offsetWidth)
118
+ : Infinity;
119
+ const minY = 0;
120
+ const maxY = grandParent
121
+ ? Math.max(
122
+ 0,
123
+ grandParent.clientHeight / currentZoom - node.offsetHeight,
124
+ )
125
+ : Infinity;
132
126
 
133
- const X = node.offsetLeft + transform().x / zoom();
134
- const Y = node.offsetTop + transform().y / zoom();
135
- node.style.setProperty('top', Y + 'px');
136
- node.style.setProperty('left', X + 'px');
137
- node.style.removeProperty('transform');
127
+ const targetX = node.offsetLeft + _transform.x / currentZoom;
128
+ const targetY = node.offsetTop + _transform.y / currentZoom;
138
129
 
139
- setTimeout(() => {
140
- service.send({
141
- type: 'MOVE',
142
- payload: { id: `${id}`, x: X, y: Y },
143
- });
144
- setTransform({ x: 0, y: 0 });
145
- }, 0);
146
- }}
147
- >
148
- <div
149
- onWheel={e => {
150
- if (e.ctrlKey || e.metaKey) {
151
- e.preventDefault();
152
- updateScrollPercentages();
153
- const delta = e.deltaY < 0 ? 0.1 : -0.1;
154
- setZoom(prev =>
155
- Math.min(
156
- Math.max(Number((prev + delta).toFixed(2)), 0.2),
157
- 3,
158
- ),
130
+ const x = Math.min(Math.max(targetX, minX), maxX);
131
+ const y = Math.min(Math.max(targetY, minY), maxY);
132
+
133
+ const deltaX = x - node.offsetLeft;
134
+ const deltaY = y - node.offsetTop;
135
+
136
+ // Directly update the draggable node's CSS transform adjusted for zoom:
137
+ node.style.setProperty(
138
+ 'transform',
139
+ `translate3d(${deltaX}px, ${deltaY}px, 0)`,
159
140
  );
141
+
142
+ service.send({ type: 'MOVE_IMMEDIATE', payload: { id: `${id}`, x, y } });
143
+ setTransform({ x: deltaX * currentZoom, y: deltaY * currentZoom });
160
144
  }
161
145
  }}
162
146
 
163
- class='relative mx-auto h-[calc(100vh-64px)] w-[calc(100vw-32px)]'
147
+ onDragEnd={({ draggable: { node, id } }) => {
148
+ if (!selected(id)) return;
149
+
150
+ const grandParent = node.parentElement?.parentElement;
151
+ const currentZoom = zoom();
152
+ const minX = 0;
153
+ const maxX = grandParent
154
+ ? Math.max(0, grandParent.clientWidth / currentZoom - node.offsetWidth)
155
+ : Infinity;
156
+ const minY = 0;
157
+ const maxY = grandParent
158
+ ? Math.max(0, grandParent.clientHeight / currentZoom - node.offsetHeight)
159
+ : Infinity;
160
+
161
+ const rawX = node.offsetLeft + transform().x / currentZoom;
162
+ const rawY = node.offsetTop + transform().y / currentZoom;
163
+ const X = Math.min(Math.max(rawX, minX), maxX);
164
+ const Y = Math.min(Math.max(rawY, minY), maxY);
165
+
166
+ node.style.setProperty('top', Y + 'px');
167
+ node.style.setProperty('left', X + 'px');
168
+ node.style.removeProperty('transform');
169
+
170
+ setTimeout(() => {
171
+ service.send({ type: 'MOVE', payload: { id: `${id}`, x: X, y: Y } });
172
+ setTransform({ x: 0, y: 0 });
173
+ }, 0);
174
+ }}
164
175
  >
165
176
  <div
166
177
  ref={el => {
@@ -170,21 +181,14 @@ export const NodesBoard: Component = () => {
170
181
  class='relative h-full w-full overflow-scroll rounded-lg border-2 border-gray-600'
171
182
  >
172
183
  <DragDropSensors />
173
- <DragBounds />
174
184
  <div
175
185
  ref={setRef}
176
- class='relative cursor-crosshair'
186
+ class='relative cursor-crosshair overflow-hidden'
177
187
  classList={{ 'cursor-grabbing': isPanning() }}
178
- style={{
179
- height: `calc(${cDim()}vh - 85px)`,
180
- width: `calc(${cDim()}vw - 53px)`,
181
- scale: zoom(),
182
- 'transform-origin': 'top left',
183
- }}
188
+ style={{ height: cHeight(), width: cWidth() }}
189
+
184
190
  onMouseDown={e => {
185
191
  if (newEdge() || e.button !== 0) return;
186
- if (!containerRef) return;
187
-
188
192
  service.send('DESELECT');
189
193
 
190
194
  // #region Props
@@ -197,11 +201,10 @@ export const NodesBoard: Component = () => {
197
201
  setIsPanning(true);
198
202
 
199
203
  const handleMouseMove = (moveEvent: MouseEvent) => {
200
- if (!containerRef) return;
201
204
  const dx = moveEvent.clientX - startX;
202
205
  const dy = moveEvent.clientY - startY;
203
- containerRef.scrollLeft = startScrollLeft - dx * 3;
204
- containerRef.scrollTop = startScrollTop - dy * 3;
206
+ containerRef.scrollLeft = startScrollLeft - dx * SCROLL_MULTIPLIER;
207
+ containerRef.scrollTop = startScrollTop - dy * SCROLL_MULTIPLIER;
205
208
  updateScrollPercentages();
206
209
  };
207
210
 
@@ -209,18 +212,22 @@ export const NodesBoard: Component = () => {
209
212
  window.removeEventListener('mousemove', handleMouseMove);
210
213
  window.removeEventListener('mouseup', handleMouseUp);
211
214
  setTimeout(() => setIsPanning(false), 200);
212
- (cleanupPanning as any) = undefined;
213
215
  };
214
216
 
215
217
  // #region Attach Windows listeners
216
- cleanupPanning = handleMouseUp;
217
218
  window.addEventListener('mousemove', handleMouseMove);
218
219
  window.addEventListener('mouseup', handleMouseUp);
219
220
  // #endregion
220
221
  }}
221
222
  >
222
- <EdgesBoard />
223
- <For each={nodes()} children={NodeComponent} />
223
+ <div
224
+ class='relative h-full w-full'
225
+ style={{ scale: zoom(), 'transform-origin': 'top left' }}
226
+ >
227
+ <DragBounds />
228
+ <EdgesBoard />
229
+ <For each={nodes()} children={NodeComponent} />
230
+ </div>
224
231
  </div>
225
232
  </div>
226
233
 
@@ -233,7 +240,7 @@ export const NodesBoard: Component = () => {
233
240
  updateScrollPercentages();
234
241
  setPreviousZoom(undefined);
235
242
  setZoom(prev =>
236
- Math.max(0.2, Number((prev - 0.1).toFixed(2))),
243
+ Math.max(1 / CANVAS_FACTOR, Number((prev - 0.1).toFixed(2))),
237
244
  );
238
245
  }}
239
246
  title='Zoom out'
@@ -266,7 +273,10 @@ export const NodesBoard: Component = () => {
266
273
  updateScrollPercentages();
267
274
  setPreviousZoom(undefined);
268
275
  setZoom(prev =>
269
- Math.min(3, Number((prev + 0.1).toFixed(2))),
276
+ Math.min(
277
+ Math.min(3, CANVAS_FACTOR),
278
+ Number((prev + 0.1).toFixed(2)),
279
+ ),
270
280
  );
271
281
  }}
272
282
  title='Zoom in'
@@ -288,11 +298,10 @@ export const NodesBoard: Component = () => {
288
298
  </svg>
289
299
  </button>
290
300
  </div>
291
- </div>
292
-
293
- <Show when={!id() || !selected(id())}>
294
- <DragOverlay children='' />
295
- </Show>
296
- </DragDropProvider>
301
+ <Show when={!id() || !selected(id())}>
302
+ <DragOverlay children='' />
303
+ </Show>
304
+ </DragDropProvider>
305
+ </div>
297
306
  );
298
307
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
- * List of Tailwind CSS class names used across flowchart UI components to
3
- * ensure compilation safelist.
2
+ * List of Tailwind CSS class names used across flowchart UI components to ensure
3
+ * compilation safelist.
4
4
  */
5
5
  export const CLASSES = [
6
6
  'fill-transparent',