@bemedev/mind-flow 0.0.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 (50) hide show
  1. package/lib/helpers/createContext.d.ts +21 -0
  2. package/lib/helpers/createContext.d.ts.map +1 -0
  3. package/lib/index.cjs.js +1 -0
  4. package/lib/index.d.ts +4 -0
  5. package/lib/index.d.ts.map +1 -0
  6. package/lib/index.es.js +859 -0
  7. package/lib/output.css +2 -0
  8. package/lib/server.cjs.js +1058 -0
  9. package/lib/server.es.js +1056 -0
  10. package/lib/services/main.machine.d.ts +1198 -0
  11. package/lib/services/main.machine.d.ts.map +1 -0
  12. package/lib/services/main.typings.d.ts +68 -0
  13. package/lib/services/main.typings.d.ts.map +1 -0
  14. package/lib/ui/Flow.d.ts +13 -0
  15. package/lib/ui/Flow.d.ts.map +1 -0
  16. package/lib/ui/components/Bounds.d.ts +10 -0
  17. package/lib/ui/components/Bounds.d.ts.map +1 -0
  18. package/lib/ui/components/EdgeComponent.d.ts +20 -0
  19. package/lib/ui/components/EdgeComponent.d.ts.map +1 -0
  20. package/lib/ui/components/EdgesBoard.d.ts +9 -0
  21. package/lib/ui/components/EdgesBoard.d.ts.map +1 -0
  22. package/lib/ui/components/FlowChart.context.d.ts +1228 -0
  23. package/lib/ui/components/FlowChart.context.d.ts.map +1 -0
  24. package/lib/ui/components/FlowChart.d.ts +63 -0
  25. package/lib/ui/components/FlowChart.d.ts.map +1 -0
  26. package/lib/ui/components/FlowChart.data.d.ts +67 -0
  27. package/lib/ui/components/FlowChart.data.d.ts.map +1 -0
  28. package/lib/ui/components/NodeComponent.d.ts +34 -0
  29. package/lib/ui/components/NodeComponent.d.ts.map +1 -0
  30. package/lib/ui/components/NodesBoard.d.ts +9 -0
  31. package/lib/ui/components/NodesBoard.d.ts.map +1 -0
  32. package/lib/ui/components/classes.d.ts +6 -0
  33. package/lib/ui/components/classes.d.ts.map +1 -0
  34. package/package.json +146 -0
  35. package/src/README.md +36 -0
  36. package/src/helpers/createContext.ts +37 -0
  37. package/src/index.ts +3 -0
  38. package/src/input.css +15 -0
  39. package/src/services/main.machine.ts +247 -0
  40. package/src/services/main.typings.ts +50 -0
  41. package/src/ui/Flow.tsx +18 -0
  42. package/src/ui/components/Bounds.tsx +87 -0
  43. package/src/ui/components/EdgeComponent.tsx +106 -0
  44. package/src/ui/components/EdgesBoard.tsx +56 -0
  45. package/src/ui/components/FlowChart.context.ts +373 -0
  46. package/src/ui/components/FlowChart.data.ts +89 -0
  47. package/src/ui/components/FlowChart.tsx +112 -0
  48. package/src/ui/components/NodeComponent.tsx +288 -0
  49. package/src/ui/components/NodesBoard.tsx +298 -0
  50. package/src/ui/components/classes.ts +85 -0
@@ -0,0 +1,247 @@
1
+ import { createMachine } from '@bemedev/app';
2
+ import { type } from '@bemedev/app/bemedev';
3
+ import { nanoid } from 'nanoid';
4
+ import { edgeJSON, extremities, nodeJSON } from './main.typings';
5
+
6
+ /**
7
+ * Constructs a unique edge identifier string from source and destination
8
+ * 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
+ };
29
+
30
+ /**
31
+ * State machine managing flowchart state transitions, nodes, edges,
32
+ * selection, and layout actions.
33
+ */
34
+ export const machine = createMachine(
35
+ {
36
+ initial: 'idle',
37
+ states: {
38
+ idle: {
39
+ on: {
40
+ CONFIGURE: { actions: ['configure'], target: '/working' },
41
+ CONFIGURE_EMPTY: '/working',
42
+ },
43
+ },
44
+
45
+ construction: {
46
+ always: { actions: ['buildUI'], target: '/working' },
47
+ },
48
+
49
+ working: {
50
+ on: {
51
+ MOVE: {
52
+ actions: ['moveNode', 'buildUI'],
53
+ target: '/construction',
54
+ },
55
+ MOVE_IMMEDIATE: {
56
+ actions: [
57
+ {
58
+ name: 'buildImmediateUI',
59
+ description: 'Must be in the ui',
60
+ },
61
+ ],
62
+ },
63
+
64
+ UPDATE_UI: '/construction',
65
+
66
+ ADD_CHILD: {
67
+ actions: [
68
+ 'generateID',
69
+ { name: 'placeChild', description: 'Must be in the ui' },
70
+ 'linkChild',
71
+ ],
72
+ target: '/construction',
73
+ },
74
+
75
+ ADD_PARENT: {
76
+ actions: [
77
+ 'generateID',
78
+ { name: 'placeParent', description: 'Must be in the ui' },
79
+ 'selectParent',
80
+ ],
81
+ target: '/construction',
82
+ },
83
+
84
+ ADD_SIBLING: {
85
+ actions: [
86
+ 'generateID',
87
+ { name: 'placeSibling', description: 'Must be in the ui' },
88
+ 'linkSibling',
89
+ ],
90
+ target: '/construction',
91
+ },
92
+
93
+ ADD_EDGE: { actions: ['addEdge'], target: '/construction' },
94
+
95
+ DELETE: { actions: ['delete'], target: '/construction' },
96
+
97
+ SELECT: { actions: ['select'] },
98
+
99
+ DESELECT: { actions: ['deselect'] },
100
+ },
101
+ },
102
+ },
103
+ },
104
+ {
105
+ eventsMap: type(({ intersection, use, array }) => ({
106
+ CONFIGURE: {
107
+ nodes: array(intersection(use(nodeJSON), { id: 'string' })),
108
+ edges: array(intersection(use(edgeJSON), { id: 'string' })),
109
+ },
110
+
111
+ CONFIGURE_EMPTY: 'never',
112
+
113
+ MOVE: { id: 'string', x: 'number', y: 'number' },
114
+
115
+ MOVE_IMMEDIATE: { id: 'string', x: 'number', y: 'number' },
116
+
117
+ ADD_CHILD: 'string',
118
+ ADD_PARENT: 'never',
119
+ ADD_SIBLING: 'string',
120
+ DELETE: 'string',
121
+ SELECT: 'string',
122
+ DESELECT: 'never',
123
+ ADD_EDGE: use(extremities),
124
+ })),
125
+
126
+ pContext: type(({ union }) => ({
127
+ generatedId: union('string', 'null'),
128
+ })),
129
+
130
+ context: type(({ optional, use, array }) => ({
131
+ data: optional({
132
+ nodes: array({ ...use(nodeJSON), id: 'string' }),
133
+ edges: array({ ...use(edgeJSON), id: 'string' }),
134
+ }),
135
+
136
+ selected: optional('string'),
137
+ updatingUI: optional('boolean'),
138
+ })),
139
+
140
+ sync: true,
141
+ },
142
+ ).provideOptions(({ assign, batch, erase }) => ({
143
+ actions: {
144
+ configure: batch(
145
+ assign('context.data', () => ({ nodes: [], edges: [] })),
146
+
147
+ assign('context.data.nodes', {
148
+ CONFIGURE: ({ payload: { nodes } }) => nodes,
149
+ }),
150
+
151
+ assign('context.data.edges', {
152
+ CONFIGURE: ({ payload: { edges } }) => edges,
153
+ }),
154
+
155
+ assign('context.updatingUI', () => false),
156
+ assign('pContext.generatedId', () => null),
157
+ ),
158
+
159
+ generateID: assign('pContext.generatedId', () => nanoid()),
160
+
161
+ linkChild: batch(
162
+ assign('context.data.edges', {
163
+ ADD_CHILD: ({ context, pContext, payload }) => {
164
+ const data = context.data;
165
+ const from = payload;
166
+ const generatedId = pContext?.generatedId;
167
+ const to = buildNodeID(generatedId);
168
+ const id = buildEdgeId(from, to);
169
+ return [...(data?.edges ?? []), { id, from, to }];
170
+ },
171
+ }),
172
+
173
+ assign('context.selected', ({ pContext: { generatedId } }) =>
174
+ buildNodeID(generatedId),
175
+ ),
176
+ ),
177
+
178
+ linkSibling: batch(
179
+ assign('context.data.edges', {
180
+ ADD_SIBLING: ({ pContext, payload, context }) => {
181
+ const edges = context.data?.edges;
182
+ const generatedId = pContext?.generatedId;
183
+ const out = [...(edges ?? [])];
184
+ const from = edges?.find(({ to }) => to === payload)?.from;
185
+ if (!from) return out;
186
+ const to = buildNodeID(generatedId);
187
+ const id = buildEdgeId(from, to);
188
+ out.push({ from, to, id });
189
+
190
+ return out;
191
+ },
192
+ }),
193
+
194
+ assign('context.selected', ({ pContext: { generatedId } }) =>
195
+ buildNodeID(generatedId),
196
+ ),
197
+ ),
198
+
199
+ selectParent: assign(
200
+ 'context.selected',
201
+ ({ pContext: { generatedId } }) => buildNodeID(generatedId),
202
+ ),
203
+
204
+ moveNode: assign('context.data.nodes', {
205
+ MOVE: ({ context: { data }, payload }) => {
206
+ const { id, x, y } = payload;
207
+ if (!id) return data?.nodes ?? [];
208
+
209
+ return (
210
+ data?.nodes?.map(d => {
211
+ if (d.id === id) {
212
+ return { ...d, position: { x, y } };
213
+ }
214
+ return d;
215
+ }) ?? []
216
+ );
217
+ },
218
+ }),
219
+
220
+ select: assign('context.selected', {
221
+ SELECT: ({ payload }) => payload,
222
+ }),
223
+
224
+ delete: assign(['context.data.nodes', 'context.data.edges'], {
225
+ DELETE: ({ context: { data }, payload }) => {
226
+ const nodes = data?.nodes?.filter(({ id }) => id !== payload);
227
+ const edges = data?.edges?.filter(
228
+ ({ id, from, to }) =>
229
+ id !== payload && from !== payload && to !== payload,
230
+ );
231
+ return [nodes, edges];
232
+ },
233
+ }),
234
+
235
+ addEdge: assign('context.data.edges', {
236
+ ADD_EDGE: ({ context, payload: { from, to } }) => {
237
+ const edges = context.data?.edges ?? [];
238
+ const id = buildEdgeId(from, to);
239
+ if (edges.some(e => e.id === id)) return edges;
240
+ const out = [...edges, { id, from, to }];
241
+ return out;
242
+ },
243
+ }),
244
+
245
+ deselect: erase('context.selected'),
246
+ },
247
+ }));
@@ -0,0 +1,50 @@
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
+ /** Schema definition for node handle offsets (input and output). */
11
+ export const nodeOffset = type(({ optional, use }) => ({
12
+ input: optional(use(point)),
13
+ output: use(point),
14
+ }));
15
+
16
+ /** Schema definition for edge extremities */
17
+ export const extremities = type({ from: 'string', to: 'string' });
18
+
19
+ /** Schema definition for a serialized flowchart node entity. */
20
+ export const nodeJSON = type(({ optional, use }) => ({
21
+ position: use(point),
22
+ data: { label: optional('string'), content: 'string' },
23
+ input: 'boolean',
24
+ }));
25
+
26
+ /** Schema definition for a serialized flowchart edge entity. */
27
+ export const edgeJSON = extremities;
28
+
29
+ /** Schema definition for layout dimensions and connection points of a node. */
30
+ export const dimensions = type(({ optional, use }) => ({
31
+ width: 'number',
32
+ height: 'number',
33
+ id: 'string',
34
+ output: use(point),
35
+ input: optional(use(point)),
36
+ }));
37
+
38
+ /**
39
+ * Schema definition for a 2D line vector representing edge coordinates
40
+ * `(x0, y0)` to `(x1, y1)`.
41
+ */
42
+ export const vector = type({
43
+ x0: 'number',
44
+ y0: 'number',
45
+ x1: 'number',
46
+ y1: 'number',
47
+ });
48
+
49
+ /** 2D vector coordinate type inferred from schema {@linkcode vector}. */
50
+ export type Vector = inferT<typeof vector>;
@@ -0,0 +1,18 @@
1
+ import type { Component } from 'solid-js';
2
+ import { FlowChart, type FlowProps } from './components/FlowChart';
3
+ import { Provider } from './components/FlowChart.context';
4
+
5
+ /**
6
+ * Root Flow component wrapping the {@linkcode FlowChart} inside the flow
7
+ * context provider.
8
+ *
9
+ * @param props - Flow chart configuration and callbacks of type
10
+ * {@linkcode FlowProps}.
11
+ *
12
+ * @returns The rendered Solid component.
13
+ */
14
+ export const Flow: Component<FlowProps> = props => (
15
+ <Provider>
16
+ <FlowChart {...props} />
17
+ </Provider>
18
+ );
@@ -0,0 +1,87 @@
1
+ import {
2
+ useDragDropContext,
3
+ type Transformer,
4
+ } from '@thisbeyond/solid-dnd';
5
+ import { type Component } from 'solid-js';
6
+ import { BOUNDS_CONSTRAINTS } from './FlowChart.data';
7
+ import { useFlow } from './FlowChart.context';
8
+
9
+ /**
10
+ * Drag boundary transformer component that clamps draggable nodes within
11
+ * container scroll bounds.
12
+ *
13
+ * @returns `null` as this component performs side-effect transformer
14
+ * registrations only.
15
+ */
16
+ export const DragBounds: Component = () => {
17
+ const {
18
+ board: [ref],
19
+ zoom: [zoom],
20
+ } = useFlow();
21
+
22
+ const [
23
+ state,
24
+ { addTransformer, removeTransformer, onDragStart, onDragEnd },
25
+ ] = useDragDropContext()!;
26
+
27
+ const transformer: Transformer = {
28
+ id: 'clamp-to-container',
29
+ order: 100,
30
+ callback: transform => {
31
+ const container = ref()?.parentElement;
32
+ const activeDraggable = state.active.draggable;
33
+ if (!container || !activeDraggable) return transform;
34
+ const draggableLayout = activeDraggable.layout;
35
+
36
+ // #region Inner visible boundaries (excluding borders and scrollbars)
37
+ const containerRect = container.getBoundingClientRect();
38
+ const innerLeft = containerRect.left + container.clientLeft;
39
+ const innerTop = containerRect.top + container.clientTop;
40
+ const innerRight = innerLeft + container.clientWidth;
41
+ const innerBottom = innerTop + container.clientHeight;
42
+ // #endregion
43
+
44
+ // #region Convert boundaries to board coordinate space
45
+ const currentZoom = zoom();
46
+ const minX =
47
+ innerLeft -
48
+ draggableLayout.left +
49
+ BOUNDS_CONSTRAINTS.horizontal * currentZoom;
50
+
51
+ const maxX = Math.max(
52
+ minX,
53
+ innerRight -
54
+ draggableLayout.right -
55
+ BOUNDS_CONSTRAINTS.horizontal * currentZoom,
56
+ );
57
+
58
+ const minY =
59
+ innerTop -
60
+ draggableLayout.top +
61
+ BOUNDS_CONSTRAINTS.vertical * currentZoom;
62
+
63
+ const maxY = Math.max(
64
+ minY,
65
+ innerBottom -
66
+ draggableLayout.bottom -
67
+ BOUNDS_CONSTRAINTS.vertical * currentZoom,
68
+ );
69
+ // #endregion
70
+
71
+ return {
72
+ x: Math.min(Math.max(transform.x, minX), maxX),
73
+ y: Math.min(Math.max(transform.y, minY), maxY),
74
+ };
75
+ },
76
+ };
77
+
78
+ onDragStart(({ draggable }) => {
79
+ addTransformer('draggables', draggable.id, transformer);
80
+ });
81
+
82
+ onDragEnd(({ draggable }) => {
83
+ removeTransformer('draggables', draggable.id, transformer.id);
84
+ });
85
+
86
+ return null;
87
+ };
@@ -0,0 +1,106 @@
1
+ import { useState } from '@bemedev/app-solidjs';
2
+ import { Component, createEffect, createSignal, Show } from 'solid-js';
3
+ import type { Vector } from '../../services/main.typings';
4
+ import { useFlow } from './FlowChart.context';
5
+
6
+ /** Properties for rendering an SVG connection edge between two points. */
7
+ type Props = {
8
+ /** Unique identifier of the edge. */
9
+ id: string;
10
+ /** Whether this is a temporary edge currently being dragged. */
11
+ isNew?: boolean;
12
+ } & Vector;
13
+
14
+ /**
15
+ * Computes bezier curve control point offset based on horizontal distance.
16
+ *
17
+ * @param value - Absolute horizontal delta between start and end points.
18
+ *
19
+ * @returns Offset distance in pixels for bezier curvature.
20
+ */
21
+ const calculateOffset = (value: number) => (value * 100) / 200;
22
+
23
+ /**
24
+ * Generates an SVG cubic bezier path `d` string between coordinates `(x0,
25
+ * y0)` and `(x1, y1)`.
26
+ *
27
+ * @param vector - Vector coordinates of type {@linkcode Vector}.
28
+ *
29
+ * @returns SVG cubic bezier path string.
30
+ */
31
+ const draw = ({ x0, y0, x1, y1 }: Vector) => {
32
+ return `M ${x0} ${y0} C ${x0 + calculateOffset(Math.abs(x1 - x0))} ${y0}, ${x1 - calculateOffset(Math.abs(x1 - x0))} ${y1}, ${x1} ${y1}`;
33
+ };
34
+
35
+ /**
36
+ * SVG edge component rendering bezier curves and delete interaction
37
+ * handles for node connections.
38
+ *
39
+ * @param props - Edge properties of type {@linkcode Props}.
40
+ *
41
+ * @returns The rendered SVG JSX elements.
42
+ */
43
+ export const EdgeComponent: Component<Props> = props => {
44
+ const [middlePoint, setMiddlePoint] = createSignal({
45
+ x: props.x0 + (props.x1 - props.x0) / 2,
46
+ y: props.y0 + (props.y1 - props.y0) / 2,
47
+ });
48
+
49
+ const { service } = useFlow();
50
+
51
+ createEffect(() => {
52
+ const middleX = props.x0 + (props.x1 - props.x0) / 2;
53
+ const middleY = props.y0 + (props.y1 - props.y0) / 2;
54
+ setMiddlePoint({ x: middleX, y: middleY });
55
+ });
56
+
57
+ const selected = useState(service, {
58
+ selector: s => s.context.selected === props.id,
59
+ });
60
+
61
+ return (
62
+ <>
63
+ <path
64
+ class='relative cursor-pointer fill-transparent'
65
+ classList={{
66
+ 'stroke-[rgba(168,168,168,0.4)] stroke-3 z-200': !!props.isNew,
67
+ 'stroke-[rgba(168,168,168,1)] stroke-4 z-100':
68
+ selected() && !props.isNew,
69
+ 'stroke-[rgba(168,168,168,0.8)] stroke-3':
70
+ !selected() && !props.isNew,
71
+ }}
72
+ style={{ 'pointer-events': props.isNew ? 'none' : 'all' }}
73
+ d={draw(props)}
74
+ onMouseDown={e => e.stopPropagation()}
75
+ onClick={() => service.send({ type: 'SELECT', payload: props.id })}
76
+ />
77
+ <Show when={selected()}>
78
+ <g
79
+ cursor='pointer'
80
+ transform={`translate(${middlePoint().x}, ${middlePoint().y})`}
81
+ onMouseDown={e => {
82
+ e.stopPropagation();
83
+ service.send({ type: 'DELETE', payload: props.id });
84
+ }}
85
+ style={{ 'pointer-events': 'all', 'z-index': '30' }}
86
+ >
87
+ <circle cx='0' cy='0' r='12' fill='rgba(168, 168, 168, 1)' />
88
+ <svg
89
+ fill='currentColor'
90
+ stroke-width='0'
91
+ xmlns='http://www.w3.org/2000/svg'
92
+ class='h-25 w-25 bg-white fill-white'
93
+ width='20'
94
+ height='20'
95
+ viewBox='0 0 20 20'
96
+ color='white'
97
+ x='-10'
98
+ y='-10'
99
+ >
100
+ <path d='M10.185,1.417c-4.741,0-8.583,3.842-8.583,8.583c0,4.74,3.842,8.582,8.583,8.582S18.768,14.74,18.768,10C18.768,5.259,14.926,1.417,10.185,1.417 M10.185,17.68c-4.235,0-7.679-3.445-7.679-7.68c0-4.235,3.444-7.679,7.679-7.679S17.864,5.765,17.864,10C17.864,14.234,14.42,17.68,10.185,17.68 M10.824,10l2.842-2.844c0.178-0.176,0.178-0.46,0-0.637c-0.177-0.178-0.461-0.178-0.637,0l-2.844,2.841L7.341,6.52c-0.176-0.178-0.46-0.178-0.637,0c-0.178,0.176-0.178,0.461,0,0.637L9.546,10l-2.841,2.844c-0.178,0.176-0.178,0.461,0,0.637c0.178,0.178,0.459,0.178,0.637,0l2.844-2.841l2.844,2.841c0.178,0.178,0.459,0.178,0.637,0c0.178-0.176,0.178-0.461,0-0.637L10.824,10z'></path>
101
+ </svg>
102
+ </g>
103
+ </Show>
104
+ </>
105
+ );
106
+ };
@@ -0,0 +1,56 @@
1
+ import {
2
+ Component,
3
+ createEffect,
4
+ createMemo,
5
+ createSignal,
6
+ For,
7
+ Show,
8
+ } from 'solid-js';
9
+ import { EdgeComponent } from './EdgeComponent';
10
+ import { useFlow } from './FlowChart.context';
11
+
12
+ /**
13
+ * SVG board overlay component that renders all active connecting edges and
14
+ * ongoing edge creation previews.
15
+ *
16
+ * @returns The rendered SVG JSX element.
17
+ */
18
+ export const EdgesBoard: Component = () => {
19
+ const [selected, setSelected] = createSignal<string>();
20
+
21
+ const {
22
+ newEdge: [newEdge],
23
+ edgesPositions: [edgesPositions],
24
+ } = useFlow();
25
+
26
+ const datas = createMemo(() => {
27
+ const entries = Object.entries(edgesPositions());
28
+ return entries.map(([id, vector]) => ({ id, ...vector }));
29
+ });
30
+
31
+ createEffect(() => {
32
+ if (selected() && newEdge()) setSelected();
33
+ });
34
+
35
+ return (
36
+ <svg
37
+ class='pointer-events-none h-full w-full overflow-visible'
38
+ // style={{ scale: zoom() }}
39
+ >
40
+ <Show when={newEdge()}>
41
+ {value => (
42
+ <EdgeComponent
43
+ id='__#new-edge#__TEMP'
44
+ isNew
45
+ x0={value().x0}
46
+ y0={value().y0}
47
+ x1={value().x1}
48
+ y1={value().y1}
49
+ />
50
+ )}
51
+ </Show>
52
+
53
+ <For each={datas()} children={EdgeComponent} />
54
+ </svg>
55
+ );
56
+ };