@bemedev/mind-flow 1.2.0 → 1.4.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.
package/lib/server.es.js CHANGED
@@ -272,6 +272,15 @@ var CLASSES = [
272
272
  ];
273
273
  //#endregion
274
274
  //#region src/helpers/clamp.ts
275
+ /**
276
+ * Restricts a number to be within the specified minimum and maximum bounds.
277
+ *
278
+ * @param value - The numerical value to clamp.
279
+ * @param min - The lower boundary limit.
280
+ * @param max - The upper boundary limit.
281
+ *
282
+ * @returns The clamped numerical value between `min` and `max`.
283
+ */
275
284
  var clamp = (value, min, max) => {
276
285
  return Math.min(Math.max(value, min), max);
277
286
  };
@@ -1005,13 +1014,6 @@ var _tmpl$2$5 = [
1005
1014
  *
1006
1015
  * @returns Offset distance in pixels for bezier curvature.
1007
1016
  */
1008
- /**
1009
- * Computes bezier curve control point offset based on horizontal distance.
1010
- *
1011
- * @param value - Absolute horizontal delta between start and end points.
1012
- *
1013
- * @returns Offset distance in pixels for bezier curvature.
1014
- */
1015
1017
  var calculateOffset = (value) => value * 100 / 200;
1016
1018
  /**
1017
1019
  * Generates an SVG cubic bezier path `d` string between coordinates `(x0, y0)` and
@@ -1132,12 +1134,14 @@ var _tmpl$2$4 = [
1132
1134
  "<!--/--><svg 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\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\" style=\"",
1133
1135
  "\"><path d=\"M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z\"></path></svg></div><div>",
1134
1136
  "</div><div id=\"inputs\" class=\"pointer-events-none absolute top-0 z-10 flex cursor-default flex-col\" style=\"",
1135
- "\"><div class=\"cursor-default rounded-full bg-[#e38b29] shadow-md\" style=\"",
1136
- "\"></div></div><div id=\"outputs\" class=\"pointer-events-none absolute top-0 z-10 flex flex-col\" style=\"",
1137
- "\"><div class=\"cursor-crosshair rounded-full bg-[#e38b29] shadow-md\" style=\"",
1137
+ "\"><div data-handle-type=\"input\"",
1138
+ " class=\"cursor-default rounded-full bg-[#e38b29] shadow-md transition-transform duration-150 ease-in-out hover:scale-150\" style=\"",
1139
+ "\"></div></div><div id=\"outputs\" style=\"",
1140
+ "\" class=\"pointer-events-none absolute top-0 z-10 flex flex-col\"><div data-handle-type=\"output\"",
1141
+ " class=\"cursor-crosshair rounded-full bg-[#e38b29] shadow-md transition-transform duration-150 ease-in-out hover:scale-150\" style=\"",
1138
1142
  "\"></div></div></div>"
1139
1143
  ];
1140
- var _tmpl$3$2 = [
1144
+ var _tmpl$3$1 = [
1141
1145
  "<div",
1142
1146
  " class=\"p-3 select-none\"><!--$-->",
1143
1147
  "<!--/--><!--$-->",
@@ -1194,7 +1198,7 @@ var NodeComponent = (props) => {
1194
1198
  if (!edges) return false;
1195
1199
  return edges.some(({ to }) => to === props.id);
1196
1200
  } });
1197
- return ssr(_tmpl$2$4, ssrHydrationKey(), `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 ${selected() ? "border border-[#e38c29] z-100" : ""} ${!selected() ? "border border-[#e6d4be] z-1" : ""} min-w-48`, ssrStyleProperty("top:", `${escape(node().y, true)}px`) + ssrStyleProperty(";left:", `${escape(node().x, true)}px`), ssrAttribute("id", escape(props.id, true), false), `pointer-events-none absolute flex items-center justify-end -top-7.5 right-0 transition-all duration-200 ease-in-out space-x-2 ${selected() ? "w-full opacity-100" : ""} ${!selected() ? "w-0 -right-3 opacity-0 overflow-hidden" : ""}`, ssrStyleProperty("overflow:", "visible") + ssrStyleProperty(";pointer-events:", "all") + ssrStyleProperty(";width:", `${escape(12, true) * 2}px`) + ssrStyleProperty(";height:", `${escape(12, true) * 2}px`), escape(createComponent(Show, {
1201
+ return ssr(_tmpl$2$4, ssrHydrationKey(), `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 ${selected() ? "border border-[#e38c29] z-100" : ""} ${!selected() ? "border border-[#e6d4be] z-1" : ""} group min-w-48`, ssrStyleProperty("top:", `${escape(node().y, true)}px`) + ssrStyleProperty(";left:", `${escape(node().x, true)}px`), ssrAttribute("id", escape(props.id, true), false), `pointer-events-none absolute flex items-center justify-end -top-7.5 right-0 transition-all duration-200 ease-in-out space-x-2 ${selected() ? "w-full opacity-100" : ""} ${!selected() ? "w-0 -right-3 opacity-0 overflow-hidden" : ""}`, ssrStyleProperty("overflow:", "visible") + ssrStyleProperty(";pointer-events:", "all") + ssrStyleProperty(";width:", `${escape(12, true) * 2}px`) + ssrStyleProperty(";height:", `${escape(12, true) * 2}px`), escape(createComponent(Show, {
1198
1202
  get when() {
1199
1203
  return hasParent();
1200
1204
  },
@@ -1206,7 +1210,7 @@ var NodeComponent = (props) => {
1206
1210
  return props.children;
1207
1211
  },
1208
1212
  get fallback() {
1209
- return ssr(_tmpl$3$2, ssrHydrationKey(), escape(createComponent(Show, {
1213
+ return ssr(_tmpl$3$1, ssrHydrationKey(), escape(createComponent(Show, {
1210
1214
  get when() {
1211
1215
  return node().data.label;
1212
1216
  },
@@ -1237,26 +1241,57 @@ var NodeComponent = (props) => {
1237
1241
  return props.children;
1238
1242
  } }, () => node().data));
1239
1243
  }
1240
- })), ssrStyleProperty("left:", `-${escape(18, true)}px`), ssrStyleProperty("width:", `${escape(12, true)}px`) + ssrStyleProperty(";height:", `${escape(12, true)}px`) + ssrStyleProperty(";margin-top:", `${escape(12, true)}px`) + ssrStyleProperty(";pointer-events:", "all"), ssrStyleProperty("right:", `-${escape(18, true)}px`), ssrStyleProperty("width:", `${escape(12, true)}px`) + ssrStyleProperty(";height:", `${escape(12, true)}px`) + ssrStyleProperty(";margin-top:", `${escape(12, true)}px`) + ssrStyleProperty(";pointer-events:", "all"));
1244
+ })), ssrStyleProperty("left:", `-${escape(18, true)}px`), ssrAttribute("data-node-id", escape(props.id, true), false), ssrStyleProperty("width:", `${escape(12, true)}px`) + ssrStyleProperty(";height:", `${escape(12, true)}px`) + ssrStyleProperty(";margin-top:", `${escape(12, true)}px`) + ssrStyleProperty(";pointer-events:", "all"), ssrStyleProperty("right:", `-${escape(18, true)}px`), ssrAttribute("data-node-id", escape(props.id, true), false), ssrStyleProperty("width:", `${escape(12, true)}px`) + ssrStyleProperty(";height:", `${escape(12, true)}px`) + ssrStyleProperty(";margin-top:", `${escape(12, true)}px`) + ssrStyleProperty(";pointer-events:", "all"));
1241
1245
  };
1242
1246
  //#endregion
1247
+ //#region src/ui/utils.ts
1248
+ /**
1249
+ * Merges conditional CSS class names using `clsx` and resolves Tailwind CSS class
1250
+ * conflicts with `twMerge`.
1251
+ *
1252
+ * @param inputs - Variable list of class values, objects, or arrays.
1253
+ *
1254
+ * @returns The resolved single class name string.
1255
+ */
1256
+ function cn(...inputs) {
1257
+ return twMerge(clsx(inputs));
1258
+ }
1259
+ //#endregion
1243
1260
  //#region src/ui/components/Panels.tsx
1244
1261
  var _tmpl$$7 = [
1245
1262
  "<div",
1246
- " class=\"absolute top-4 left-4 z-50\">",
1263
+ " class=\"",
1264
+ "\">",
1247
1265
  "</div>"
1248
1266
  ];
1249
1267
  var _tmpl$2$3 = [
1250
1268
  "<div",
1251
- " class=\"absolute top-4 right-4 z-50\">",
1252
- "</div>"
1253
- ];
1254
- var _tmpl$3$1 = [
1255
- "<div",
1256
- " class=\"absolute bottom-4 left-4 z-50\">",
1257
- "</div>"
1269
+ " class=\"pointer-events-none absolute inset-0\"><!--$-->",
1270
+ "<!--/--><!--$-->",
1271
+ "<!--/--><!--$-->",
1272
+ "<!--/--></div>"
1258
1273
  ];
1259
1274
  /**
1275
+ * Overlay slot component rendering an optional panel component with z-index
1276
+ * positioning.
1277
+ *
1278
+ * @param props - Panel properties of type {@linkcode PanelProps}.
1279
+ *
1280
+ * @returns The rendered panel container element or fallback.
1281
+ *
1282
+ * @see {@linkcode cn}
1283
+ */
1284
+ var Panel = (props) => {
1285
+ const exists = isDefined(props.children);
1286
+ return createComponent(Show, {
1287
+ get when() {
1288
+ return props.children;
1289
+ },
1290
+ keyed: true,
1291
+ children: (Children) => ssr(_tmpl$$7, ssrHydrationKey(), `${escape(cn(props.class) || "", true)} ${exists ? "z-50" : ""}`, escape(createComponent(Children, {})))
1292
+ });
1293
+ };
1294
+ /**
1260
1295
  * Overlay container rendering custom panel slots positioned around the flowchart
1261
1296
  * canvas.
1262
1297
  *
@@ -1265,29 +1300,22 @@ var _tmpl$3$1 = [
1265
1300
  * @returns Rendered overlay panels JSX elements.
1266
1301
  */
1267
1302
  var Panels = (props) => {
1268
- return [
1269
- createComponent(Show, {
1270
- get when() {
1271
- return props.topLeft;
1272
- },
1273
- keyed: true,
1274
- children: (Panel) => ssr(_tmpl$$7, ssrHydrationKey(), escape(createComponent(Panel, {})))
1275
- }),
1276
- createComponent(Show, {
1277
- get when() {
1278
- return props.topRight;
1279
- },
1280
- keyed: true,
1281
- children: (Panel) => ssr(_tmpl$2$3, ssrHydrationKey(), escape(createComponent(Panel, {})))
1282
- }),
1283
- createComponent(Show, {
1284
- get when() {
1285
- return props.bottomLeft;
1286
- },
1287
- keyed: true,
1288
- children: (Panel) => ssr(_tmpl$3$1, ssrHydrationKey(), escape(createComponent(Panel, {})))
1289
- })
1290
- ];
1303
+ return ssr(_tmpl$2$3, ssrHydrationKey(), escape(createComponent(Panel, {
1304
+ get children() {
1305
+ return props.topLeft;
1306
+ },
1307
+ "class": "absolute top-4 left-4"
1308
+ })), escape(createComponent(Panel, {
1309
+ get children() {
1310
+ return props.topRight;
1311
+ },
1312
+ "class": "absolute top-4 right-4"
1313
+ })), escape(createComponent(Panel, {
1314
+ get children() {
1315
+ return props.bottomLeft;
1316
+ },
1317
+ "class": "absolute bottom-4 left-4"
1318
+ })));
1291
1319
  };
1292
1320
  //#endregion
1293
1321
  //#region src/ui/components/NodesBoard.tsx
@@ -1500,7 +1528,7 @@ var FlowChart = (props) => {
1500
1528
  const primaryNodes = props.config?.nodes ?? DEFAULT_NODES;
1501
1529
  const primaryEdges = props.config?.edges;
1502
1530
  const service = useFlow();
1503
- const hasNewEdge = createState(service, { selector: (s) => !!s.context.newEdge });
1531
+ const fromSignal = createState(service, { selector: (s) => s.context.newEdge?.from });
1504
1532
  onCleanup(service.pause);
1505
1533
  onMount(() => {
1506
1534
  service.resume();
@@ -1513,7 +1541,59 @@ var FlowChart = (props) => {
1513
1541
  }
1514
1542
  });
1515
1543
  });
1516
- return ssr(_tmpl$$5, ssrHydrationKey(), ssrStyleProperty("cursor:", hasNewEdge() ? "inherit" : "crosshair"), escape(createComponent(NodesBoard, {
1544
+ let added = false;
1545
+ let activeHandle = null;
1546
+ const clearActiveHandle = () => {
1547
+ if (activeHandle) {
1548
+ activeHandle.classList.remove("scale-150");
1549
+ activeHandle = null;
1550
+ added = false;
1551
+ }
1552
+ };
1553
+ const handlePointerMove = (e) => {
1554
+ service.send({
1555
+ type: "MOVE_NEW_EDGE",
1556
+ payload: {
1557
+ x: e.clientX,
1558
+ y: e.clientY
1559
+ }
1560
+ });
1561
+ const inputHandle = document.elementsFromPoint(e.clientX, e.clientY).map((el) => el.closest(".rounded-full[data-handle-type=\"input\"]")).find((handle) => Boolean(handle && handle.id !== "inputs"));
1562
+ if (activeHandle && activeHandle !== inputHandle) clearActiveHandle();
1563
+ if (inputHandle) {
1564
+ if (activeHandle !== inputHandle) {
1565
+ inputHandle.classList.add("scale-150");
1566
+ activeHandle = inputHandle;
1567
+ added = true;
1568
+ }
1569
+ }
1570
+ };
1571
+ const createHandlePointerUp = (from) => (e) => {
1572
+ const inputHandle = document.elementsFromPoint(e.clientX, e.clientY).map((el) => el.closest(".rounded-full[data-handle-type=\"input\"]")).find((handle) => Boolean(handle && handle.id !== "inputs"));
1573
+ const to = inputHandle?.getAttribute("data-node-id");
1574
+ if (inputHandle && to) service.send({
1575
+ type: "ADD_EDGE",
1576
+ payload: {
1577
+ from,
1578
+ to
1579
+ }
1580
+ });
1581
+ clearActiveHandle();
1582
+ service.send("CLEAR_NEW_EDGE");
1583
+ };
1584
+ createEffect(() => {
1585
+ const from = fromSignal();
1586
+ if (!from || added) return;
1587
+ const handlePointerUp = createHandlePointerUp(from);
1588
+ window.addEventListener("pointermove", handlePointerMove);
1589
+ window.addEventListener("pointerup", handlePointerUp);
1590
+ onCleanup(() => {
1591
+ clearActiveHandle();
1592
+ window.removeEventListener("pointermove", handlePointerMove);
1593
+ window.removeEventListener("pointerup", handlePointerUp);
1594
+ });
1595
+ });
1596
+ return ssr(_tmpl$$5, ssrHydrationKey(), ssrStyleProperty("cursor:", fromSignal() ? "inherit" : "crosshair"), escape(createComponent(NodesBoard, {
1517
1597
  get panels() {
1518
1598
  return props.panels;
1519
1599
  },
@@ -1523,19 +1603,6 @@ var FlowChart = (props) => {
1523
1603
  })));
1524
1604
  };
1525
1605
  //#endregion
1526
- //#region src/ui/utils.ts
1527
- /**
1528
- * Merges conditional CSS class names using `clsx` and resolves Tailwind CSS class
1529
- * conflicts with `twMerge`.
1530
- *
1531
- * @param inputs - Variable list of class values, objects, or arrays.
1532
- *
1533
- * @returns The resolved single class name string.
1534
- */
1535
- function cn(...inputs) {
1536
- return twMerge(clsx(inputs));
1537
- }
1538
- //#endregion
1539
1606
  //#region src/ui/globals/helpers/espace.ts
1540
1607
  /**
1541
1608
  * Returns a zero-width space if string is empty, preserving layout flow.
@@ -2007,7 +2074,11 @@ var EditPanel = (props) => {
2007
2074
  get when() {
2008
2075
  return hooks.editing();
2009
2076
  },
2010
- children: (node) => ssr(_tmpl$, ssrHydrationKey(), `${escape(cn(`w-80 cursor-default rounded-2xl border border-gray-200 bg-white/95 p-4 shadow-xl backdrop-blur-md transition-all ${props.class ?? ""}`) || "", true)} ${escape(ssrClassList(classList()) || "", true)}`, ssrStyle(props.style), escape(createComponent(Show, {
2077
+ children: (node) => ssr(_tmpl$, ssrHydrationKey(), `${escape(cn(`w-80 cursor-default rounded-2xl border border-gray-200 bg-white/95 p-4 shadow-xl backdrop-blur-md transition-all ${props.class ?? ""}`) || "", true)} ${escape(ssrClassList({
2078
+ ...classList(),
2079
+ "pointer-events-none! -z-10": !hooks.editing(),
2080
+ "pointer-events-all! z-50": !!hooks.editing()
2081
+ }) || "", true)}`, ssrStyle(props.style), escape(createComponent(Show, {
2011
2082
  get when() {
2012
2083
  return props.header;
2013
2084
  },
@@ -1 +1 @@
1
- {"version":3,"file":"EdgeComponent.d.ts","sourceRoot":"","sources":["../../../src/ui/components/EdgeComponent.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAQ,MAAM,UAAU,CAAC;AAM3C,0EAA0E;AAC1E,KAAK,KAAK,GAAG;IACX,qCAAqC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,gEAAgE;IAChE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAkCF;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,CAAC,KAAK,CAwE1C,CAAC"}
1
+ {"version":3,"file":"EdgeComponent.d.ts","sourceRoot":"","sources":["../../../src/ui/components/EdgeComponent.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAQ,MAAM,UAAU,CAAC;AAM3C,0EAA0E;AAC1E,KAAK,KAAK,GAAG;IACX,qCAAqC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,gEAAgE;IAChE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AA2BF;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS,CAAC,KAAK,CAwE1C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"EditPanel.d.ts","sourceRoot":"","sources":["../../../src/ui/components/EditPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAQ,MAAM,UAAU,CAAC;AAE1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAK/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,QAAQ,GAAG,QAAQ,EACrD,OAAO,cAAc,CAAC,CAAC,CAAC,KACvB,GAAG,CAAC,OA+DN,CAAC"}
1
+ {"version":3,"file":"EditPanel.d.ts","sourceRoot":"","sources":["../../../src/ui/components/EditPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAQ,MAAM,UAAU,CAAC;AAE1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAK/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,QAAQ,GAAG,QAAQ,EACrD,OAAO,cAAc,CAAC,CAAC,CAAC,KACvB,GAAG,CAAC,OAoEN,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"FlowChart.d.ts","sourceRoot":"","sources":["../../../src/ui/components/FlowChart.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,GAAG,EAAsB,MAAM,UAAU,CAAC;AAGxD,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAG1E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAGnD,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AAIpC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,QAAQ,GAAG,QAAQ,EACrD,OAAO,SAAS,CAAC,CAAC,CAAC,KAClB,GAAG,CAAC,OAuCN,CAAC"}
1
+ {"version":3,"file":"FlowChart.d.ts","sourceRoot":"","sources":["../../../src/ui/components/FlowChart.tsx"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,GAAG,EAAsB,MAAM,UAAU,CAAC;AAGtE,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAG1E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAGnD,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AAIpC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,QAAQ,GAAG,QAAQ,EACrD,OAAO,SAAS,CAAC,CAAC,CAAC,KAClB,GAAG,CAAC,OAuGN,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"NodeComponent.d.ts","sourceRoot":"","sources":["../../../src/ui/components/NodeComponent.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAO,KAAK,GAAG,EAAQ,MAAM,UAAU,CAAC;AAQ/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAK/D,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,IAAI;IAC9D,qCAAqC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,iEAAiE;IACjE,QAAQ,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,SAAS,QAAQ,GAAG,QAAQ,EACzD,OAAO,kBAAkB,CAAC,CAAC,CAAC,KAC3B,GAAG,CAAC,OAwNN,CAAC"}
1
+ {"version":3,"file":"NodeComponent.d.ts","sourceRoot":"","sources":["../../../src/ui/components/NodeComponent.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAO,KAAK,GAAG,EAAQ,MAAM,UAAU,CAAC;AAQ/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAK/D,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,QAAQ,GAAG,QAAQ,IAAI;IAC9D,qCAAqC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,iEAAiE;IACjE,QAAQ,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,SAAS,QAAQ,GAAG,QAAQ,EACzD,OAAO,kBAAkB,CAAC,CAAC,CAAC,KAC3B,GAAG,CAAC,OA4NN,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"Panels.d.ts","sourceRoot":"","sources":["../../../src/ui/components/Panels.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAEhD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,EAAE,SAAS,CAAC,UAAU,CAkBxC,CAAC"}
1
+ {"version":3,"file":"Panels.d.ts","sourceRoot":"","sources":["../../../src/ui/components/Panels.tsx"],"names":[],"mappings":"AACA,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,UAAU,CAAC;AAGhD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAiCpD;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,EAAE,SAAS,CAAC,UAAU,CAQxC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bemedev/mind-flow",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Flow Chart UI library for Solid.js applications",
5
5
  "keywords": [
6
6
  "diagram",
@@ -65,13 +65,13 @@
65
65
  "@bemedev/app": "^2.5.0",
66
66
  "@bemedev/app-cli": "^2.5.0",
67
67
  "@bemedev/app-solidjs": "^2.5.1",
68
- "@bemedev/dev-utils": "^1.2.0",
68
+ "@bemedev/dev-utils": "^1.3.1",
69
69
  "@rollup/plugin-babel": "^7.1.0",
70
70
  "@rollup/plugin-node-resolve": "^16.0.3",
71
71
  "@size-limit/file": "^13.0.3",
72
72
  "@tailwindcss/cli": "^4.3.3",
73
73
  "@tailwindcss/vite": "^4.3.3",
74
- "@types/node": "^26.3.0",
74
+ "@types/node": "^26.4.0",
75
75
  "@vitest/coverage-v8": "^4.1.11",
76
76
  "glob": "^13.0.6",
77
77
  "globals": "^17.11.0",
@@ -81,7 +81,7 @@
81
81
  "oxlint": "^1.80.0",
82
82
  "pretty-quick": "^4.2.2",
83
83
  "rimraf": "^6.1.3",
84
- "rolldown": "^1.2.5",
84
+ "rolldown": "^1.2.6",
85
85
  "rollup-preset-solid": "^3.0.0",
86
86
  "size-limit": "^13.0.3",
87
87
  "solid-js": "^1.9.15",
@@ -113,7 +113,8 @@
113
113
  "node": ">=22"
114
114
  },
115
115
  "scripts": {
116
- "build": "pnpm rm:lib && pnpm run build:server && pnpm run build:client && pnpm run build:css",
116
+ "build": "pnpm rm:lib && pnpm run build:dev",
117
+ "build:dev": "pnpm run build:server && pnpm run build:client && pnpm run build:css",
117
118
  "build:client": "vite build",
118
119
  "build:server": "vite build --mode server",
119
120
  "build:css": "pnpx @tailwindcss/cli -i ./input.css -o ../lib/output.css --minify --cwd ./src",
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Restricts a number to be within the specified minimum and maximum bounds.
3
+ *
4
+ * @param value - The numerical value to clamp.
5
+ * @param min - The lower boundary limit.
6
+ * @param max - The upper boundary limit.
7
+ *
8
+ * @returns The clamped numerical value between `min` and `max`.
9
+ */
1
10
  export const clamp = (value: number, min: number, max: number) => {
2
11
  return Math.min(Math.max(value, min), max);
3
12
  };
@@ -14,13 +14,6 @@ type Props = {
14
14
  isNew?: boolean;
15
15
  };
16
16
 
17
- /**
18
- * Computes bezier curve control point offset based on horizontal distance.
19
- *
20
- * @param value - Absolute horizontal delta between start and end points.
21
- *
22
- * @returns Offset distance in pixels for bezier curvature.
23
- */
24
17
  /**
25
18
  * Computes bezier curve control point offset based on horizontal distance.
26
19
  *
@@ -25,6 +25,7 @@ export const EditPanel = <D extends NodeData = NodeData>(
25
25
  props: EditPanelProps<D>,
26
26
  ): JSX.Element => {
27
27
  void clickOutside;
28
+ // const editing
28
29
  const hooks = useHook<D>(props.timeout);
29
30
  const classList = () =>
30
31
  props.classList instanceof Function ? props.classList(hooks) : props.classList;
@@ -36,7 +37,11 @@ export const EditPanel = <D extends NodeData = NodeData>(
36
37
  class={cn(
37
38
  `w-80 cursor-default rounded-2xl border border-gray-200 bg-white/95 p-4 shadow-xl backdrop-blur-md transition-all ${props.class ?? ''}`,
38
39
  )}
39
- classList={classList()}
40
+ classList={{
41
+ ...classList(),
42
+ 'pointer-events-none! -z-10': !hooks.editing(),
43
+ 'pointer-events-all! z-50': !!hooks.editing(),
44
+ }}
40
45
  style={props.style}
41
46
  onMouseDown={e => e.stopPropagation()}
42
47
  use:clickOutside={hooks.close}
@@ -1,5 +1,5 @@
1
1
  import { createState } from '@bemedev/app-solidjs';
2
- import { type JSX, onCleanup, onMount } from 'solid-js';
2
+ import { createEffect, type JSX, onCleanup, onMount } from 'solid-js';
3
3
 
4
4
  import { DEFAULT_NODES } from '#services/main.machine.data';
5
5
  import type { NodeData, NodeProps } from '#services/main.machine.typings';
@@ -32,7 +32,9 @@ export const FlowChart = <D extends NodeData = NodeData>(
32
32
  const primaryNodes = props.config?.nodes ?? DEFAULT_NODES;
33
33
  const primaryEdges = props.config?.edges;
34
34
  const service = useFlow();
35
- const hasNewEdge = createState(service, { selector: s => !!s.context.newEdge });
35
+ const fromSignal = createState(service, {
36
+ selector: s => s.context.newEdge?.from,
37
+ });
36
38
  onCleanup(service.pause);
37
39
 
38
40
  onMount(() => {
@@ -47,21 +49,83 @@ export const FlowChart = <D extends NodeData = NodeData>(
47
49
  });
48
50
  });
49
51
 
52
+ let added = false;
53
+
54
+ // Track the currently enlarged handle during edge drag
55
+ let activeHandle: HTMLElement | null = null;
56
+
57
+ const clearActiveHandle = () => {
58
+ if (activeHandle) {
59
+ activeHandle.classList.remove('scale-150');
60
+ activeHandle = null;
61
+ added = false;
62
+ }
63
+ };
64
+
65
+ const handlePointerMove = (e: MouseEvent | PointerEvent) => {
66
+ service.send({ type: 'MOVE_NEW_EDGE', payload: { x: e.clientX, y: e.clientY } });
67
+
68
+ const elements = document.elementsFromPoint(e.clientX, e.clientY);
69
+ const inputHandle = elements
70
+ .map(el => el.closest<HTMLElement>('.rounded-full[data-handle-type="input"]'))
71
+ .find((handle): handle is HTMLElement =>
72
+ Boolean(handle && handle.id !== 'inputs'),
73
+ );
74
+
75
+ // If cursor moved away from previous handle, reset its scale
76
+ if (activeHandle && activeHandle !== inputHandle) {
77
+ clearActiveHandle();
78
+ }
79
+
80
+ // If cursor entered a valid target handle, enlarge it
81
+ if (inputHandle) {
82
+ if (activeHandle !== inputHandle) {
83
+ inputHandle.classList.add('scale-150');
84
+ activeHandle = inputHandle;
85
+ added = true;
86
+ }
87
+ }
88
+ };
89
+
90
+ const createHandlePointerUp = (from: string) => (e: MouseEvent | PointerEvent) => {
91
+ const elements = document.elementsFromPoint(e.clientX, e.clientY);
92
+ const inputHandle = elements
93
+ .map(el => el.closest<HTMLElement>('.rounded-full[data-handle-type="input"]'))
94
+ .find((handle): handle is HTMLElement =>
95
+ Boolean(handle && handle.id !== 'inputs'),
96
+ );
97
+ const to = inputHandle?.getAttribute('data-node-id');
98
+
99
+ if (inputHandle && to) {
100
+ service.send({ type: 'ADD_EDGE', payload: { from, to } });
101
+ }
102
+
103
+ // Cleanup on release
104
+ clearActiveHandle();
105
+ service.send('CLEAR_NEW_EDGE');
106
+ };
107
+
108
+ createEffect(() => {
109
+ const from = fromSignal();
110
+ if (!from || added) return;
111
+
112
+ const handlePointerUp = createHandlePointerUp(from);
113
+ window.addEventListener('pointermove', handlePointerMove);
114
+ window.addEventListener('pointerup', handlePointerUp);
115
+
116
+ onCleanup(() => {
117
+ // Cleanup on effect disposal
118
+ clearActiveHandle();
119
+ window.removeEventListener('pointermove', handlePointerMove);
120
+ window.removeEventListener('pointerup', handlePointerUp);
121
+ });
122
+ });
123
+
50
124
  return (
51
- <div
52
- class='relative h-full w-full'
53
- onMouseUp={() => service.send('CLEAR_NEW_EDGE')}
54
- onMouseMove={({ clientX, clientY }) => {
55
- if (hasNewEdge())
56
- service.send({
57
- type: 'MOVE_NEW_EDGE',
58
- payload: { x: clientX, y: clientY },
59
- });
60
- }}
61
- >
125
+ <div class='relative h-full w-full'>
62
126
  <div
63
127
  class='h-full w-full'
64
- style={{ cursor: hasNewEdge() ? 'inherit' : 'crosshair' }}
128
+ style={{ cursor: fromSignal() ? 'inherit' : 'crosshair' }}
65
129
  >
66
130
  <NodesBoard panels={props.panels} component={props.component} />
67
131
  </div>
@@ -78,7 +78,7 @@ export const NodeComponent = <D extends NodeData = NodeData>(
78
78
  }}
79
79
 
80
80
  style={{ top: `${node().y}px`, left: `${node().x}px` }}
81
- class='min-w-48'
81
+ class='group min-w-48'
82
82
  use:draggable={{ skipTransform: true }}
83
83
  id={props.id}
84
84
 
@@ -204,7 +204,12 @@ export const NodeComponent = <D extends NodeData = NodeData>(
204
204
  style={{ left: `-${HANDLE_CONTAINER_OFFSET_X}px` }}
205
205
  >
206
206
  <div
207
- class='cursor-default rounded-full bg-[#e38b29] shadow-md'
207
+ data-handle-type='input'
208
+ data-node-id={props.id}
209
+ onMouseDown={event => event.stopPropagation()}
210
+ onPointerDown={e => e.stopPropagation()}
211
+
212
+ class='cursor-default rounded-full bg-[#e38b29] shadow-md transition-transform duration-150 ease-in-out hover:scale-150'
208
213
 
209
214
  style={{
210
215
  width: `${HANDLE_SIZE}px`,
@@ -213,43 +218,42 @@ export const NodeComponent = <D extends NodeData = NodeData>(
213
218
  'pointer-events': 'all',
214
219
  }}
215
220
 
216
- onMouseDown={event => {
217
- event.stopPropagation();
218
- }}
219
-
220
- onPointerDown={e => e.stopPropagation()}
221
-
222
221
  onMouseUp={event => {
223
222
  event.stopPropagation();
224
223
  const from = newEdge()?.from;
225
224
 
226
- if (from) {
225
+ if (from && from !== props.id) {
227
226
  service.send({ type: 'ADD_EDGE', payload: { from, to: props.id } });
228
- // service.send('CLEAR_NEW_EDGE');
229
- } else service.send('CLEAR_NEW_EDGE');
227
+ }
228
+ service.send('CLEAR_NEW_EDGE');
230
229
  }}
231
230
  ></div>
232
231
  </div>
233
232
  <div
234
233
  id='outputs'
235
- class='pointer-events-none absolute top-0 z-10 flex flex-col'
234
+
236
235
  style={{ right: `-${HANDLE_CONTAINER_OFFSET_X}px` }}
236
+
237
+ class='pointer-events-none absolute top-0 z-10 flex flex-col'
237
238
  >
238
239
  <div
239
- class='cursor-crosshair rounded-full bg-[#e38b29] shadow-md'
240
-
240
+ data-handle-type='output'
241
+ data-node-id={props.id}
242
+ class='cursor-crosshair rounded-full bg-[#e38b29] shadow-md transition-transform duration-150 ease-in-out hover:scale-150'
241
243
  style={{
242
244
  width: `${HANDLE_SIZE}px`,
243
245
  height: `${HANDLE_SIZE}px`,
244
246
  'margin-top': `${HANDLE_MARGIN_TOP}px`,
245
247
  'pointer-events': 'all',
246
248
  }}
247
-
248
249
  onMouseDown={event => {
249
250
  event.stopPropagation();
250
251
  service.send('DESELECT');
251
252
  service.send({ type: 'START_NEW_EDGE', payload: props.id });
252
253
  }}
254
+ onPointerDown={event => {
255
+ event.stopPropagation();
256
+ }}
253
257
  ></div>
254
258
  </div>
255
259
  </div>
@@ -1,7 +1,40 @@
1
+ import { isDefined } from '@bemedev/app/bemedev';
1
2
  import { Show, type Component } from 'solid-js';
2
3
 
4
+ import { cn } from '../utils';
3
5
  import type { FlowPanels } from './FlowChart.types';
4
6
 
7
+ /** Properties for the internal {@linkcode Panel} wrapper component. */
8
+ type PanelProps = {
9
+ /** Optional component to render inside the panel container. */
10
+ children?: Component;
11
+ /** CSS class names for positioning and layout. */
12
+ class: string;
13
+ };
14
+
15
+ /**
16
+ * Overlay slot component rendering an optional panel component with z-index
17
+ * positioning.
18
+ *
19
+ * @param props - Panel properties of type {@linkcode PanelProps}.
20
+ *
21
+ * @returns The rendered panel container element or fallback.
22
+ *
23
+ * @see {@linkcode cn}
24
+ */
25
+ const Panel: Component<PanelProps> = props => {
26
+ const exists = isDefined(props.children);
27
+ return (
28
+ <Show when={props.children} keyed>
29
+ {Children => (
30
+ <div class={cn(props.class)} classList={{ 'z-50': exists }}>
31
+ <Children />
32
+ </div>
33
+ )}
34
+ </Show>
35
+ );
36
+ };
37
+
5
38
  /**
6
39
  * Overlay container rendering custom panel slots positioned around the flowchart
7
40
  * canvas.
@@ -12,20 +45,10 @@ import type { FlowPanels } from './FlowChart.types';
12
45
  */
13
46
  export const Panels: Component<FlowPanels> = props => {
14
47
  return (
15
- <>
16
- <Show when={props.topLeft} keyed>
17
- {Panel => <div class='absolute top-4 left-4 z-50'>{<Panel />}</div>}
18
- </Show>
19
-
20
- {/* Top-Right Panel */}
21
- <Show when={props.topRight} keyed>
22
- {Panel => <div class='absolute top-4 right-4 z-50'>{<Panel />}</div>}
23
- </Show>
24
-
25
- {/* Bottom-Left Panel */}
26
- <Show when={props.bottomLeft} keyed>
27
- {Panel => <div class='absolute bottom-4 left-4 z-50'>{<Panel />}</div>}
28
- </Show>
29
- </>
48
+ <div class='pointer-events-none absolute inset-0'>
49
+ <Panel children={props.topLeft} class='absolute top-4 left-4' />
50
+ <Panel children={props.topRight} class='absolute top-4 right-4' />
51
+ <Panel children={props.bottomLeft} class='absolute bottom-4 left-4' />
52
+ </div>
30
53
  );
31
54
  };