@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,288 @@
1
+ /* eslint-disable @typescript-eslint/no-namespace */
2
+ import { useState } from '@bemedev/app-solidjs';
3
+ import { createDraggable } from '@thisbeyond/solid-dnd';
4
+ import { Component, createEffect, createSignal, Show } from 'solid-js';
5
+ import { produce } from 'solid-js/store';
6
+ import { useFlow } from './FlowChart.context';
7
+ import {
8
+ DEFAULT_INPUT_OFFSET_X,
9
+ DEFAULT_INPUT_OFFSET_Y,
10
+ HANDLE_CONTAINER_OFFSET_X,
11
+ HANDLE_MARGIN_TOP,
12
+ HANDLE_SIZE,
13
+ } from './FlowChart.data';
14
+
15
+ declare module 'solid-js' {
16
+ namespace JSX {
17
+ interface Directives {
18
+ draggable: any;
19
+ }
20
+ }
21
+ }
22
+
23
+ /** Properties for rendering an individual flowchart node component. */
24
+ type Props = {
25
+ /** Unique identifier of the node. */
26
+ id: string;
27
+ /** Horizontal position of the node in board coordinates. */
28
+ x: number;
29
+ /** Vertical position of the node in board coordinates. */
30
+ y: number;
31
+ /** Optional header label for the node. */
32
+ label?: string;
33
+ /** Body content text of the node. */
34
+ content: string;
35
+ /** Whether the node has an input handle. */
36
+ input: boolean;
37
+ };
38
+
39
+ /**
40
+ * Interactive flowchart node component supporting dragging, selection,
41
+ * handle connections, and child/sibling creation.
42
+ *
43
+ * @param props - Node rendering properties of type {@linkcode Props}.
44
+ *
45
+ * @returns The rendered Solid component.
46
+ */
47
+ export const NodeComponent: Component<Props> = props => {
48
+ let inputRef: HTMLDivElement | undefined;
49
+ let outputRef: HTMLDivElement | undefined;
50
+ const [ref, setRef] = createSignal<HTMLDivElement>();
51
+ const {
52
+ dimensions: [dimensions, setDimensions],
53
+ newEdge: [newEdge, setNewEdge],
54
+ getBoardPoint,
55
+ service,
56
+ zoom: [zoom],
57
+ } = useFlow();
58
+
59
+ const selected = useState(service, {
60
+ selector: s => s.context.selected === props.id,
61
+ });
62
+
63
+ createEffect(() => {
64
+ const _inputRef = inputRef;
65
+ const _outputRef = outputRef;
66
+ const _rootRef = ref();
67
+ const currentZoom = zoom();
68
+
69
+ if (!_outputRef || !_rootRef) return;
70
+
71
+ const rootRect = _rootRef.getBoundingClientRect();
72
+ const outputRect = _outputRef.getBoundingClientRect();
73
+ const inputRect = _inputRef?.getBoundingClientRect();
74
+
75
+ const outputOffsetX =
76
+ (outputRect.left - rootRect.left + outputRect.width / 2) /
77
+ currentZoom;
78
+ const outputOffsetY =
79
+ (outputRect.top - rootRect.top + outputRect.height / 2) /
80
+ currentZoom;
81
+
82
+ const inputOffsetX = inputRect
83
+ ? (inputRect.left - rootRect.left + inputRect.width / 2) /
84
+ currentZoom
85
+ : DEFAULT_INPUT_OFFSET_X;
86
+ const inputOffsetY = inputRect
87
+ ? (inputRect.top - rootRect.top + inputRect.height / 2) / currentZoom
88
+ : DEFAULT_INPUT_OFFSET_Y;
89
+
90
+ const width = rootRect.width / currentZoom;
91
+ const height = rootRect.height / currentZoom;
92
+
93
+ const output = {
94
+ x: props.x + outputOffsetX,
95
+ y: props.y + outputOffsetY,
96
+ };
97
+
98
+ const input = { x: props.x + inputOffsetX, y: props.y + inputOffsetY };
99
+
100
+ setDimensions(
101
+ produce(data => {
102
+ data[props.id] = {
103
+ width,
104
+ height,
105
+ output,
106
+ input,
107
+ outputOffset: { x: outputOffsetX, y: outputOffsetY },
108
+ inputOffset: { x: inputOffsetX, y: inputOffsetY },
109
+ };
110
+ }),
111
+ );
112
+ });
113
+
114
+ const hasParent = useState(service, {
115
+ selector: ({ context: { data } }) => {
116
+ const edges = data?.edges;
117
+ if (!edges) return false;
118
+ return Object.values(edges).some(edge => edge.to === props.id);
119
+ },
120
+ });
121
+
122
+ const draggable = createDraggable(props.id);
123
+ void draggable;
124
+
125
+ return (
126
+ <div
127
+ ref={setRef}
128
+ id={props.id}
129
+ classList={{
130
+ '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': true,
131
+ 'border border-[#e38c29] z-[100]': selected(),
132
+ 'border border-[#e6d4be] z-[1]': !selected(),
133
+ }}
134
+ style={{ top: `${props.y}px`, left: `${props.x}px` }}
135
+ onMouseDown={e => {
136
+ e.stopPropagation();
137
+ e.stopImmediatePropagation();
138
+ service.send({ type: 'SELECT', payload: props.id });
139
+ }}
140
+ class='min-w-48'
141
+
142
+ use:draggable={{ skipTransform: true }}
143
+ >
144
+ <div
145
+ classList={{
146
+ 'pointer-events-none absolute flex items-center justify-end -top-7.5 right-0 transition-all duration-200 ease-in-out space-x-2': true,
147
+ 'w-full opacity-100': selected(),
148
+ 'w-0 -right-3 opacity-0 overflow-hidden': !selected(),
149
+ }}
150
+ >
151
+ <svg
152
+ class='cursor-pointer rounded-full fill-[#a11111] opacity-100 transition-all duration-200 ease-in-out'
153
+ onClick={e => {
154
+ e.stopPropagation();
155
+ service.send({ type: 'DELETE', payload: props.id });
156
+ }}
157
+ fill='currentColor'
158
+ stroke-width='2'
159
+ viewBox='4 4 16 16'
160
+ style={{
161
+ overflow: 'visible',
162
+ 'pointer-events': 'all',
163
+ width: `${HANDLE_SIZE * 2}px`,
164
+ height: `${HANDLE_SIZE * 2}px`,
165
+ }}
166
+ >
167
+ <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' />
168
+ </svg>
169
+ <Show when={hasParent()}>
170
+ <svg
171
+ class='cursor-pointer overflow-visible rounded-full bg-green-500 p-0.5 text-center font-bold hover:bg-green-600'
172
+ style={{
173
+ 'pointer-events': 'all',
174
+ 'fill-rule': 'evenodd',
175
+ 'clip-rule': 'evenodd',
176
+ 'stroke-linejoin': 'round',
177
+ 'stroke-miterlimit': '2',
178
+ width: `${HANDLE_SIZE * 2}px`,
179
+ height: `${HANDLE_SIZE * 2}px`,
180
+ }}
181
+ viewBox='0 0 1024 1024'
182
+ preserveAspectRatio='xMaxYMax'
183
+ xmlns='http://www.w3.org/2000/svg'
184
+ fill='white'
185
+ onClick={() =>
186
+ service.send({ type: 'ADD_SIBLING', payload: props.id })
187
+ }
188
+ >
189
+ <g id='background'>
190
+ <path d='M467.40667,277.66696c-0.05948,-14.53055 5.75527,-22.95613 -8.62044,-20.90487c-112.55699,16.0607 -222.1609,112.14558 -245.06161,239.85765c-46.52056,259.43466 231.33083,443.06705 449.51209,316.97506c117.31668,-67.80002 160.95215,-190.43324 151.34416,-288.29849c-5.92276,-60.32819 -27.80273,-107.95668 -53.44246,-144.25469l59.39269,-42.05363c111.72214,156.309 73.11535,351.55635 -25.06953,459.45565c-184.18877,202.4124 -470.46624,145.52064 -592.95027,-32.92123c-156.18269,-227.53604 -27.15324,-543.64371 261.18883,-582.44416c5.0579,-0.68061 3.56556,-7.04079 3.56442,-8.58985c-0.05594,-76.3354 -0.11021,-76.7687 1.10909,-77.24589c2.06886,-0.80969 151.41433,118.4561 151.92482,118.95524c4.65592,4.55233 -0.99548,7.829 -29.07828,30.50907c-120.49369,97.31245 -120.4977,98.55675 -123.0691,97.87586c-0.43639,-0.11555 -0.80698,-0.31322 -0.74442,-66.91571Z' />
191
+ <path d='M316.61562,611.03414c0.11517,-90.16257 -0.25516,-99.92912 0.64739,-101.78856c1.56486,-3.22393 131.99102,0.91032 134.6959,-1.89763c2.21336,-2.2977 -0.59362,-129.97807 1.1376,-132.37034c0.7253,-1.00225 11.10552,-0.99175 12.07474,-0.99077c104.50336,0.10564 104.90098,-0.37811 105.967,0.85765c1.56461,1.81373 0.25596,114.18436 0.67852,129.81412c0.1322,4.88975 3.22386,3.28152 99.72028,3.4563c33.0841,0.05992 36.14259,-1.40368 36.21852,4.50462c0.11713,9.11348 1.41954,110.45369 -0.40274,113.92715c-1.81106,3.45208 -130.39967,-0.42618 -134.48289,1.62075c-2.29035,1.14816 -0.36989,101.12392 -1.11542,130.51904c-0.09548,3.76459 -2.13506,3.20617 -47.84854,3.17365c-69.30253,-0.0493 -69.31099,-0.25627 -69.65762,-0.42191c-3.77927,-1.80595 0.16287,-129.33555 -2.24266,-132.58994c-1.79931,-2.43424 -124.06108,-0.29118 -132.80252,-0.97392c-3.81102,-0.29766 -2.58229,-14.8847 -2.58758,-16.8402Z' />
192
+ </g>
193
+ </svg>
194
+ </Show>
195
+ <svg
196
+ 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'
197
+ onClick={() =>
198
+ service.send({ type: 'ADD_CHILD', payload: props.id })
199
+ }
200
+ style={{
201
+ 'pointer-events': 'all',
202
+ width: `${HANDLE_SIZE * 2}px`,
203
+ height: `${HANDLE_SIZE * 2}px`,
204
+ }}
205
+ viewBox='0 0 24 24'
206
+ stroke='currentColor'
207
+ stroke-width='2'
208
+ >
209
+ <path d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z' />
210
+ </svg>
211
+ </div>
212
+ <Show when={props.label} keyed>
213
+ {label => (
214
+ <span class='min-w-max border-b border-[#f0f0f0] p-3 whitespace-nowrap text-red-600 select-none'>
215
+ {label}
216
+ </span>
217
+ )}
218
+ </Show>
219
+ <Show when={props.content} keyed>
220
+ {content => <div class='p-3 select-none'>{content}</div>}
221
+ </Show>
222
+ <Show when={props.input || hasParent()}>
223
+ <div
224
+ id='outputs'
225
+ class='pointer-events-none absolute top-0 z-10 flex cursor-default flex-col'
226
+ style={{ left: `-${HANDLE_CONTAINER_OFFSET_X}px` }}
227
+ >
228
+ <div
229
+ ref={el => (inputRef = el)}
230
+ class='cursor-default rounded-full bg-[#e38b29] shadow-md'
231
+ style={{
232
+ width: `${HANDLE_SIZE}px`,
233
+ height: `${HANDLE_SIZE}px`,
234
+ 'margin-top': `${HANDLE_MARGIN_TOP}px`,
235
+ 'pointer-events': 'all',
236
+ }}
237
+ onMouseDown={event => {
238
+ event.stopPropagation();
239
+ }}
240
+ onMouseUp={event => {
241
+ event.stopPropagation();
242
+ const from = newEdge()?.from;
243
+
244
+ if (from) {
245
+ service.send({
246
+ type: 'ADD_EDGE',
247
+ payload: { from, to: props.id },
248
+ });
249
+ }
250
+
251
+ setNewEdge();
252
+ }}
253
+ ></div>
254
+ </div>
255
+ </Show>
256
+ <div
257
+ id='inputs'
258
+ class='pointer-events-none absolute top-0 z-10 flex flex-col'
259
+ style={{ right: `-${HANDLE_CONTAINER_OFFSET_X}px` }}
260
+ >
261
+ <div
262
+ ref={el => (outputRef = el)}
263
+ class='cursor-crosshair rounded-full bg-[#e38b29] shadow-md'
264
+ style={{
265
+ width: `${HANDLE_SIZE}px`,
266
+ height: `${HANDLE_SIZE}px`,
267
+ 'margin-top': `${HANDLE_MARGIN_TOP}px`,
268
+ 'pointer-events': 'all',
269
+ }}
270
+ onMouseDown={event => {
271
+ event.stopPropagation();
272
+ service.send('DESELECT');
273
+ const output = dimensions()[props.id]?.output;
274
+ const boardPoint = getBoardPoint(event.clientX, event.clientY);
275
+ if (output)
276
+ setNewEdge({
277
+ x0: output.x,
278
+ y0: output.y,
279
+ x1: boardPoint.x,
280
+ y1: boardPoint.y,
281
+ from: props.id,
282
+ });
283
+ }}
284
+ ></div>
285
+ </div>
286
+ </div>
287
+ );
288
+ };
@@ -0,0 +1,298 @@
1
+ import { useState } from '@bemedev/app-solidjs';
2
+ import {
3
+ DragDropProvider,
4
+ DragDropSensors,
5
+ DragOverlay,
6
+ } from '@thisbeyond/solid-dnd';
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';
17
+ import { DragBounds } from './Bounds';
18
+ import { EdgesBoard } from './EdgesBoard';
19
+ import { useFlow } from './FlowChart.context';
20
+ import { NodeComponent } from './NodeComponent';
21
+ import { CANVAS_FACTOR } from './FlowChart.data';
22
+
23
+ /**
24
+ * Interactive board component containing the drag-drop viewport, zoom
25
+ * controls, panning gestures, and rendered nodes/edges.
26
+ *
27
+ * @returns The rendered Solid component.
28
+ */
29
+ export const NodesBoard: Component = () => {
30
+ let containerRef: HTMLDivElement | undefined;
31
+ const [isPanning, setIsPanning] = createSignal(false);
32
+ const [transform, setTransform] = createSignal({ x: 0, y: 0 });
33
+ const [id, setId] = createSignal<string | number>('');
34
+ const [previousZoom, setPreviousZoom] = createSignal<number>();
35
+ let cleanupPanning = () => {};
36
+ onCleanup(cleanupPanning);
37
+ let percentX = 0;
38
+ let percentY = 0;
39
+
40
+ const {
41
+ board: [, setRef],
42
+ service,
43
+ newEdge: [newEdge],
44
+ zoom: [zoom, setZoom],
45
+ } = useFlow();
46
+
47
+ const updateScrollPercentages = () => {
48
+ if (!containerRef) return;
49
+ const maxScrollX = containerRef.scrollWidth - containerRef.clientWidth;
50
+ const maxScrollY =
51
+ containerRef.scrollHeight - containerRef.clientHeight;
52
+ percentX = maxScrollX > 0 ? containerRef.scrollLeft / maxScrollX : 0;
53
+ percentY = maxScrollY > 0 ? containerRef.scrollTop / maxScrollY : 0;
54
+ };
55
+
56
+ createEffect(
57
+ on(
58
+ zoom,
59
+ () => {
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;
67
+ if (maxScrollY > 0) containerRef.scrollTop = percentY * maxScrollY;
68
+ },
69
+ { defer: true },
70
+ ),
71
+ );
72
+
73
+ const selectedId = useState(service, {
74
+ selector: s => s.context?.selected,
75
+ });
76
+
77
+ const selected = (id: string | number) => selectedId() === id;
78
+
79
+ const nodes = useState(service, {
80
+ selector: s => {
81
+ const list = s.context.data?.nodes ?? [];
82
+ return list.map(item => ({
83
+ id: item.id,
84
+ x: item.position.x,
85
+ y: item.position.y,
86
+ label: item.data.label,
87
+ content: item.data.content ?? '',
88
+ input: item.input,
89
+ }));
90
+ },
91
+ equals: dequal,
92
+ });
93
+
94
+ const cDim = () => {
95
+ const _zoom = zoom();
96
+ if (_zoom < 1) {
97
+ return CANVAS_FACTOR * 100 * _zoom;
98
+ }
99
+ return CANVAS_FACTOR * 100;
100
+ };
101
+
102
+ 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)`,
120
+ );
121
+
122
+ service.send({
123
+ type: 'MOVE_IMMEDIATE',
124
+ payload: { id: `${id}`, x, y },
125
+ });
126
+ setTransform({ ..._transform });
127
+ }
128
+ }}
129
+
130
+ onDragEnd={({ draggable: { node, id } }) => {
131
+ if (!selected(id)) return;
132
+
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');
138
+
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
+ ),
159
+ );
160
+ }
161
+ }}
162
+
163
+ class='relative mx-auto h-[calc(100vh-64px)] w-[calc(100vw-32px)]'
164
+ >
165
+ <div
166
+ ref={el => {
167
+ return (containerRef = el);
168
+ }}
169
+ onScroll={updateScrollPercentages}
170
+ class='relative h-full w-full overflow-scroll rounded-lg border-2 border-gray-600'
171
+ >
172
+ <DragDropSensors />
173
+ <DragBounds />
174
+ <div
175
+ ref={setRef}
176
+ class='relative cursor-crosshair'
177
+ 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
+ }}
184
+ onMouseDown={e => {
185
+ if (newEdge() || e.button !== 0) return;
186
+ if (!containerRef) return;
187
+
188
+ service.send('DESELECT');
189
+
190
+ // #region Props
191
+ const startX = e.clientX;
192
+ const startY = e.clientY;
193
+ const startScrollLeft = containerRef.scrollLeft;
194
+ const startScrollTop = containerRef.scrollTop;
195
+ // #endregion
196
+
197
+ setIsPanning(true);
198
+
199
+ const handleMouseMove = (moveEvent: MouseEvent) => {
200
+ if (!containerRef) return;
201
+ const dx = moveEvent.clientX - startX;
202
+ const dy = moveEvent.clientY - startY;
203
+ containerRef.scrollLeft = startScrollLeft - dx * 3;
204
+ containerRef.scrollTop = startScrollTop - dy * 3;
205
+ updateScrollPercentages();
206
+ };
207
+
208
+ const handleMouseUp = () => {
209
+ window.removeEventListener('mousemove', handleMouseMove);
210
+ window.removeEventListener('mouseup', handleMouseUp);
211
+ setTimeout(() => setIsPanning(false), 200);
212
+ (cleanupPanning as any) = undefined;
213
+ };
214
+
215
+ // #region Attach Windows listeners
216
+ cleanupPanning = handleMouseUp;
217
+ window.addEventListener('mousemove', handleMouseMove);
218
+ window.addEventListener('mouseup', handleMouseUp);
219
+ // #endregion
220
+ }}
221
+ >
222
+ <EdgesBoard />
223
+ <For each={nodes()} children={NodeComponent} />
224
+ </div>
225
+ </div>
226
+
227
+ {/* Panel */}
228
+ <div class='absolute right-4 bottom-4 z-50 flex items-center gap-2 rounded-xl border border-gray-200 bg-white/90 p-2 shadow-lg backdrop-blur-md'>
229
+ <button
230
+ type='button'
231
+ class='flex size-9 cursor-pointer items-center justify-center rounded-lg bg-gray-100 text-lg font-bold text-gray-700 shadow-sm transition-all duration-150 hover:bg-gray-200 active:scale-95'
232
+ onClick={() => {
233
+ updateScrollPercentages();
234
+ setPreviousZoom(undefined);
235
+ setZoom(prev =>
236
+ Math.max(0.2, Number((prev - 0.1).toFixed(2))),
237
+ );
238
+ }}
239
+ title='Zoom out'
240
+ aria-label='Zoom out'
241
+ >
242
+ -
243
+ </button>
244
+ <button
245
+ type='button'
246
+ class='h-9 cursor-pointer rounded-lg px-2 text-xs font-semibold text-gray-700 transition-colors hover:bg-gray-100'
247
+ onClick={() => {
248
+ updateScrollPercentages();
249
+ if (previousZoom()) {
250
+ setZoom(previousZoom()!);
251
+ setPreviousZoom(undefined);
252
+ } else {
253
+ setPreviousZoom(zoom());
254
+ setZoom(1);
255
+ }
256
+ }}
257
+ title='Reset zoom'
258
+ aria-label='Reset zoom'
259
+ >
260
+ {Math.round(zoom() * 100)}%
261
+ </button>
262
+ <button
263
+ type='button'
264
+ class='flex size-9 cursor-pointer items-center justify-center rounded-lg bg-gray-100 text-lg font-bold text-gray-700 shadow-sm transition-all duration-150 hover:bg-gray-200 active:scale-95'
265
+ onClick={() => {
266
+ updateScrollPercentages();
267
+ setPreviousZoom(undefined);
268
+ setZoom(prev =>
269
+ Math.min(3, Number((prev + 0.1).toFixed(2))),
270
+ );
271
+ }}
272
+ title='Zoom in'
273
+ aria-label='Zoom in'
274
+ >
275
+ +
276
+ </button>
277
+ <div class='h-5 w-px bg-gray-300' />
278
+
279
+ <button
280
+ type='button'
281
+ class='flex size-9 cursor-pointer items-center justify-center rounded-lg bg-blue-600 text-white shadow transition-all duration-150 hover:bg-blue-700 active:scale-95'
282
+ onClick={() => service.send('ADD_PARENT')}
283
+ title='Add parent node'
284
+ aria-label='Add parent node'
285
+ >
286
+ <svg class='size-5' viewBox='0 0 24 24' fill='currentColor'>
287
+ <path d='M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z' />
288
+ </svg>
289
+ </button>
290
+ </div>
291
+ </div>
292
+
293
+ <Show when={!id() || !selected(id())}>
294
+ <DragOverlay children='' />
295
+ </Show>
296
+ </DragDropProvider>
297
+ );
298
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * List of Tailwind CSS class names used across flowchart UI components to
3
+ * ensure compilation safelist.
4
+ */
5
+ export const CLASSES = [
6
+ 'fill-transparent',
7
+ 'cursor-pointer',
8
+ 'relative',
9
+ 'stroke-[rgba(168,168,168,0.4)]',
10
+ 'stroke-3',
11
+ 'stroke-[rgba(168,168,168,1)]',
12
+ 'stroke-4',
13
+ 'z-100',
14
+ 'stroke-[rgba(168,168,168,0.8)]',
15
+ 'pointer-events-all',
16
+ 'w-[100px]',
17
+ 'h-[100px]',
18
+ 'bg-white',
19
+ 'fill-white',
20
+ 'pointer-events-none',
21
+ 'absolute',
22
+ 'top-0',
23
+ 'w-full',
24
+ 'h-full',
25
+ 'overflow-hidden',
26
+ 'overflow-scroll',
27
+ 'h-[150vh]',
28
+ 'w-[2160px]',
29
+ 'bg-size-[30px_30px]',
30
+ 'flex',
31
+ 'flex-col',
32
+ 'cursor-grab',
33
+ 'rounded-md',
34
+ 'shadow-[1px_1px_11px_-6px_rgba(0,0,0,0.75)]',
35
+ 'select-none',
36
+ 'transition-[border,box-shadow]',
37
+ 'duration-200',
38
+ 'ease-in-out',
39
+ 'hover:shadow-[2px_2px_12px_-6px_rgba(0,0,0,0.75)]',
40
+ 'draggable',
41
+ 'border',
42
+ 'border-[#e38c29]',
43
+ 'z-[100]',
44
+ 'border-[#e6d4be]',
45
+ 'z-[1]',
46
+ 'items-center',
47
+ 'justify-end',
48
+ '-top-[30px]',
49
+ 'right-0',
50
+ 'transition-all',
51
+ 'space-x-2',
52
+ 'opacity-100',
53
+ 'w-0',
54
+ '-right-3',
55
+ 'opacity-0',
56
+ 'w-6',
57
+ 'h-6',
58
+ 'fill-[#a11111]',
59
+ 'rounded-full',
60
+ 'size-6',
61
+ 'bg-green-500',
62
+ 'p-0.5',
63
+ 'hover:bg-green-600',
64
+ 'font-bold',
65
+ 'text-center',
66
+ 'overflow-visible',
67
+ 'bg-blue-500',
68
+ 'text-white',
69
+ 'rounded-lg',
70
+ 'hover:bg-blue-600',
71
+ 'justify-center',
72
+ 'p-3',
73
+ 'border-b',
74
+ 'border-[#f0f0f0]',
75
+ 'cursor-default',
76
+ 'z-[-3]',
77
+ '-left-[18px]',
78
+ 'bg-[#e38b29]',
79
+ 'w-3',
80
+ 'h-3',
81
+ 'my-3',
82
+ '-right-[18px]',
83
+ 'cursor-crosshair',
84
+ 'mt-3',
85
+ ];