@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
@@ -1,53 +1,14 @@
1
1
  import { createState } from '@bemedev/app-solidjs';
2
- import type { inferT } from '@bemedev/app/typings';
3
- import { Component, onCleanup, onMount } from 'solid-js';
2
+ import { type JSX, onCleanup, onMount } from 'solid-js';
3
+
4
+ import { DEFAULT_NODES } from '#services/main.machine.data';
5
+ import type { NodeData, NodeProps } from '#services/main.machine.typings';
4
6
 
5
- import { DEFAULT_NODES } from '../../services/main.machine.data';
6
- import type { edgeJSON, nodeJSON } from '../../services/main.machine.typings';
7
7
  import { useFlow } from './FlowChart.context';
8
+ import type { FlowProps } from './FlowChart.types';
8
9
  import { NodesBoard } from './NodesBoard';
9
10
 
10
- /** Serialized node properties type inferred from schema {@linkcode nodeJSON}. */
11
- export type NodeProps = inferT<typeof nodeJSON>;
12
-
13
- /** Serialized edge properties type inferred from schema {@linkcode edgeJSON}. */
14
- export type EdgeProps = inferT<typeof edgeJSON>;
15
-
16
- /**
17
- * Configuration options and callback handlers for the {@linkcode FlowChart}
18
- * component.
19
- */
20
- export type FlowProps = {
21
- /** Initial flowchart state configuration with nodes and edges. */
22
- config?: {
23
- nodes?: (NodeProps & { id: string })[];
24
- edges?: (EdgeProps & { id: string })[];
25
- };
26
- /**
27
- * Callback triggered when a new node is created.
28
- *
29
- * @param node - The created node object of type {@linkcode NodeProps}.
30
- */
31
- onNodeAdded?: (node: NodeProps) => void;
32
- /**
33
- * Callback triggered when a node is deleted.
34
- *
35
- * @param nodeId - The identifier of the deleted node.
36
- */
37
- onNodeDeleted?: (nodeId: string) => void;
38
- /**
39
- * Callback triggered when an edge is created.
40
- *
41
- * @param edge - The created edge object of type {@linkcode EdgeProps}.
42
- */
43
- onEdgeAdded?: (edge: EdgeProps) => void;
44
- /**
45
- * Callback triggered when an edge is deleted.
46
- *
47
- * @param edgeId - The identifier of the deleted edge.
48
- */
49
- onEdgeDeleted?: (edgeId: string) => void;
50
- };
11
+ export type { NodeData, NodeProps };
51
12
 
52
13
  // const PARENT_CHILD_GAP_WIDTH = 75;
53
14
 
@@ -55,6 +16,9 @@ export type FlowProps = {
55
16
  * Flowchart board canvas component that renders interactive nodes, edges, pan/zoom,
56
17
  * and toolbar controls.
57
18
  *
19
+ * @template | {@linkcode NodeData} `D` - Custom node data dictionary type extending
20
+ * {@linkcode NodeData}.
21
+ *
58
22
  * @param props - Flowchart configuration and event handlers of type
59
23
  * {@linkcode FlowProps}.
60
24
  *
@@ -62,45 +26,44 @@ export type FlowProps = {
62
26
  *
63
27
  * @see {@linkcode NodesBoard}, {@linkcode useFlow}, {@linkcode DEFAULT_NODES}
64
28
  */
65
- export const FlowChart: Component<FlowProps> = props => {
29
+ export const FlowChart = <D extends NodeData = NodeData>(
30
+ props: FlowProps<D>,
31
+ ): JSX.Element => {
66
32
  const primaryNodes = props.config?.nodes ?? DEFAULT_NODES;
67
33
  const primaryEdges = props.config?.edges;
68
-
69
34
  const service = useFlow();
70
-
71
35
  const hasNewEdge = createState(service, { selector: s => !!s.context.newEdge });
36
+ onCleanup(service.pause);
72
37
 
73
38
  onMount(() => {
74
39
  service.resume();
75
40
  service.send({
76
41
  type: 'CONFIGURE',
77
- payload: { nodes: primaryNodes, edges: primaryEdges ?? [] },
42
+ payload: {
43
+ nodes: primaryNodes,
44
+ edges: primaryEdges ?? [],
45
+ defaultData: props.defaultData,
46
+ },
78
47
  });
79
48
  });
80
49
 
81
- onCleanup(service.pause);
82
-
83
50
  return (
84
51
  <div
85
52
  class='relative h-full w-full'
86
53
  onMouseUp={() => service.send('CLEAR_NEW_EDGE')}
87
- onMouseMove={event => {
88
- service.send({
89
- type: 'MOVE_NEW_EDGE',
90
- payload: { x: event.clientX, y: event.clientY },
91
- });
54
+ onMouseMove={({ clientX, clientY }) => {
55
+ if (hasNewEdge())
56
+ service.send({
57
+ type: 'MOVE_NEW_EDGE',
58
+ payload: { x: clientX, y: clientY },
59
+ });
92
60
  }}
93
- style={{}}
94
61
  >
95
62
  <div
96
- class='relative h-full w-full bg-white bg-size-[30px_30px]'
97
- style={{
98
- cursor: hasNewEdge() ? 'inherit' : 'crosshair',
99
- 'background-image':
100
- 'radial-gradient(circle, #b8b8b8bf 1px, rgba(0, 0, 0, 0) 1px)',
101
- }}
63
+ class='h-full w-full'
64
+ style={{ cursor: hasNewEdge() ? 'inherit' : 'crosshair' }}
102
65
  >
103
- <NodesBoard />
66
+ <NodesBoard panels={props.panels} component={props.component} />
104
67
  </div>
105
68
  </div>
106
69
  );
@@ -0,0 +1,92 @@
1
+ import type { NotUndefined } from '@bemedev/app/bemedev';
2
+ import type { Component, JSX } from 'solid-js';
3
+
4
+ import type { EdgeProps } from '#services/main.machine.typings';
5
+
6
+ import type { NodeData, NodeProps } from './FlowChart';
7
+
8
+ /** Overlay panel slots positioned around the flowchart canvas. */
9
+ export type FlowPanels = {
10
+ /** Top-left corner overlay panel slot. */
11
+ topLeft?: JSX.Element | Component;
12
+ /** Top-right corner overlay panel slot. */
13
+ topRight?: JSX.Element | Component;
14
+ /** Bottom-left corner overlay panel slot. */
15
+ bottomLeft?: JSX.Element | Component;
16
+ };
17
+
18
+ /**
19
+ * Configuration options and callback handlers for the {@linkcode FlowChart}
20
+ * component.
21
+ *
22
+ * @template | {@linkcode NodeData} `D` - Custom node data dictionary type extending
23
+ * {@linkcode NodeData}.
24
+ */
25
+ export type FlowProps<D extends NodeData = NodeData> = {
26
+ /** Optional delay in milliseconds before mounting the flowchart canvas. */
27
+ delay?: number;
28
+ /** Initial flowchart state configuration with nodes and edges. */
29
+ config?: {
30
+ nodes?: (NodeProps<D> & { id: string })[];
31
+ edges?: (EdgeProps & { id: string })[];
32
+ };
33
+ /** Custom node component to render inside each flowchart node. */
34
+ component?: Component<D>;
35
+
36
+ /** Default data for new nodes created in the flowchart. */
37
+ defaultData?: D;
38
+ /** Custom overlay panels positioned around the canvas. */
39
+ panels?: FlowPanels;
40
+ /**
41
+ * Callback triggered when a new node is created.
42
+ *
43
+ * @param node - The created node object of type {@linkcode NodeProps}.
44
+ */
45
+ onNodeAdded?: (node: NodeProps<D>) => void;
46
+ /**
47
+ * Callback triggered when a node is deleted.
48
+ *
49
+ * @param nodeId - The identifier of the deleted node.
50
+ */
51
+ onNodeDeleted?: (nodeId: string) => void;
52
+ /**
53
+ * Callback triggered when an edge is created.
54
+ *
55
+ * @param edge - The created edge object of type {@linkcode EdgeProps}.
56
+ */
57
+ onEdgeAdded?: (edge: EdgeProps) => void;
58
+ /**
59
+ * Callback triggered when an edge is deleted.
60
+ *
61
+ * @param edgeId - The identifier of the deleted edge.
62
+ */
63
+ onEdgeDeleted?: (edgeId: string) => void;
64
+ };
65
+
66
+ /**
67
+ * Type alias extracting the non-undefined flowchart configuration object.
68
+ *
69
+ * @template | {@linkcode NodeData} `D` - Custom node data dictionary type extending
70
+ * {@linkcode NodeData}.
71
+ */
72
+ export type ConfigFrom<D extends NodeData = NodeData> = NotUndefined<
73
+ FlowProps<D>['config']
74
+ >;
75
+
76
+ /**
77
+ * Type alias extracting the non-undefined list of nodes from flowchart
78
+ * configuration.
79
+ *
80
+ * @template | {@linkcode NodeData} `D` - Custom node data dictionary type extending
81
+ * {@linkcode NodeData}.
82
+ */
83
+ export type NodesFrom<D extends NodeData> = NotUndefined<ConfigFrom<D>['nodes']>;
84
+
85
+ /**
86
+ * Type alias extracting the non-undefined list of edges from flowchart
87
+ * configuration.
88
+ *
89
+ * @template | {@linkcode NodeData} `D` - Custom node data dictionary type extending
90
+ * {@linkcode NodeData}.
91
+ */
92
+ export type EdgesFrom<D extends NodeData> = NotUndefined<ConfigFrom<D>['edges']>;
@@ -1,64 +1,64 @@
1
- /* eslint-disable @typescript-eslint/no-namespace */
2
1
  import { toArray } from '@bemedev/app';
3
2
  import { createState } from '@bemedev/app-solidjs';
4
3
  import { createDraggable } from '@thisbeyond/solid-dnd';
5
4
  import { dequal } from 'dequal';
6
- import { Component, Show } from 'solid-js';
5
+ import { type Component, For, type JSX, Show } from 'solid-js';
6
+ import { Dynamic } from 'solid-js/web';
7
7
 
8
8
  import {
9
9
  HANDLE_CONTAINER_OFFSET_X,
10
10
  HANDLE_MARGIN_TOP,
11
11
  HANDLE_SIZE,
12
12
  } from '#services/main.machine.data';
13
+ import type { NodeData } from '#services/main.machine.typings';
13
14
 
15
+ import { resize } from '../globals/directives';
14
16
  import { useFlow } from './FlowChart.context';
15
17
 
16
- declare module 'solid-js' {
17
- namespace JSX {
18
- interface Directives {
19
- draggable: any;
20
- }
21
- }
22
- }
23
-
24
18
  /** Properties for rendering an individual flowchart node component. */
25
- type Props = {
19
+ export type NodeComponentProps<D extends NodeData = NodeData> = {
26
20
  /** Unique identifier of the node. */
27
21
  id: string;
22
+ /** Custom node component to render inside the node container. */
23
+ children?: Component<D>;
28
24
  };
29
25
 
30
26
  /**
31
27
  * Interactive flowchart node component supporting dragging, selection, handle
32
28
  * connections, and child/sibling creation.
33
29
  *
34
- * @param props - Node rendering properties of type {@linkcode Props}.
30
+ * @template | {@linkcode NodeData} `D` - Custom node data dictionary type extending
31
+ * {@linkcode NodeData}.
32
+ *
33
+ * @param props - Node rendering properties of type {@linkcode NodeComponentProps}.
35
34
  *
36
35
  * @returns The rendered Solid component.
37
36
  *
38
37
  * @see {@linkcode useFlow}, {@linkcode HANDLE_CONTAINER_OFFSET_X}, {@linkcode HANDLE_MARGIN_TOP}, {@linkcode HANDLE_SIZE}
39
38
  */
40
- export const NodeComponent: Component<Props> = props => {
39
+ export const NodeComponent = <D extends NodeData = NodeData>(
40
+ props: NodeComponentProps<D>,
41
+ ): JSX.Element => {
41
42
  const service = useFlow();
43
+ const newEdge = createState(service, { selector: s => s.context.newEdge });
44
+ const draggable = createDraggable(props.id);
45
+ void draggable;
42
46
 
43
47
  const node = createState(service, {
44
48
  selector: ({ context }) => {
45
49
  const list = toArray.typed(context.data?.nodes);
46
- const item = list.find(n => n.id === props.id)!;
50
+ const item = list.find(n => n.id === props.id);
47
51
  return {
48
- x: item.position.x,
49
- y: item.position.y,
50
- label: item.data.label,
51
- content: item.data.content ?? '',
52
- input: item.input,
52
+ x: item?.position.x ?? 0,
53
+ y: item?.position.y ?? 0,
54
+ data: (item?.data ?? {}) as D,
53
55
  };
54
56
  },
55
57
  equals: dequal,
56
58
  });
57
59
 
58
- const newEdge = createState(service, { selector: s => s.context.newEdge });
59
-
60
60
  const selected = createState(service, {
61
- selector: s => s.context.selected === props.id,
61
+ selector: ({ context }) => context.selected === props.id,
62
62
  });
63
63
 
64
64
  const hasParent = createState(service, {
@@ -69,15 +69,12 @@ export const NodeComponent: Component<Props> = props => {
69
69
  },
70
70
  });
71
71
 
72
- const draggable = createDraggable(props.id);
73
- void draggable;
74
-
75
72
  return (
76
73
  <div
77
74
  classList={{
78
75
  '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,
79
- 'border border-[#e38c29] z-[100]': selected(),
80
- 'border border-[#e6d4be] z-[1]': !selected(),
76
+ 'border border-[#e38c29] z-100': selected(),
77
+ 'border border-[#e6d4be] z-1': !selected(),
81
78
  }}
82
79
 
83
80
  style={{ top: `${node().y}px`, left: `${node().x}px` }}
@@ -89,6 +86,10 @@ export const NodeComponent: Component<Props> = props => {
89
86
  e.stopPropagation();
90
87
  service.send({ type: 'SELECT', payload: props.id });
91
88
  }}
89
+ onDblClick={e => {
90
+ e.stopPropagation();
91
+ service.send({ type: 'EDIT', payload: props.id });
92
+ }}
92
93
  >
93
94
  <div
94
95
  classList={{
@@ -121,6 +122,7 @@ export const NodeComponent: Component<Props> = props => {
121
122
  <Show when={hasParent()}>
122
123
  <svg
123
124
  class='cursor-pointer overflow-visible rounded-full bg-green-500 p-0.5 text-center font-bold hover:bg-green-600'
125
+
124
126
  style={{
125
127
  'pointer-events': 'all',
126
128
  'fill-rule': 'evenodd',
@@ -144,6 +146,7 @@ export const NodeComponent: Component<Props> = props => {
144
146
  </g>
145
147
  </svg>
146
148
  </Show>
149
+
147
150
  <svg
148
151
  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'
149
152
  onClick={() => service.send({ type: 'ADD_CHILD', payload: props.id })}
@@ -161,51 +164,71 @@ export const NodeComponent: Component<Props> = props => {
161
164
  </svg>
162
165
  </div>
163
166
 
164
- <Show when={node().label} keyed>
165
- {label => (
166
- <span class='min-w-max border-b border-[#f0f0f0] p-3 whitespace-nowrap text-red-600 select-none'>
167
- {label}
168
- </span>
169
- )}
170
- </Show>
171
-
172
- <Show when={node().content} keyed>
173
- {content => <div class='p-3 select-none'>{content}</div>}
174
- </Show>
167
+ <div ref={resize(props.id)}>
168
+ <Show
169
+ when={props.children}
170
+ fallback={
171
+ <div class='p-3 select-none'>
172
+ <Show when={node().data.label} keyed>
173
+ {label => (
174
+ <span class='mb-2 block min-w-max border-b border-[#f0f0f0] pb-2 font-semibold whitespace-nowrap text-red-600 select-none'>
175
+ {String(label)}
176
+ </span>
177
+ )}
178
+ </Show>
179
+
180
+ <Show when={node().data.content} keyed>
181
+ {content => <div class='select-none'>{String(content)}</div>}
182
+ </Show>
183
+
184
+ <Show when={!node().data.label && !node().data.content}>
185
+ <For each={Object.entries(node().data)}>
186
+ {([key, val]) => (
187
+ <div>
188
+ <span class='font-semibold'>{key}: </span>
189
+ <span>{String(val)}</span>
190
+ </div>
191
+ )}
192
+ </For>
193
+ </Show>
194
+ </div>
195
+ }
196
+ >
197
+ <Dynamic component={props.children} {...node().data} />
198
+ </Show>
199
+ </div>
175
200
 
176
- <Show when={node().input || hasParent()}>
201
+ <div
202
+ id='inputs'
203
+ class='pointer-events-none absolute top-0 z-10 flex cursor-default flex-col'
204
+ style={{ left: `-${HANDLE_CONTAINER_OFFSET_X}px` }}
205
+ >
177
206
  <div
178
- id='outputs'
179
- class='pointer-events-none absolute top-0 z-10 flex cursor-default flex-col'
180
- style={{ left: `-${HANDLE_CONTAINER_OFFSET_X}px` }}
181
- >
182
- <div
183
- class='cursor-default rounded-full bg-[#e38b29] shadow-md'
207
+ class='cursor-default rounded-full bg-[#e38b29] shadow-md'
184
208
 
185
- style={{
186
- width: `${HANDLE_SIZE}px`,
187
- height: `${HANDLE_SIZE}px`,
188
- 'margin-top': `${HANDLE_MARGIN_TOP}px`,
189
- 'pointer-events': 'all',
190
- }}
209
+ style={{
210
+ width: `${HANDLE_SIZE}px`,
211
+ height: `${HANDLE_SIZE}px`,
212
+ 'margin-top': `${HANDLE_MARGIN_TOP}px`,
213
+ 'pointer-events': 'all',
214
+ }}
191
215
 
192
- onMouseDown={event => {
193
- event.stopPropagation();
194
- }}
216
+ onMouseDown={event => {
217
+ event.stopPropagation();
218
+ }}
195
219
 
196
- onMouseUp={event => {
197
- event.stopPropagation();
198
- const from = newEdge()?.from;
220
+ onMouseUp={event => {
221
+ event.stopPropagation();
222
+ const from = newEdge()?.from;
199
223
 
200
- if (from) {
201
- service.send({ type: 'ADD_EDGE', payload: { from, to: props.id } });
202
- } else service.send('CLEAR_NEW_EDGE');
203
- }}
204
- ></div>
205
- </div>
206
- </Show>
224
+ if (from) {
225
+ service.send({ type: 'ADD_EDGE', payload: { from, to: props.id } });
226
+ } else service.send('CLEAR_NEW_EDGE');
227
+ }}
228
+ ></div>
229
+ </div>
207
230
  <div
208
- id='inputs'
231
+ id='outputs'
209
232
  class='pointer-events-none absolute top-0 z-10 flex flex-col'
210
233
  style={{ right: `-${HANDLE_CONTAINER_OFFSET_X}px` }}
211
234
  >
@@ -7,38 +7,55 @@ import {
7
7
  } from '@thisbeyond/solid-dnd';
8
8
  import { dequal } from 'dequal';
9
9
  import {
10
- Component,
10
+ type Component,
11
11
  createEffect,
12
12
  createSignal,
13
13
  For,
14
+ type JSX,
14
15
  on,
15
16
  onMount,
16
17
  Show,
17
18
  } from 'solid-js';
18
19
 
19
20
  import { CANVAS_FACTOR, SCROLL_MULTIPLIER } from '../../services/main.machine.data';
21
+ import type { NodeData } from '../../services/main.machine.typings';
20
22
  import { DragBounds } from './Bounds';
21
23
  import { EdgesBoard } from './EdgesBoard';
22
24
  import { useFlow } from './FlowChart.context';
25
+ import type { FlowPanels } from './FlowChart.types';
23
26
  import { NodeComponent } from './NodeComponent';
27
+ import { Panels } from './Panels';
28
+
29
+ /** Properties for the {@linkcode NodesBoard} component. */
30
+ type Props<D extends NodeData = NodeData> = {
31
+ /** Optional custom node component. */
32
+ component?: Component<D>;
33
+ /** Optional custom overlay panels. */
34
+ panels?: FlowPanels;
35
+ };
24
36
 
25
37
  /**
26
38
  * Interactive board component containing the drag-drop viewport, zoom controls,
27
39
  * panning gestures, and rendered nodes/edges.
28
40
  *
41
+ * @template | {@linkcode NodeData} `D` - Custom node data dictionary type extending
42
+ * {@linkcode NodeData}.
43
+ *
44
+ * @param props - Board component properties of type {@linkcode Props}.
45
+ *
29
46
  * @returns The rendered Solid component.
30
47
  *
31
48
  * @see {@linkcode DragBounds}, {@linkcode EdgesBoard}, {@linkcode NodeComponent}, {@linkcode useFlow}, {@linkcode CANVAS_FACTOR}, {@linkcode SCROLL_MULTIPLIER}
32
49
  */
33
- export const NodesBoard: Component = () => {
50
+ export const NodesBoard = <D extends NodeData = NodeData>(
51
+ props: Props<D>,
52
+ ): JSX.Element => {
34
53
  let containerRef: HTMLDivElement;
35
54
  const [isPanning, setIsPanning] = createSignal(false);
36
55
  const [transform, setTransform] = createSignal({ x: 0, y: 0 });
37
- const [id, setId] = createSignal<string | number>('');
38
56
  const [ref, setRef] = createSignal<HTMLDivElement | undefined>();
39
57
  let percentX = 0;
40
58
  let percentY = 0;
41
-
42
59
  const service = useFlow();
43
60
 
44
61
  const newEdge = createState(service, {
@@ -107,15 +124,6 @@ export const NodesBoard: Component = () => {
107
124
 
108
125
  const selectedId = createState(service, { selector: s => s.context?.selected });
109
126
 
110
- /**
111
- * Determines whether the given element ID matches the currently selected entity.
112
- *
113
- * @param id - The node or edge identifier to check.
114
- *
115
- * @returns `true` if the item is currently selected, otherwise `false`.
116
- */
117
- const selected = (id: string | number) => selectedId() === id;
118
-
119
127
  const nodeIds = createState(service, {
120
128
  selector: ({ context }) => {
121
129
  const list = toArray.typed(context.data?.nodes);
@@ -145,48 +153,42 @@ export const NodesBoard: Component = () => {
145
153
  }
146
154
  }}
147
155
 
148
- class='relative mx-auto h-[calc(100vh-64px)] w-[calc(100vw-32px)] overflow-hidden'
156
+ class='relative mx-auto h-full w-full overflow-hidden'
149
157
  >
150
158
  <DragDropProvider
151
159
  onDragMove={({ draggable: { transform: _transform, node, id } }) => {
152
- setId(id);
153
- if (selected(id)) {
154
- const grandParent = node.parentElement?.parentElement;
155
- const currentZoom = zoom();
156
- const minX = 0;
157
- const maxX = grandParent
158
- ? Math.max(0, grandParent.clientWidth / currentZoom - node.offsetWidth)
159
- : Infinity;
160
- const minY = 0;
161
- const maxY = grandParent
162
- ? Math.max(
163
- 0,
164
- grandParent.clientHeight / currentZoom - node.offsetHeight,
165
- )
166
- : Infinity;
167
-
168
- const targetX = node.offsetLeft + _transform.x / currentZoom;
169
- const targetY = node.offsetTop + _transform.y / currentZoom;
170
-
171
- const x = Math.min(Math.max(targetX, minX), maxX);
172
- const y = Math.min(Math.max(targetY, minY), maxY);
173
-
174
- const deltaX = x - node.offsetLeft;
175
- const deltaY = y - node.offsetTop;
176
-
177
- // Directly update the draggable node's CSS transform adjusted for zoom:
178
- node.style.setProperty(
179
- 'transform',
180
- `translate3d(${deltaX}px, ${deltaY}px, 0)`,
181
- );
182
-
183
- service.send({ type: 'MOVE_IMMEDIATE', payload: { id: `${id}`, x, y } });
184
- setTransform({ x: deltaX * currentZoom, y: deltaY * currentZoom });
185
- }
160
+ const grandParent = node.parentElement?.parentElement;
161
+ const currentZoom = zoom();
162
+ const minX = 0;
163
+ const maxX = grandParent
164
+ ? Math.max(0, grandParent.clientWidth / currentZoom - node.offsetWidth)
165
+ : Infinity;
166
+ const minY = 0;
167
+ const maxY = grandParent
168
+ ? Math.max(0, grandParent.clientHeight / currentZoom - node.offsetHeight)
169
+ : Infinity;
170
+
171
+ const targetX = node.offsetLeft + _transform.x / currentZoom;
172
+ const targetY = node.offsetTop + _transform.y / currentZoom;
173
+
174
+ const x = Math.min(Math.max(targetX, minX), maxX);
175
+ const y = Math.min(Math.max(targetY, minY), maxY);
176
+
177
+ const deltaX = x - node.offsetLeft;
178
+ const deltaY = y - node.offsetTop;
179
+
180
+ // Directly update the draggable node's CSS transform adjusted for zoom:
181
+ node.style.setProperty(
182
+ 'transform',
183
+ `translate3d(${deltaX}px, ${deltaY}px, 0)`,
184
+ );
185
+
186
+ service.send({ type: 'MOVE_IMMEDIATE', payload: { id: `${id}`, x, y } });
187
+ setTransform({ x: deltaX * currentZoom, y: deltaY * currentZoom });
186
188
  }}
187
189
 
188
190
  onDragEnd={({ draggable: { node, id } }) => {
189
- if (!selected(id)) return;
191
+ // if (!selected(id)) return;
190
192
 
191
193
  const grandParent = node.parentElement?.parentElement;
192
194
  const currentZoom = zoom();
@@ -219,7 +221,7 @@ export const NodesBoard: Component = () => {
219
221
  return (containerRef = el);
220
222
  }}
221
223
  onScroll={updateScrollPercentages}
222
- class='relative h-full w-full overflow-auto rounded-lg border-2 border-gray-600'
224
+ class='relative h-full w-full overflow-auto'
223
225
  >
224
226
  <DragDropSensors />
225
227
  <div
@@ -227,11 +229,12 @@ export const NodesBoard: Component = () => {
227
229
  class='relative cursor-crosshair overflow-hidden'
228
230
  classList={{ 'cursor-grabbing': isPanning() }}
229
231
  style={{ height: cHeight(), width: cWidth() }}
230
- onScroll={() => {}}
232
+ // onScroll={() => {}}
231
233
 
232
234
  onMouseDown={e => {
233
235
  if (newEdge() || e.button !== 0) return;
234
236
  service.send('DESELECT');
237
+ setIsPanning(true);
235
238
 
236
239
  // #region Props
237
240
  const startX = e.clientX;
@@ -240,8 +243,6 @@ export const NodesBoard: Component = () => {
240
243
  const startScrollTop = containerRef.scrollTop;
241
244
  // #endregion
242
245
 
243
- setIsPanning(true);
244
-
245
246
  const handleMouseMove = (moveEvent: MouseEvent) => {
246
247
  const dx = moveEvent.clientX - startX;
247
248
  const dy = moveEvent.clientY - startY;
@@ -268,12 +269,16 @@ export const NodesBoard: Component = () => {
268
269
  >
269
270
  <DragBounds />
270
271
  <EdgesBoard />
271
- <For each={nodeIds()}>{id => <NodeComponent id={id} />}</For>
272
+ <For each={nodeIds()}>
273
+ {id => <NodeComponent id={id} children={props.component} />}
274
+ </For>
272
275
  </div>
273
276
  </div>
274
277
  </div>
275
278
 
276
- {/* Panel */}
279
+ <Panels {...props.panels} />
280
+
281
+ {/* Bottom-Right Panel (Main Zoom & Creation Controls) */}
277
282
  <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'>
278
283
  <button
279
284
  type='button'
@@ -325,7 +330,7 @@ export const NodesBoard: Component = () => {
325
330
  </svg>
326
331
  </button>
327
332
  </div>
328
- <Show when={!id() || !selected(id())}>
333
+ <Show when={!selectedId()}>
329
334
  <DragOverlay children='' />
330
335
  </Show>
331
336
  </DragDropProvider>